تقليل الذاكرة

بعد فهم إدارة الذاكرة في Android وإعداد الأدوات اللازمة لقياس استخدام الذاكرة في لعبتك، تتمثّل الخطوة التالية في تقليل استخدام الذاكرة وتحسينه بشكل نشط. يساعد الالتزام بالحدود الصارمة التي يفرضها نظام التشغيل Android في منع النظام من إغلاق لعبتك، ويقلّل من أوقات بدء التشغيل الطويلة، ويضمن تشغيل لعبتك بشكل جيد على جميع الأجهزة.

يقدّم هذا الدليل تقنيات عملية لتقليل استهلاك الذاكرة في لعبتك، مع التركيز بشكل خاص على تحسين مستوى مواد العرض، والإعدادات الخاصة بمحرك الألعاب، وأفضل الممارسات لإدارة الذاكرة.

تقليل استخدام الذاكرة في Unity

بسبب التصميم المعماري لـ Unity، بعد توسيع أدوات تخصيص الحِزم الأصلية وذاكرة التخزين المؤقت المُدارة، يميل المحرّك إلى الاحتفاظ بصفحات الذاكرة هذه لإعادة استخدامها بدلاً من إعادتها فورًا إلى نظام التشغيل، حتى بعد إصدار الأصول. على وجه التحديد، تظل مساحة العناوين الافتراضية (الذاكرة المحجوزة) محجوزة طوال مدة العملية، ولا تتم استعادة الذاكرة الفعلية (RSS) على الفور إلا بعد حدوث عدة دورات لجمع البيانات غير الضرورية (GC) وتقليل حجم الذاكرة. نتيجةً لذلك، يمكن أن تؤدي زيادة مؤقتة في الذاكرة إلى بقاء الذاكرة المقيمة مرتفعة لفترة طويلة حتى بعد انخفاض الاستخدام الفعلي. يزيد هذا السلوك من خطر حدوث أعطال بسبب نفاد الذاكرة (OOM) على الأجهزة المنخفضة المواصفات، ويؤدي إلى تدهور ثبات وقت التشغيل بشكل عام.

لذلك، يجب التعامل مع تحسين استخدام الذاكرة في Unity من خلال ثلاث ركائز أساسية مصمَّمة خصيصًا لسلوكيات المحرك هذه:

  • التحكّم في استخدام الذاكرة النشطة لمنع حدوث ارتفاعات كبيرة في استخدام الذاكرة في المقام الأول
  • إدارة تنسيقات مواد العرض المتكررة وتصاميم التظليل المتكررة لضمان عدم إنشاء مواد عرض وموارد أصلية غير ضرورية
  • أعِد تصميم بنى رموز وقت التشغيل لإزالة عمليات التخصيص غير الضرورية في الذاكرة المؤقتة المُدارة لتقليل عدد مرات تنفيذ عملية جمع البيانات غير الضرورية وتوسيع الذاكرة المؤقتة.

لمزيد من المعلومات، يُرجى الاطّلاع على تحسين استخدام الذاكرة في Unity.

تقليل استخدام الذاكرة في Unreal Engine

في Unreal Engine، يمكن أن تؤدي مسارات عرض الصور عالية الدقة ومخططات تبعية العناصر المعقّدة إلى زيادة كل من حجم مجموعة RSS المجهولة والضغط على الذاكرة المستندة إلى الملفات بشكل كبير. على وجه الخصوص، يؤدي الاعتماد على المراجع الثابتة والتسلسل الهرمي العميق لعمليات توريث المخطط الأساسي إلى تحميل مواد العرض المرتبطة غير المستخدَمة في الذاكرة. بالإضافة إلى ذلك، تساهم عمليات تبديل التظليل المفرطة ومجموعات نقل البيانات غير المحسَّنة للنسيج وجداول نقل البيانات غير المضغوطة بتنسيق ELF في زيادة مساحة الذاكرة الأساسية، ما يزيد من خطر إنهاء العمليات بواسطة أداة Low Memory Killer (LMK).

لذلك، يجب التعامل مع تحسين استخدام الذاكرة في Unreal Engine من خلال ثلاث ركائز أساسية مصمَّمة خصيصًا لسلوكيات المحرّك هذه:

  • افصل البيانات عن المنطق، واستبدِل المراجع الثابتة أو القوية بمراجع ضعيفة أو غير مباشرة.
  • يمكنك إزالة ميزات الإضاءة غير المستخدَمة على الأجهزة الجوّالة وخيارات التبديل لتقليل عدد كائنات حالة مسار العرض (PSO) وأهداف العرض المكرّرة، مع تطبيق ضغط ASTC وتخصيص مجموعات بث النسيج باستخدام ملفات تعريف الأجهزة.
  • فعِّل RELR وAPS Relocation Table Compression لتقليل حجم ملف ELF الثنائي وتقليل مساحة الذاكرة الفعلية المستخدَمة في وقت التشغيل.

لمزيد من المعلومات، اطّلِع على تحسين استخدام الذاكرة في Unreal.

تحسين العمليات المتعدّدة

يتم استبعاد استخدام الذاكرة في العمليات المخزّنة مؤقتًا من احتساب حدود الذاكرة لأنّه ليس له أي تأثير في التطبيقات النشطة. يساعد تشغيل الخدمة في عملية منفصلة ومعزولة على انتقال العملية الرئيسية إلى حالة مخزّنة مؤقتًا بأسرع ما يمكن، ما يؤدي إلى تحسين سلامة لعبتك.

لمزيد من المعلومات، يُرجى الاطّلاع على كيفية تتبُّع حالة العملية واستخدام الذاكرة وكيفية عزل عملية خدمة باستخدام Unity وكيفية عزل عملية خدمة باستخدام Unreal.

تقليل استخدام الذاكرة في الخدمات التي يلاحظها المستخدم

قد تحتاج لعبتك إلى تنفيذ منطق في خدمة يدركها المستخدم لحالات الاستخدام، مثل إكمال عملية تنزيل كبيرة أو لأنظمة محادثة صوتية في الخلفية. يمكن أن تساعدك هذه الاستراتيجيات في إدارة استخدام الذاكرة وتقليله في هذه السيناريوهات.

استراتيجيات لعمليات التنزيل الكبيرة

قد تنطبق هذه الاستراتيجيات على عمليات التنزيل الكبيرة التي تريد مواصلتها حتى بعد أن يصغّر المستخدم لعبتك.

1. عزل عملية التنزيل

الإجراء: تأكَّد من أنّ نظام التشغيل يمكنه استرداد الذاكرة التي لا يستخدمها تطبيقك على الفور من خلال تنفيذ عملية التنزيل في عملية منفصلة، لأنّه من المحتمل أن يحتفظ برنامج تخصيص الذاكرة بصفحات مجموعة الذاكرة ويحافظ على ارتفاع ذاكرة Anonymous RSS بشكل مصطنع حتى بعد تحرير المصفوفات. عند إنهاء الخدمة أو الخروج منها والعملية بشكل صريح، تتم إعادة الذاكرة إلى مجموعة نظام التشغيل ولن تتأثر العملية الرئيسية.

  • في Unity: يمكنك تخفيف الحِمل عن عملية التنزيل إلى Service Android أصلي تم التعريف به باستخدام عملية مثل android:process=":downloader" في ملف بيان مخصّص، ثم استدعاؤه باستخدام AndroidJavaClass JNI في Unity. احرص على إنهاء العملية عند اكتمال التنزيل. يمكنك الاطّلاع على تشغيل خدمة ملحوظة في عملية منفصلة باستخدام Unity للحصول على إرشادات أكثر تفصيلاً.

  • في Unreal: عرِّف Service مخصّصًا لنظام Android باستخدام عملية مثل android:process=":downloader" باستخدام Unreal Plugin Language، وشغِّله باستخدام C++ JNI. احرص على إنهاء العملية عند اكتمال التنزيل. يمكنك الاطّلاع على تشغيل خدمة ملحوظة في عملية منفصلة باستخدام Unreal للحصول على إرشادات أكثر تفصيلاً.

  • على Android الأصلي: عرِّف Service في AndroidManifest باستخدام عملية مثل android:process=":downloader". نفِّذ عملية التنزيل في هذه العملية المعزولة، واستدعِ Process.killProcess(Process.myPid()) عند اكتمال عملية التنزيل.

كيف يساعد ذلك: يقلّل ذلك من مدة الاحتفاظ بالذاكرة، ويسمح بمواصلة التنزيل مع إخلاء الذاكرة التي تستخدمها العملية الرئيسية الأكبر.

2. بث عمليات التنزيل مباشرةً إلى القرص

ما هي المشكلة: يتم نقل البيانات مباشرةً من مقبس الشبكة إلى القرص باستخدام مخزن مؤقت صغير الحجم وثابت وقابل لإعادة الاستخدام بدلاً من تجميع ردود الشبكة في مصفوفة كبيرة قبل كتابتها.

  • في Unity: تجنَّب استخدام DownloadHandlerBuffer لحِزم مواد العرض أو الملفات الكبيرة، لأنّها تخصّص مخزنًا مؤقتًا للذاكرة الأصلية يعادل حجم الملف (ذاكرة RSS مجهولة المصدر). بدلاً من ذلك، استخدِم DownloadHandlerFile لبث وحدات البايت بشكل أصلي إلى القرص على سلسلة محادثات في الخلفية.

  • في Unreal: يمكنك نقل أجزاء البيانات الواردة مباشرةً إلى FArchive(أرشيف مستند إلى ملف باستخدام "إدارة الملفات" في Unreal) باستخدام SetResponseBodyReceiveStream() بدلاً من إلحاق الحِملات من IHttpRequest في TArray<uint8>.

  • بالنسبة إلى Native Android: يمكنك نقل InputStream إلى FileOutputStream باستخدام مخزن مؤقت مجمّع بدلاً من استدعاء .readBytes() أو .string() في استجابة HTTP.

مزايا هذه الطريقة: تقليل الحد الأقصى لاستخدام الذاكرة

3- بث فك ضغط الملف الذي تم تنزيله

الحل: إذا كان التنزيل مضغوطًا، يمكنك تضمين دفق إدخال الشبكة في أداة فك ضغط متدفقة مثل ZipInputStream بدلاً من تنزيل الملف وتحميله في ذاكرة الوصول العشوائي ثم استخراجه.

مزايا هذه الطريقة: تقليل الحد الأقصى لاستخدام الذاكرة

4. التفويض إلى نظام التشغيل

الحل: لتجنُّب إدارة الذاكرة في الخلفية بشكل كامل، يمكنك تفويض العمل إلى واجهات برمجة التطبيقات الأصلية في Android.

  • WorkManager هو برنامج تضمين حديث ومقترَح حول JobScheduler على مستوى نظام التشغيل. في نظام التشغيل Android 14 والإصدارات الأحدث، تعالج WorkManager تلقائيًا عمليات التنزيل التي يبدأها المستخدم كإحدى مهام نقل البيانات التي يبدأها المستخدم (UIDT). يتم تنفيذ هذا الإجراء داخل عملية تطبيقك، لذا عليك مواصلة البث مباشرةً إلى القرص لتقليل استخدام الذاكرة. تحمي ميزة UIDT تطبيقك من الأعطال الناتجة عن نقص الذاكرة من خلال السماح لنظام التشغيل بإيقاف التنزيل مؤقتًا واستئنافه بسلاسة إذا أصبحت موارد النظام محدودة.

  • يتم تشغيل DownloadManager في عملية نظام منفصلة ولا يتم احتساب استخدام الذاكرة للتنزيل في تطبيقك. سيتلقّى تطبيقك إشعارًا ببث عندما يكون الملف قد تم تنزيله وأصبح جاهزًا.

كيف يساعد ذلك: يساعد WorkManager في التعامل مع سيناريوهات نقص الذاكرة، ويقلّل DownloadManager من استخدام تطبيقك للذاكرة.

استراتيجيات للخدمات المساعدة

قد تنطبق هذه الاستراتيجيات على الخدمات المساعدة التي تشغّلها اللعبة بالتوازي مع عملية اللعبة الرئيسية، مثل محادثة صوتية في الخلفية.

1. عزل العملية

ما هو التغيير: فصل الميزة، مثل حلّ المحادثة الصوتية، عن محرك اللعبة الرئيسي. على سبيل المثال، يمكنك تشغيل عملية تسجيل الصوت من الميكروفون وبث الشبكة داخل خدمة تعمل في المقدّمة على Android ومخصّصة لعملية منفصلة (يتم الإعلان عنها في ملف البيان، مثلاً باستخدام android:process=":voice").

طريقة الاستفادة من ذلك: عند تصغير التطبيق، يمكن أن تنتقل عملية اللعبة الرئيسية الثقيلة إلى حالة مخزَّنة مؤقتًا ذات أولوية أقل، بينما تستمر الخدمة المساعدة الأخف وزنًا في حالة الخدمة التي يدركها المستخدم.

2. تقليل الذاكرة غير المستخدَمة أثناء المعالجة

ما هي المشكلة؟ إذا كانت الخدمة المساعدة مدمجة بشكل كبير في محرك اللعبة بحيث يصعب فصلها، حاوِل تقليل حجم العملية قدر الإمكان بمجرد تصغير اللعبة أو تشغيلها في الخلفية. ننصحك بتفريغ ذاكرة التخزين المؤقت للصور، وإلغاء تحميل المشاهد غير الأساسية، وخفض معدّلَي عرض اللقطات ومعدّل تكرار المحرّك إلى 0، واستدعاء عملية جمع البيانات المُهمَلة بشكل صريح.

  • في Unity: نفِّذ عملية الاقتصاص هذه عند تشغيل OnApplicationPause(). قد يكون Resources.UnloadUnusedAssets() مفيدًا.

  • في Unreal: اربط منطق الاقتصاص بالمفوَّض ApplicationWillEnterBackgroundDelegate.

  • بالنسبة إلى Android الأصلي: يمكنك إجراء عملية الاقتصاص في onPause() أو onStop() حسب الاقتضاء. قد يحاول نظام التشغيل استدعاء تنفيذك للدالة onTrimMemory() قبل اللجوء إلى إيقاف العمليات التي تستخدم مقدارًا كبيرًا من الذاكرة.

كيف يساعد ذلك: يقلّل من استخدام الذاكرة غير اللازمة عندما لا تكون اللعبة في المقدّمة.