کاهش حافظه

وقتی مدیریت حافظه اندروید را درک کردید و ابزارهایی را برای اندازه‌گیری میزان استفاده از حافظه بازی خود تنظیم کردید، قدم بعدی کاهش و بهینه‌سازی فعال آن است. رعایت محدودیت‌های سختگیرانه اندروید به جلوگیری از بسته شدن بازی توسط سیستم، توقف زمان طولانی راه‌اندازی و تضمین اجرای خوب بازی در همه دستگاه‌ها کمک می‌کند.

این راهنما تکنیک‌های عملی برای کاهش مصرف حافظه بازی شما ارائه می‌دهد، به طور خاص بر بهینه‌سازی سطح دارایی، پیکربندی‌های خاص موتور و بهترین شیوه‌های مدیریت حافظه تمرکز دارد.

کاهش حافظه در یونیتی

با توجه به طراحی معماری Unity، پس از گسترش تخصیص‌دهنده‌های بلوک بومی و هیپ مدیریت‌شده، موتور تمایل دارد آن صفحات حافظه را برای استفاده مجدد نگه دارد تا اینکه آنها را بلافاصله به سیستم عامل (OS) بازگرداند، حتی پس از انتشار دارایی‌ها. به طور خاص، فضای آدرس مجازی (حافظه رزرو شده) برای طول عمر فرآیند رزرو شده باقی می‌ماند و حافظه فیزیکی (RSS) بلافاصله پس از وقوع چندین چرخه جمع‌آوری زباله (GC) و اصلاح، بازپس گرفته نمی‌شود. در نتیجه، یک اوج حافظه موقت می‌تواند باعث شود حافظه ساکن حتی پس از کاهش واقعی استفاده، برای مدت طولانی متورم باقی بماند. این رفتار خطر خرابی‌های خارج از حافظه (OOM) را در دستگاه‌های رده پایین افزایش می‌دهد و پایداری کلی زمان اجرا را کاهش می‌دهد.

بنابراین، بهینه‌سازی حافظه یونیتی باید از طریق سه رکن اصلی متناسب با این رفتارهای موتور انجام شود:

  • برای جلوگیری از افزایش ناگهانی و ناگهانی حافظه در همان وهله اول، میزان استفاده از حافظه فعال را کنترل کنید.
  • فرمت‌های بافت و انواع سایه‌زن را مدیریت کنید تا مطمئن شوید که منابع و داده‌های بومی غیرضروری نمونه‌سازی نمی‌شوند.
  • ساختارهای کد زمان اجرا را اصلاح کنید تا تخصیص‌های غیرضروری روی هیپ مدیریت‌شده را حذف کنید تا فرکانس GC و گسترش هیپ به حداقل برسد.

برای اطلاعات بیشتر، به بهینه‌سازی حافظه در یونیتی مراجعه کنید.

بهینه‌سازی چند فرآیندی

استفاده از حافظه توسط فرآیندهای ذخیره شده در حافظه پنهان (cache) از محاسبه محدودیت‌های حافظه مستثنی است زیرا هیچ تاثیری بر برنامه‌های فعال ندارد. اجرای سرویس در یک فرآیند جداگانه و ایزوله به فرآیند اصلی کمک می‌کند تا در اسرع وقت به حالت ذخیره شده در حافظه پنهان منتقل شود و در نتیجه سلامت بازی شما را بهبود بخشد.

برای اطلاعات بیشتر، به نحوه ردیابی وضعیت فرآیند و حافظه ، نحوه جداسازی یک فرآیند سرویس با Unity و نحوه جداسازی یک فرآیند سرویس با Unreal مراجعه کنید.

کاهش مصرف حافظه در سرویس‌های مورد نظر کاربر

ممکن است بازی شما برای مواردی مانند تکمیل یک دانلود بزرگ یا برای سیستم‌های چت صوتی در پس‌زمینه، نیاز به اجرای منطق در یک سرویس مورد نظر کاربر داشته باشد. این استراتژی‌ها می‌توانند به شما در مدیریت و کاهش استفاده از حافظه در این سناریوها کمک کنند.

استراتژی‌هایی برای دانلودهای بزرگ

این استراتژی‌ها ممکن است برای دانلودهای بزرگی که می‌خواهید حتی پس از اینکه کاربر بازی شما را کوچک کرد (مینیمایز) ادامه دهند، اعمال شوند.

۱. فرآیند دانلود را ایزوله کنید

چه چیزی: با انجام دانلود در یک فرآیند جداگانه، مطمئن شوید که سیستم عامل می‌تواند بلافاصله حافظه‌ای را که برنامه شما استفاده نمی‌کند، بازپس بگیرد، زیرا ممکن است تخصیص‌دهنده حافظه صفحات استخر حافظه را نگه دارد و حافظه RSS ناشناس را حتی پس از آزاد کردن آرایه‌ها، به طور مصنوعی بالا نگه دارد. هنگامی که سرویس و فرآیند را به طور صریح خاتمه می‌دهید یا از آن خارج می‌شوید، حافظه به استخر سیستم عامل بازگردانده می‌شود و فرآیند اصلی شما بدون تغییر باقی می‌ماند.

  • در یونیتی: دانلود را به یک Service اندروید بومی که با فرآیندی مانند android:process=":downloader" در یک Manifest سفارشی اعلام شده است، منتقل کنید و آن را با AndroidJavaClass JNI یونیتی فراخوانی کنید. مطمئن شوید که پس از اتمام دانلود، فرآیند را خاتمه دهید. برای راهنمایی دقیق‌تر به بخش اجرای یک سرویس محسوس در یک فرآیند جداگانه با یونیتی مراجعه کنید.

  • در Unreal: یک Service اندروید سفارشی با فرآیندی مانند android:process=":downloader" با استفاده از زبان افزونه Unreal تعریف کنید و آن را با C++ JNI فعال کنید. مطمئن شوید که پس از اتمام دانلود، فرآیند را خاتمه دهید. برای راهنمایی دقیق‌تر به بخش اجرای یک سرویس محسوس در یک فرآیند جداگانه با Unreal مراجعه کنید.

  • برای اندروید بومی: یک Service در AndroidManifest با فرآیندی مانند android:process=":downloader" تعریف کنید. دانلود را در این فرآیند ایزوله اجرا کنید و پس از اتمام دانلود Process.killProcess(Process.myPid()) را فراخوانی کنید.

این روش چه کمکی می‌کند: مدت زمان اشغال حافظه را کاهش می‌دهد و به دانلود اجازه می‌دهد تا ادامه یابد و در عین حال حافظه مورد استفاده توسط فرآیند اصلی بزرگ‌تر را آزاد کند.

۲. دانلودها را مستقیماً روی دیسک پخش کنید

چه چیزی: داده‌ها را مستقیماً از سوکت شبکه به دیسک با استفاده از یک بافر کوچک و قابل استفاده مجدد با اندازه ثابت به جای جمع‌آوری پاسخ‌های شبکه در یک آرایه بزرگ قبل از نوشتن، منتقل کنید.

  • در یونیتی: از استفاده از DownloadHandlerBuffer برای بسته‌های دارایی یا فایل‌های بزرگ خودداری کنید، زیرا یک بافر حافظه بومی معادل اندازه فایل (حافظه RSS ناشناس) اختصاص می‌دهد. در عوض، DownloadHandlerFile برای پخش بایت‌ها به صورت بومی به دیسک در یک رشته پس‌زمینه استفاده کنید.

  • در Unreal: به جای اضافه کردن payloadها از یک IHttpRequest به یک TArray<uint8> ، تکه‌های داده ورودی را مستقیماً با SetResponseBodyReceiveStream() به یک FArchive (بایگانی پشتیبان‌گیری شده با فایل با استفاده از File Manager آنریل) پایپ کنید.

  • برای اندروید بومی: به جای فراخوانی .readBytes() یا .string() در یک پاسخ HTTP، InputStream را با استفاده از یک بافر ادغام‌شده به FileOutputStream پایپ کنید.

چگونه این کمک می‌کند: کاهش استفاده از حافظه در اوج مصرف

۳. استخراج فایل دانلود شده از حالت فشرده با استریم

توضیح: اگر فایل دانلودی شما فشرده شده است، به جای دانلود فایل، بارگذاری آن در RAM و سپس استخراج آن، جریان ورودی شبکه خود را در یک ابزار فشرده‌ساز جریانی مانند ZipInputStream قرار دهید.

چگونه این کمک می‌کند: کاهش استفاده از حافظه در اوج مصرف

۴. واگذاری به سیستم عامل

چه: برای جلوگیری از مدیریت حافظه پس‌زمینه، این کار را به APIهای بومی اندروید واگذار کنید.

  • WorkManager یک بسته‌بندی مدرن و توصیه‌شده برای زمان‌بندی JobScheduler ) در سطح سیستم‌عامل است. برای اندروید ۱۴+، WorkManager به‌طور خودکار دانلودهای انجام‌شده توسط کاربر را به‌عنوان یک کار انتقال داده توسط کاربر (UIDT) مدیریت می‌کند. این کار درون فرآیند برنامه شما اجرا می‌شود، بنابراین شما همچنان باید مستقیماً به دیسک منتقل شوید تا استفاده از حافظه به حداقل برسد. UIDT با اجازه دادن به سیستم‌عامل برای مکث و از سرگیری دانلود شما در صورت محدود شدن منابع سیستم، از برنامه شما در برابر خرابی‌های ناشی از کمبود حافظه محافظت می‌کند.

  • DownloadManager در یک فرآیند سیستمی جداگانه اجرا می‌شود و میزان استفاده از حافظه برای دانلود را به برنامه شما نسبت نمی‌دهد. برنامه شما پس از دانلود و آماده شدن فایل، یک اعلان پخش دریافت خواهد کرد.

چگونه این کمک می‌کند: WorkManager به مقابله با سناریوهای کمبود حافظه کمک می‌کند و DownloadManager میزان استفاده از حافظه توسط برنامه شما را کاهش می‌دهد.

استراتژی‌هایی برای خدمات کمکی

این استراتژی‌ها ممکن است برای سرویس‌های کمکی که یک بازی ممکن است بخواهد به موازات فرآیند اصلی بازی اجرا کند، مانند چت صوتی پس‌زمینه، اعمال شوند.

۱. فرآیند را ایزوله کنید

چه چیزی: ویژگی، مثلاً راهکار چت صوتی خود را، از موتور اصلی بازی جدا کنید. برای مثال، می‌توانید ضبط میکروفون و پخش شبکه را درون یک سرویس پیش‌زمینه اندروید که به یک فرآیند جداگانه اختصاص داده شده است (که در مانیفست تعریف شده است، مانند android:process=":voice" )، اجرا کنید.

چگونه این کمک می‌کند: وقتی برنامه به حالت مینیمایز (کوچک) درمی‌آید، فرآیند سنگین بازی اصلی می‌تواند به حالت کش‌شده با اولویت پایین‌تر برود، در حالی که سرویس کمکی سبک‌تر در حالت سرویس مورد نظر کاربر به کار خود ادامه می‌دهد.

۲. حافظه‌ی بلااستفاده‌ی درون‌پردازشی را حذف کنید

چه: اگر سرویس کمکی بیش از حد در موتور بازی ادغام شده است که نمی‌توان آن را از هم جدا کرد، سعی کنید به محض اینکه بازی در پس‌زمینه/مینیمایز شد، تا حد امکان از بار این فرآیند بکاهید. خالی کردن حافظه‌های نهان بافت، تخلیه صحنه‌های غیرضروری، کاهش نرخ تیک موتور و رندر به ۰ و فراخوانی صریح جمع‌آوری زباله را در نظر بگیرید.

  • در یونیتی: این برش را هنگام اجرای OnApplicationPause() انجام دهید. Resources.UnloadUnusedAssets() ممکن است مفید باشد.

  • در Unreal: منطق برش را به نماینده ApplicationWillEnterBackgroundDelegate متصل کنید.

  • برای اندروید بومی: در صورت لزوم، عملیات برش را در onPause() یا onStop() انجام دهید. سیستم عامل ممکن است قبل از اقدام به کشتن فرآیندها با استفاده از حافظه بالا، سعی کند پیاده‌سازی شما از onTrimMemory() را فراخوانی کند.

این روش چه کمکی می‌کند: وقتی بازی در حال اجرا نیست، میزان استفاده از حافظه که مورد نیاز نیست را کاهش می‌دهد.