تنظیم بودجه حافظه برنامه

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

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

  1. صفحه‌های تمیز با پشتیبانی فایل (مانند کد غیرفعال و دارایی‌های نگاشت‌شده) ابتدا تخلیه می‌شوند زیرا درصورت نیاز می‌توان آن‌ها را دوباره از فضای ذخیره‌سازی خواند.
  2. صفحه‌های کثیف با پشتیبانی فایل به فضای ذخیره‌سازی برگردانده می‌شوند و حذف می‌شوند.
  3. صفحه‌های حافظه ناشناس (مانند تخصیص‌های پشته) فشرده می‌شوند و به zRAM منتقل می‌شوند.

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

وقتی برنامه از بودجه‌اش فراتر می‌رود چه اتفاقی می‌افتد

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

تا زمانی که بین آنچه برنامه در هر زمان مشخص نیاز دارد (مجموعه کاری آن) و بودجه تعیین‌شده فضای خالی وجود داشته باشد، برنامه به اجرای عادی خود ادامه می‌دهد. اگر برنامه‌ای بودجه‌ای را تنظیم کند که از مجموعه کاری آن کوچک‌تر باشد، عملکرد برنامه در زمان اجرا ممکن است درنتیجه آن کند شود.

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

اعلام بودجه‌ها در مانیفست Android

اعلام بودجه‌های حافظه در AndroidManifest.xml روش اصلی و توصیه‌شده برای تعریف بودجه است. این ویژگی به کد زمان اجرا نیاز ندارد، بلافاصله پس‌از راه‌اندازی فرایند اعمال می‌شود، و قرارداد واضحی برای سیستم‌عامل ارائه می‌دهد.

اعلامیه‌های <memory-budget> در دستگاه‌هایی که از Android 17 QPR2 (سطح ای‌پی‌آی ۳۷.۲) و بالاتر استفاده می‌کنند اعمال می‌شود. در نسخه‌های قدیمی‌تر Android، تجزیه‌کننده مانیفست پلاتفرم عناصر XML ناشناخته را به‌طور ایمن نادیده می‌گیرد، بنابراین می‌توانید <memory-budget> را بدون تأثیر بر سازگاری با نسخه‌های قدیمی‌تر اتخاذ کنید.

اعلام کردن بودجه مبنا

برای اکثر برنامه‌ها، تعریف یک بودجه واحد برای برنامه تنها چیزی است که نیاز است. عنصر <memory-budget> را مستقیماً در برچسب <application> اعلام کنید:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Baseline budget for the application -->
        <memory-budget android:maxMb="256" />

    </application>
</manifest>

این کار بودجه حافظه مقیم ۲۵۶ مگابایتی را در همه فرایندها و حالت‌ها برای بسته تنظیم می‌کند. وقتی ردپای حافظه برنامه از ۲۵۶ مگابایت فراتر می‌رود، سیستم‌عامل صفحه‌های حافظه غیرفعال را بااستفاده از تخلیه و تعویض کوتاه می‌کند.

تغییر بودجه‌ها براساس وضعیت پردازش

برنامه بسته به رؤیت‌پذیری آن برای کاربر به مقادیر مختلفی از حافظه نیاز دارد:

  • پیش‌زمینه: فرایند درحال میزبانی «فعالیت» قابل‌مشاهده‌ای است که با کاربر تعامل دارد. این حالت معمولاً به‌دلیل رابط کاربری و گرافیک فعال، بیشترین ردپا را دارد.
  • قابل‌تشخیص: فرایند برای کاربر قابل‌تشخیص است اما پنجره قابل‌مشاهده‌ای را میزبانی نمی‌کند (برای مثال، میزبانی سرویس پیش‌زمینه‌ای بازپخش رسانه، بارگیری پس‌زمینه‌ای فعال، ناوبری گام‌به‌گام، یا روش ورودی فعال).
  • پس‌زمینه: فرایند درحال اجرای کارهای پس‌زمینه، گیرنده‌ها، یا همگام‌سازی‌های داده است. انتظار می‌رود که ردپای حداقلی داشته باشد.

می‌توانید چندین عبارت <memory-budget> را برای مطابقت با این حالت‌ها اعلام کنید:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Default budget for visible foreground UI -->
        <memory-budget android:maxMb="200" />

        <!-- Tighter budget when playing audio in background -->
        <memory-budget
            android:maxMb="120"
            android:state="perceptible" />

        <!-- Minimal budget when fully in background -->
        <memory-budget
            android:maxMb="48"
            android:state="background" />

    </application>
</manifest>

بند پایه بدون android:state الزامی نیست؛ اگر فقط بندهای مختص ایالت (برای مثال، android:state="background") را مشخص کنید، ایالت‌های دیگر بدون محدودیت بودجه برنامه باقی می‌مانند. وقتی گنجانده شود، بندی بدون android:state به‌عنوان بازگشت پیش‌فرض برای حالت‌های نامشخص (مانند پیش‌زمینه) عمل می‌کند، که بندهای محدودکننده‌تر بعدی وقتی برنامه به حالت‌های perceptible یا background منتقل می‌شود، آن را لغو می‌کنند.

هنگام تعیین اندازه بودجه برای هر وضعیت، مجموعه کاری فعال آن وضعیت را هدف قرار دهید نه اوج حافظه مقیم (علامت سطح بالای RSS):

  • بودجه‌های اندازه foreground برای رندر کردن روان فریم: به میانای کاربری پیش‌زمینه فضای کافی برای مجموعه کاری فعال آن بدهید تا بازیابی همگام رندر کردن فریم را به‌تأخیر نیندازد. اگر برنامه‌تان شامل گردش کار مستقل با مصرف حافظه بالا است (مثل ویرایشگر ویدیو یا تصویر با وضوح بالا)، آن فعالیت را به فرایند اختصاصی منتقل کنید (به برنامه‌های چندفرایندی مراجعه کنید) تا تخصیص‌های آن به‌طور مستقل بودجه‌بندی شود و وقتی کاربر خارج می‌شود، ازبین برود.
  • بودجه‌های background را برای کارهای بدون واسط به‌طور دقیق تعیین کنید: RSS پس‌زمینه بالا در دورسنجی میدانی اغلب نشان‌دهنده تخصیص‌های واسط کاربری است که پس‌از ترک پیش‌زمینه حفظ شده‌اند (به قرار دادن خدمات محدودکننده در یک مکان با واسط کاربری با مصرف حافظه بالا و آزاد کردن حافظه در پاسخ به رویدادها مراجعه کنید) یا خواندن‌های یک‌باره فایل درطول نمایه‌سازی و همگام‌سازی‌های پس‌زمینه. ازآنجایی‌که کار پس‌زمینه بدون سر مهلت زمانی برای قاب ندارد، بیرون کردن صفحه‌های حافظه پنهان فایل یک‌باره و جابه‌جا کردن صفحه‌های توده غیرفعال به zRAM وقتی همگام‌سازی پس‌زمینه به‌طور خلاصه فراتر از بودجه‌اش اختصاص می‌یابد، هیچ پرش قابل‌مشاهده‌ای برای کاربر ایجاد نمی‌کند و درعین‌حال از بیرون کردن برنامه پیش‌زمینه کاربر توسط جهش‌های پس‌زمینه جلوگیری می‌کند.

برای گردش کار گام‌به‌گام، مجموعه کاری‌تان را اندازه‌گیری کنید و بودجه‌ها را کالیبره کنید.

نحوه نگاشت وضعیت‌های بودجه به وضعیت‌های پردازش

پلاتفرم حالت‌های بودجه مانیفست را براساس RunningAppProcessInfo.importance به حالت‌های فرایند زمان اجرا نگاشت می‌کند، که «مدیر فعالیت» آن را هم از مؤلفه‌های فعال خود فرایند و هم از هرگونه اتصال سرویس ورودی یا پُرسمان‌های ارائه‌دهنده محتوا از برنامه‌های دیگر محاسبه می‌کند:

  • foreground: تعاملات فعال و قابل‌مشاهده کاربر، مانند میزبانی یک فعالیت ازسرگرفته‌شده، حفظ وضعیت برنامه برتر هنگام خاموش شدن صفحه‌نمایش، یا میزبانی یک سرویس یا ارائه‌دهنده محتوا که به‌طور فعال توسط برنامه پیش‌زمینه برتر محدود یا پرس‌وجو می‌شود.
  • perceptible: حجم‌های کاری که بدون پنجره نمایان برای کاربر قابل‌درک هستند، مثل بازپخش رسانه فعال، ناوبری گام‌به‌گام، ضبط دوربین یا میکروفون، بارگیری‌های فعال پس‌زمینه، سرویس‌های پیش‌زمینه همگام‌سازی داده، یا سرویس‌های محدودشده توسط کارخواه پیش‌زمینه بااستفاده از پرچم‌هایی مثل BIND_NOT_FOREGROUND (به کنترل کردن وراثت با پرچم‌های BIND مراجعه کنید). درحالی‌که سنجه‌های پلاتفرم داخلی ممکن است برخی‌از این حجم‌های کاری را PROCESS_STATE_IMPORTANT_FOREGROUND برچسب‌گذاری کنند، آن ثابت داخلی نشان‌دهنده پنجره قابل‌مشاهده نیست و تحت کنترل بودجه perceptible است.
  • background: کاری که فوراً برای کاربر قابل‌درک نیست، مثل کارهای پس‌زمینه، هشدارها، و گیرنده‌های رویداد ثبت‌شده.

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

مانیفست android:state اهمیت زمان اجرا (RunningAppProcessInfo) اجزای معمول
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
«فعالیت» نمایان ازسرگرفته‌شده، برنامه برتر با صفحه قفل‌شده، ارائه‌دهنده سرویس یا محتوا که توسط برنامه برتر محدود شده است
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
سرویس‌های پیش‌زمینه‌ای فعال بازپخش رسانه، پیمایش، بارگیری، یا همگام‌سازی؛ سرویس محدودشده با پرچم‌های سرویس پیش‌زمینه‌ای یا نمایان
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
کارهای پس‌زمینه، گیرنده‌ها، همگام‌سازی پس‌زمینه

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

وقتی فرایندی بدون مؤلفه‌های فعال وارد حالت ذخیره‌شده در حافظه نهان (IMPORTANCE_CACHED) می‌شود، پلاتفرم بودجه حافظه فعال آن را آزاد می‌کند و حافظه آن را ازطریق بازیابی استاندارد فرایند ذخیره‌شده در حافظه نهان مدیریت می‌کند.

بررسی کردن وضعیت پردازش و بودجه برنامه

یکی از راه‌های بررسی وضعیت فرایند فعال برنامه و اهمیت آن درطول توسعه، پرسمان کردن Activity Manager بااستفاده از ADB است:

adb shell dumpsys activity processes <package-name>

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

ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
  All known processes:
  *APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
    pid=3919
    oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
    curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
    hasStartedServices=true
    mHasForegroundServices=true forcingToImportant=null
...
  Process OOM control (48 total):
    Proc #22: prcp  F/S/FGS  ---NFU-TI  t: 0 3919:com.example.app/u0a123 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS  lastRss=0.00 lastCachedRss=0.00

در این برونداد:

  • curProcState=4 و state: cur=FGS نشان می‌دهند که فرایند در وضعیت سرویس پیش‌زمینه‌ای است. اگر برنامه‌تان میزبان فعالیت قابل‌مشاهده فعالی باشد، این به‌صورت TOP نشان داده می‌شود. اگر مؤلفه پیش‌زمینه مهمی درحال اجرا باشد (مثل بارگیری یا همگام‌سازی)، به‌صورت IMPF نشان داده می‌شود.
  • در جدول کنترل «کمبود حافظه» فرایند، prcp نشان می‌دهد که فرایند تحت ردیف اولویت قابل‌درک ارزیابی می‌شود که با بودجه perceptible مطابقت دارد.

برای بررسی استفاده فعلی برنامه‌تان از حافظه مقیم:

adb shell dumpsys meminfo <package-name>

برنامه‌های چندفرایندی

اگر برنامه‌تان کارش را بین چند فرایند تقسیم می‌کند، بااستفاده از برچسب <process> در <processes>، بودجه‌های فرایند اختصاصی را پیکربندی کنید.

برای مثال، یک برنامه جاری‌سازی موسیقی (com.example.radio) را درنظر بگیرید:

  1. فرایند اصلی: میزبان واسط کاربر نمایان و موتور بازپخش صدا است (MediaSessionService با یک mediaPlayback سرویس پیش‌زمینه‌ای). وقتی قابل‌مشاهده باشد، فرایند تحت بودجه پیش‌زمینه ۱۸۰ مگابایتی عمل می‌کند. وقتی کاربر از برنامه خارج می‌شود درحالی‌که موسیقی همچنان درحال پخش است، فرایند وارد حالت perceptible می‌شود که در آن بودجه ۶۴ مگابایتی برای موتور بازپخش و میان‌گیر صوتی کافی است.
  2. فرایند همگام‌سازی (:sync): فرایند اختصاصی که همگام‌سازی فراداده پس‌زمینه و نمایه‌سازی بارگیری را اجرا می‌کند. چون این فرایند فقط در پس‌زمینه فعال است، نیازی نیست state="background" را به‌طور صریح اعلام کنید؛ یک بودجه واحد اعمال می‌شود.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.radio">

    <application
        android:label="@string/app_name">

        <!-- Package baseline: main process with UI and audio playback -->
        <memory-budget android:maxMb="180" />

        <!-- Tighter budget when audio plays in the background -->
        <memory-budget
            android:maxMb="64"
            android:state="perceptible" />

        <processes>
            <!-- Dedicated background sync process -->
            <process android:process=":sync">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <service
            android:name=".playback.AudioPlayerService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false" />

        <service
            android:name=".sync.PlaylistSyncService"
            android:process=":sync"
            android:exported="false" />

    </application>
</manifest>

وقتی بلوک <processes> را در مانیفست خود اعلام می‌کنید، ورودی‌های <process> را فقط برای فرایندهایی که <memory-budget> را اعلام می‌کنند فهرست کنید. اگر می‌خواهید فقط برای زیرفرایندهای خاصی بودجه تعیین کنید و بقیه برنامه را در حد سیستم برای دستگاه رها کنید، می‌توانید عناصر <memory-budget> را مستقیماً زیر <application> حذف کنید.

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

مقیاس‌بندی بودجه‌ها برای نمایشگرهای با تراکم بالا

برای برنامه‌هایی که ردپای حافظه آن‌ها به‌طور قابل‌توجهی با تعداد پیکسل‌هایی که باید به‌طور هم‌زمان روی نمایشگر ترسیم شوند مقیاس‌بندی می‌شود (مانند برنامه گالری عکس که نقشه‌های بیتی را به اندازه صفحه ذخیره می‌کند)، Android دو سازوکار جایگزین برای مقیاس‌بندی پویا بودجه‌ها با مشخصات نمایشگر ارائه می‌دهد:

  • مقیاس‌بندی براساس دسته تراکم نمایشگر (android:additionalMbPerDensity): مگابایت‌ها را به‌طور متناسب به مربع نسبت تراکم نمایشگر نسبت‌به mdpi (۱٫۰ برابر / ۱۶۰ نقطه در اینچ) اضافه می‌کند، زیرا دارایی‌های میانای کاربر و قابل‌ترسیم شطرنجی دوبعدی در سراسر عرض و ارتفاع مقیاس‌بندی می‌شوند:

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    در نمایشگر mdpi (۱٫۰ برابر)، بودجه ۱۸۰ + ۱۶ × ۱۲ = ۱۹۶ مگابایت است. در نمایشگر xxhdpi (۳٫۰ برابر)، بودجه به ۱۸۰ + ۱۶ × ۳۲ = ۳۲۴ مگابایت مقیاس‌بندی می‌شود.

  • مقیاس‌بندی براساس وضوح نمایشگر فیزیکی (android:additionalBytesPerDisplayPixel): بایت‌ها را مستقیماً براساس پیکسل نمایشگر فیزیکی (عرض × ارتفاع) اضافه می‌کند. این برای برنامه‌هایی که سطوح گرافیکی تمام‌صفحه، میان‌گیرهای رندر، یا حافظه‌های پنهان عکس با وضوح کامل را اختصاص می‌دهند ایده‌آل است، زیرا در این برنامه‌ها مصرف حافظه مستقیماً با تعداد پیکسل‌های نمایش خام مقیاس‌بندی می‌شود، نه با دسته‌های تراکم واسط کاربر:

    <!-- Baseline 128MB + 16 bytes per physical display pixel -->
    <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering -->
    <memory-budget
        android:maxMb="128"
        android:additionalBytesPerDisplayPixel="16" />
    

    در نمایشگر ۱۰۸۰ پیکسل (۲۴۰۰ × ۱۰۸۰ &approx; ۲٫۵۹ میلیون پیکسل)، این تقریباً ۴۱٫۴ مگابایت به بودجه پایه اضافه می‌کند. در نمایشگر 1440p (۱۴۴۰ × ۳۱۲۰ &approx; ۴٫۴۹ میلیون پیکسل)، &approx; ۷۱٫۸ مگابایت اضافه می‌کند.

این دو مشخصه جایگزین یکدیگر هستند. مشخصه‌ای را انتخاب کنید که با ضریب مقیاس اصلی برنامه‌تان مطابقت داشته باشد و از ترکیب هر دو در یک بند خودداری کنید.

تخصصی کردن برای ضریب‌های شکل دستگاه

هنگام ارسال APK در تلفن‌ها، رایانه‌های لوحی، و Wear OS، از مشخصه android:feature برای تنظیم بودجه‌ها برای اهداف سخت‌افزاری مختلف استفاده کنید.

در ساعت‌های Wear OS، حافظه دسترسی تصادفی محدود است و واسط کاربر و مجموعه ویژگی‌های برنامه بسیار ساده‌تر است. می‌توانید بودجه محدودتری را برای ویژگی watch اعلام کنید:

<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />

<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
    android:maxMb="48"
    android:feature="watch" />

قانون حل‌وفصل: آخرین بند قابل‌اجرا اعمال می‌شود

هنگام تعریف چندین عنصر <memory-budget> برای یک برنامه یا فرایند، سیستم آن‌ها را به ترتیبی که در مانیفست اعلام شده‌اند ارزیابی می‌کند. آخرین بند بودجه قابل‌اجرا، بندی است که اجرا می‌شود.

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

مرجع مشخصه XML

همه مشخصه‌های اندازه حافظه برحسب مگابایت (MB) بیان می‌شوند و به Linux cgroup memory.current charge (که حافظه مشترک مانند Zygote را مستثنی می‌کند) نگاشت می‌شوند.

مشخصه قالب پیش‌فرض شرح
android:maxMb عدد صحیح (بزرگ‌تر از ۰) الزامی حد بودجه حافظه مقیم مبنا برحسب مگابایت.
android:state شمارشی همه وضعیت فرایندی که این بودجه برای آن اعمال می‌شود: foreground، perceptible، یا background. اگر حذف شود، این عبارت به‌عنوان جایگزین برای هر وضعیت نامشخصی عمل می‌کند.
android:additionalMbPerDensity عدد صحیح (≥ ۰) 0 مگابایت‌های اضافی ضرب در مربع نسبت تراکم نمایشگر نسبت به mdpi (۱٫۰ برابر).
android:additionalBytesPerDisplayPixel عدد صحیح (≥ ۰) 0 بایت‌های اضافی اختصاص‌داده‌شده به هر پیکسل نمایشگر فیزیکی (عرض × ارتفاع)، مفید برای میان‌گیرهای سطح و نقشه‌های بیتی.
android:feature رشته همه عبارت را به دستگاه‌هایی محدود می‌کند که ویژگی‌های سخت‌افزاری خاصی را اعلام می‌کنند: watch، automotive، یا leanback.

مجموعه کاری‌تان را بسنجید و بودجه‌ها را کالیبره کنید

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

آشنایی با مجموعه کاری درمقابل حافظه مقیم

بدون بودجه، ردپای مقیم برنامه‌تان (memory.current) دو نوع حافظه را نگه می‌دارد:

  • مجموعه کاری: پشته Java،‏ Kotlin، و بومی و صفحه‌های داغ با پشتیبانی فایل (کد DEX، کتابخانه‌های بومی، و دارایی‌ها) که جریان کاربر فعلی بارها و بارها با آن‌ها تماس برقرار می‌کند
  • Cold slack: حافظه‌های نهان نگاشت بیتی از صفحه‌های قبلی، میان‌گیرهای راه‌اندازی یک‌باره، فضای خالی توده باقی‌مانده پس‌از انفجار تخصیص، و صفحه‌های فایل که یک‌بار خوانده شده‌اند و دیگر هرگز لمس نشده‌اند

بودجه‌ای که در اوج نامحدود شما باشد هرگز زمان غیرفعال سرد را بازیابی نمی‌کند. بودجه‌ای کمتر از مجموعه کاری شما باعث می‌شود هسته صفحه‌های توده را جابه‌جا کند و صفحه‌های کد را که برنامه‌تان فوراً به‌دلیل خطا برمی‌گرداند بیرون کند. این خطاهای مجدد مجموعه کاری زمان واحد پردازش مرکزی و باتری را مصرف می‌کنند و وقتی به رشته میانای کاربر می‌رسند، قاب‌ها را نیز مصرف می‌کنند.

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

آماده کردن محیط آزمایشی

آزمایشی تکرارپذیر راه‌اندازی کنید که مانند تولید عمل کند:

  • همان ساخت نسخه پخش غیرقابل‌اشکال‌زدایی بهینه‌شده با R8 را که ارسال می‌کنید کالیبره کنید. ساخت‌های اشکال‌زدایی از حافظه بیشتری استفاده می‌کنند، بنابراین بودجه‌ای که برای ساخت اشکال‌زدایی تنظیم شده است برای انتشار بسیار زیاد است.
  • در دستگاه یا شبیه‌سازی که Android 17 QPR2 (سطح میانای برنامه‌سازی کاربردی ۳۷.۲) یا بالاتر را اجرا می‌کند آزمایش کنید. ساخت userdebug یا تصویر شبیه‌ساز بدون Google Play بهترین گزینه است، زیرا برخی دستگاه‌ها فقط پس‌از اجرای adb root به شما اجازه می‌دهند فایل‌های cgroup را در بخش‌های زیر بخوانید.
  • یک جریان کاربر ۳۰ تا ۶۰ ثانیه‌ای را با Jetpack Macrobenchmark،‏ UI Automator، یا adb shell input، یک‌بار برای هر وضعیتی که قصد دارید بودجه‌بندی کنید، اسکریپت‌نویسی کنید. برنامه غیرفعال چیزی درباره مجموعه کاری‌اش به شما نمی‌گوید.
    • foreground: پیمایش فهرست‌های متراکم و شبکه‌های رسانه، جابه‌جایی بین مقصدهای اصلی، باز کردن منوها و کادرهای گفتگو، و اجرای پویانمایی‌ها
    • perceptible: پنجره قابل‌مشاهده را درحین ادامه کار قابل‌تشخیص (برای مثال، بازپخش صدا یا بارگیری) ترک کنید، اجازه دهید ۱۰ تا ۱۵ ثانیه اجرا شود، و سپس به برنامه برگردید
    • background: راه‌اندازی همگام‌سازی، کار WorkManager (برای مثال، با adb shell cmd jobscheduler run -f <package-name> <job-id>)، سرویس شروع‌شده، یا گیرنده رویداد ثبت‌شده درحالی‌که برنامه فاقد رابط کاربری نمایان است

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

اندازه‌گیری مقدار پایه

با اجراهایی شروع کنید که اصلاً بودجه ندارند تا بدانید حالت عادی چگونه است. از ساخت بدون اعلام <memory-budget> استفاده کنید و هر اجرا را از یک فرایند جدید (adb shell am force-stop <package-name>) شروع کنید.

هر فرایند برنامه کنترل‌کننده حافظه cgroup v2 خودش را دارد. مسیر آن را از خط 0:: از /proc/<PID>/cgroup جستجو کنید به‌جای اینکه خودتان مسیر را بسازید، زیرا طرح‌بندی cgroup می‌تواند بین دستگاه‌ها متفاوت باشد. هر فرمان دستگاه را به‌عنوان یک رشته نقل‌قول‌شده تکی به adb shell ارسال کنید تا پوسته دستگاه نقل‌قول‌ها را ببیند:

PID=$(adb shell pidof -s <package-name>)
CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")
adb shell "cat $CG/memory.current $CG/memory.swap.current"
adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

خواندن این فایل‌ها ارزان است و برنامه شما را مختل نمی‌کند، بنابراین می‌توانید آن‌ها را در هر ثانیه درطول اجرا نمونه‌برداری کنید:

  • memory.current: حافظه مقیم کسرشده از پردازش، به بایت. این تعداد محدودیت‌های <memory-budget> است.
  • memory.swap.current: حافظه فشرده‌شده در مبادله zRAM، برحسب بایت
  • anon و file در memory.stat: حافظه ناشناس مقیم و با پشتیبانی فایل، به بایت
  • workingset_refault_anon و workingset_refault_file در memory.stat: تعداد درحال اجرای صفحه‌های جابه‌جاشده یا اخراج‌شده‌ای که برنامه شما دوباره به آن‌ها نیاز داشته است. این شمارنده‌ها نشانه اصلی شما هستند که بودجه بسیار محدود است.

اگر خطای اجازه دریافت کردید، adb root را اجرا کنید و دوباره امتحان کنید. اگر نمی‌توانید دسترسی ریشه‌ای داشته باشید، adb shell dumpsys meminfo <package-name> نمای کلی‌تری به شما می‌دهد. در App Summary، Java Heap، Native Heap، و Stack عمدتاً حافظه ناشناس هستند و Code پشتیبان‌گیری‌شده با فایل است. نگاشت‌های بیتی نرم‌افزار در Native Heap و بودجه شما لحاظ می‌شوند، اما نگاشت‌های بیتی سخت‌افزار و Graphics لحاظ نمی‌شوند. هنگام اندازه‌گیری زمان‌بندی فریم، dumpsys meminfo را در حلقه اجرا نکنید. این کار جمع‌آوری زباله را در برنامه‌تان اجباری می‌کند و همه نگاشت‌های حافظه آن را بررسی می‌کند، که آنچه را می‌خواهید اندازه‌گیری کنید تغییر می‌دهد.

بودجه‌های نامزد را جارو کنید

می‌توانید بودجه‌های نامزد را بدون بازسازی برنامه‌تان امتحان کنید. از am memory-limiter manual برای تنظیم بودجه برای فرایند درحال اجرا و از none برای برداشتن آن استفاده کنید:

adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none

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

همچنین می‌توانید با خواندن مقدار نامزد از آرگومان ابزار و تنظیم MemoryBudgetManager.processBudgetBytes، از ساخت محک جاروب کنید. بودجه زمان اجرا فقط می‌تواند بودجه مانیفست را محدود کند، بنابراین بدون اعلامیه‌های <memory-budget>، ساخت را پاک‌سازی کنید. همچنین در تغییرات وضعیت فرایند تا زمانی که clearProcessBudget را فراخوانی کنید، معتبر باقی می‌ماند.

از بالاترین memory.current از خط مبنای خود شروع کنید و هر بار ۱۰ تا ۱۵ مگابایت کاهش دهید. برای هر بودجه نامزد:

  1. فرایند جدیدی را شروع کنید، آن را به حالت هدف برسانید، و بودجه را اعمال کنید.
  2. جریان کاربر را یک‌بار به‌عنوان دست‌گرمی اجرا کنید. اعمال بودجه یک‌بار صفحه‌های راه‌اندازی سرد را بیرون می‌کند و شما می‌خواهید حالت پایدار را به‌جای آن انتقال اندازه‌گیری کنید.
  3. آمار قاب را بازنشانی کنید و شمارنده‌های خطای مجدد شروع و زمان واحد پردازش مرکزی پردازش (utime و stime، در تیک‌های ساعت) را ضبط کنید:

    adb shell dumpsys gfxinfo <package-name> reset
    adb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"
    adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"
  4. گذر اندازه‌گیری‌شده جریان کاربرتان را اجرا کنید.

  5. memory.current،‏ anon،‏ file، شمارنده‌های خطای مجدد، و زمان واحد پردازش مرکزی را دوباره ضبط کنید، و آمار قاب را جمع‌آوری کنید. dumpsys gfxinfo می‌تواند همان آمار را بیش‌از یک بار چاپ کند، بنابراین اولین مطابقت را که کل برنامه را پوشش می‌دهد درنظر بگیرید:

    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'
    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'

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

خواندن نتایج

با کاهش بودجه، برنامه شما سه رژیم را طی می‌کند:

  • بالای زانو: memory.current با رها کردن صفحه‌های سرد file و توده بی‌کار، بودجه را کاهش می‌دهد. anon ثابت می‌ماند، خطاهای مجدد نزدیک به خط مبنا می‌مانند، و زمان واحد پردازش مرکزی و زمان پردازش فریم تغییر نمی‌کنند. اینجا جایی است که می‌خواهید باشید.
  • زیر زانو: صفحه‌های سرد ازبین رفته‌اند، بنابراین هسته شروع به مبادله صفحه‌های توده می‌کند که برنامه‌تان هنوز از آن‌ها استفاده می‌کند. خطاهای مجدد در هر اجرا صدها یا هزاران صفحه افزایش می‌یابد و زمان واحد پردازش مرکزی بالا می‌رود. زمان پردازش فریم ممکن است به‌ندرت در دستگاه آزمایشی که در حالت دیگری غیرفعال است تغییر کند، اما برنامه‌تان اکنون زمان واحد پردازش مرکزی و باتری را صرف فشرده‌سازی و ازفشردگی خارج کردن مجموعه کاری خودش می‌کند.
  • بسیار پایین‌تر از زانو: خطاهای بازپس‌گیری و تعویض در رشته میانای کاربر و RenderThread قرار می‌گیرند و زمان‌های پرش، زمان‌های فریم، و زمان‌های ازسرگیری گرم همگی افزایش می‌یابند.

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

  • فرایند پس‌از اجرا باقی می‌ماند
  • تغییرات Refault نزدیک به خط مبنا باقی می‌ماند
  • زمان واحد پردازش مرکزی فرایند بیش‌از ۵٪ افزایش نیابد
  • قاب‌های ناموزون حداکثر ۱ درصد افزایش یابد
  • زمان پردازش فریم P95 بیش‌از ۳ میلی‌ثانیه افزایش نیابد
  • زمان ازسرگیری گرم (TotalTime از adb shell am start -W) برای بودجه‌های perceptible و background بیش‌از ۱۵٪ افزایش می‌یابد

زانو کمترین بودجه نامزد است که پذیرفته می‌شود. برای مثال، در اینجا یک جاروب پیش‌زمینه برای یک برنامه فرضی با اوج نامحدود ۱۳۸ مگابایت آورده شده است:

بودجه نامزد memory.current anon file خطاهای مجدد (صفحه) زمان پردازش فریم صدک ۹۵ رژیم
هیچ‌کدام ‫۱۳۸ مگابایت ‫۷۵ مگابایت ‫۶۳ مگابایت 0 ‫۱۰٫۵ میلی‌ثانیه مقدار پایه
‫۱۲۵ مگابایت ‫۱۲۱ مگابایت ‫۷۴ مگابایت ‫۴۷ مگابایت 10 ‫۱۰٫۵ میلی‌ثانیه بالای زانو
‫۱۱۰ مگابایت ‫۱۱۰ مگابایت ‫۷۵ مگابایت ‫۳۵ مگابایت 15 ‫۱۰٫۸ میلی‌ثانیه زانو
‫۹۵ مگابایت ‫۹۵ مگابایت ‫۷۴ مگابایت ‫۲۱ مگابایت 176 ‫۱۱٫۲ میلی‌ثانیه زیر زانو
‫۸۵ مگابایت ‫۸۵ مگابایت ‫۶۸ مگابایت ‫۱۷ مگابایت ۲٬۰۵۰ ‫۱۲٫۸ میلی‌ثانیه زیر زانو
‫۷۰ مگابایت ‫۷۰ مگابایت ‫۵۴ مگابایت ‫۱۶ مگابایت ۸٬۷۷۱ ‫۱۶٫۵ میلی‌ثانیه خیلی پایین‌تر از زانو

بودجه‌تان را انتخاب کنید

android:maxMb را ۱۰٪ تا ۲۰٪ بالاتر از حد آستانه تنظیم کنید تا تفاوت در داده‌های کاربر، مکان‌ها، و جهش‌های تخصیص برنامه شما را به زیر حد آستانه نبرد. در مثال قبلی، زانو روی ۱۱۰ مگابایت است، بنابراین ۱۲۵ مگابایت انتخاب خوبی است.

سپس ساخت نسخهٔ پخش را با اعلامیه‌های <memory-budget> جدید نصب کنید، جریان کاربر را دوباره اجرا کنید، و بررسی کنید که معیارهای قبولی همچنان برقرار باشند. dumpsys meminfo بودجه فعال و منبع آن را در بخش «بودجه حافظه» نشان می‌دهد.

اعتبارسنجی بودجه‌های غیرپیش‌زمینه با بازگشت گرم

بودجه محدود perceptible یا background ممکن است زمانی که برنامه‌تان در آن وضعیت است خوب به‌نظر برسد و سپس وقتی کاربر برمی‌گردد برایتان هزینه داشته باشد. سفر برگشت را هم امتحان کنید:

  1. جریان پیش‌نما را اجرا کنید، برنامه را به حالت هدف منتقل کنید، و بودجه نامزد را با am memory-limiter manual اعمال کنید.
  2. اجازه دهید کار قابل‌درک یا پس‌زمینه ۱۰ تا ۱۵ ثانیه اجرا شود.
  3. لغو (adb shell am memory-limiter manual <PID> none) را پاک کنید، سپس فوراً برنامه را با adb shell am start -W برگردانید و TotalTime را ضبط کنید. اگر نامزد را ازطریق MemoryBudgetManager تنظیم کردید، به‌جای آن، قبل‌از بازگشت برنامه، با clearProcessBudget تماس بگیرید.
  4. تعامل پیش‌زمینه کوتاهی اجرا کنید و خطاهای مجدد و آمار قاب را با معیارهای قبولی مقایسه کنید.

کمترین بودجه‌ای را که هر دو را درحالی‌که برنامه در آن وضعیت است و در بازگشت گرم است پاس می‌کند انتخاب کنید، و همان ۱۰٪ تا ۲۰٪ حاشیه امن را اضافه کنید.

واسنجی مقیاس‌بندی نمایشگر

اگر از android:additionalBytesPerDisplayPixel استفاده می‌کنید، زانوی پیش‌زمینه را در دو نمایشگر با وضوح‌های متفاوت، مثلاً ۱۰۸۰ × ۲۴۰۰ (تقریباً ۲٫۵۹ میلیون پیکسل) و ۱۴۴۰ × ۳۱۲۰ (تقریباً ۴٫۴۹ میلیون پیکسل) پیدا کنید. تفاوت بین دو زانو را برحسب بایت بر تفاوت تعداد پیکسل‌ها تقسیم کنید تا android:additionalBytesPerDisplayPixel را به‌دست آورید. آن مقدار هر پیکسل را برای نمایشگر کوچک‌تر از زانوی آن کم کنید تا پایه مستقل از وضوح را به‌دست آورید، و فضای اضافی را اضافه کنید تا android:maxMb را به‌دست آورید.

میاناهای برنامه‌سازی کاربردی زمان اجرا (گزینه پویای ثانویه)

اعلام بودجه به‌صورت ایستا در AndroidManifest.xml راه‌حل ترجیحی برای تقریباً همه برنامه‌ها است. بااین‌حال، برای برنامه‌هایی با حجم کاری پویا یا برای آزمایش زمان اجرا، Android میاناهای برنامه‌سازی کاربردی کیت توسعه نرم‌افزار و کیت توسعه بومی زمان اجرا را به‌عنوان گزینه ثانویه ارائه می‌دهد.

‫runtime API به شما امکان می‌دهد:

  • استفاده فعلی از حافظه و بودجه‌های مؤثر را پُرسمان کنید.
  • بودجه پردازش را به‌طور پویا کاهش دهید.
  • به رویدادهای فراتر از بودجه گوش دهید تا قبل‌از اینکه سیستم‌عامل بازیابی مستقیم را راه‌اندازی کند، حافظه‌های پنهان را به‌طور فعال کاهش دهید.

Android SDK API (MemoryBudgetManager)

سرویس سیستم MemoryBudgetManager برای برنامه‌های نوشته‌شده به زبان Kotlin و Java از Android 17 QPR2 (نسخه جزئی کیت توسعه نرم‌افزار، سطح میانای برنامه‌سازی کاربردی ۳۷.۲ / Build.VERSION_CODES_FULL.CINNAMON_BUN_2) دردسترس است.

بازیابی سرویس

قبل‌از دسترسی به MemoryBudgetManager، با بررسی SDK_INT_FULL مطمئن شوید که دستگاه از Android 17 QPR2 یا بالاتر استفاده می‌کند. اگر minSdk برنامه‌تان کمتر از ۳۶ است، ابتدا SDK_INT را بررسی کنید تا دستگاه‌هایی که Android نسخه ۱۵ (سطح API‏ ۳۵) یا پایین‌تر را اجرا می‌کنند هنگام خواندن SDK_INT_FULL، NoSuchFieldError را نشان ندهند:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
    Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

استفاده از پُرسمان و بودجه‌ها

// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes

// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

تنظیم یا پاک کردن پویا بودجه‌ها

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

// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()

برخلاف بندهای مانیفست <memory-budget> که هرگاه فرایند بین حالت‌های foreground، perceptible، و background جابه‌جا می‌شود به‌طور خودکار بودجه‌ها را تغییر می‌دهد، بودجه پویایی که ازطریق MemoryBudgetManager تنظیم می‌شود تا زمانی که کد شما آن را به‌روزرسانی یا پاک کند، یک سقف واحد را اعمال می‌کند. اگر از میانای برنامه‌سازی کاربردی زمان اجرا برای اجرای آزمایش‌های میدانی یا مدیریت محدودیت‌های خاص وضعیت استفاده می‌کنید، بودجه پویا را در سراسر انتقال‌های چرخه حیات به‌روزرسانی یا پاک کنید (برای مثال، بااستفاده از ProcessLifecycleOwner یا فراخوانی‌های چرخه حیات فعالیت، همان‌طور که در نمونه ویرایشگر تصویر تطبیقی نشان داده شده است).

عملکرد و هم‌پایگی

هنگام کار با MemoryBudgetManager در زمان اجرا، ویژگی‌های زیر را به‌خاطر داشته باشید:

  • تغییرناپذیری: همه میاناهای برنامه‌سازی کاربردی تغییر بودجه تغییرناپذیر هستند. فراخوانی مکرر clearProcessBudget() یا clearPackageBudget() یا زمانی که هیچ بودجه پویایی فعال نیست، یک عملیات بی‌اثر ایمن است. به‌طور مشابه، تنظیم متوالی setProcessBudgetBytes() یا setPackageBudgetBytes() روی یک مقدار یکسان، پیکربندی‌های مجدد سیستم زائد را فعال نمی‌کند.
  • دفعات فراخوانی: تنظیم، به‌روزرسانی، یا پاک کردن بودجه باعث می‌شود فراخوانی بین‌فرایندی به سرویس سیستم برای به‌روزرسانی کنترل‌کننده حافظه سیستم‌عامل برای فرایند انجام شود. اگرچه سبک است، اما کاملاً رایگان نیست. این میاناهای برنامه‌سازی کاربردی را درطول انتقال‌های چرخه حیات گسسته و نقاط عطف تکلیف (مانند ورود یا خروج از فعالیت، شروع یا تکمیل پردازش پس‌زمینه، یا رسیدگی به درخواست‌های گفتار) فراخوانی کنید و از فراخوانی آن‌ها در حلقه‌های تنگ، رشته‌های پردازش صدا، یا روال‌های ارائه هر فریم خودداری کنید.
  • بسامد پُرسمان: ویژگی‌های پُرسمان processCurrentUsageBytes و packageCurrentUsageBytes استفاده از حافظه زنده را از سرویس سیستم واکشی می‌کنند. درحالی‌که سریع است، پُرسمان‌ها باید درصورت درخواست (مثلاً درطول انتقال تکلیف یا داخل فراخوانی‌های برگشتی OnOverBudgetListener) فراخوانده شوند، نه اینکه در حلقه‌های پیوسته نظرسنجی شوند.

Listen for over-budget pressure callbacks

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

val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict thread-safe caches on the background Looper
    imageTileCache.evictAll()
}

// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)

// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

روال‌های مطلوب برای بازخوانی‌های فراتر از بودجه:

  • استفاده از پس‌زمینه Looper: هر زمان ممکن بود، پس‌زمینه HandlerThread Looper را به registerProcessOverBudgetListener یا registerPackageOverBudgetListener منتقل کنید. اجرای تخلیه حافظه نهان یا پاک‌سازی فایل در رشته پس‌زمینه از رقابت با پردازش قاب در رشته میانای کاربر در شرایطی که فرایند تحت فشار حافظه است جلوگیری می‌کند. Looper.getMainLooper را برای شنوندگانی رزرو کنید که فقط با حالت واسط کاربر محدود به رشته اصلی در تماس هستند.
  • انتظار بازخوانی‌های مکرر را داشته باشید: بازخوانی‌های فراتر از بودجه اعلان‌های یک‌باره نیستند. وقتی استفاده از بودجه فراتر می‌رود، سیستم‌عامل ابتدا صفحات سرد را بازیابی می‌کند، و اگر فشار ادامه یابد، سیستم به بازیابی حافظه و فراخوانی شنونده شما حداکثر یک بار در ثانیه ادامه می‌دهد. فراخوانی برگشتی‌تان را هم‌توان کنید. اگر برنامه‌تان فقط به یک اعلان نیاز دارد یا می‌خواهد زمان استراحت طولانی‌تری بین مراحل پاک‌سازی داشته باشد، unregisterProcessOverBudgetListener (یا unregisterPackageOverBudgetListener) را در فراخوانی متقابل فراخوانی کنید، یا پاک‌سازی‌تان را با مُهر زمان استراحت یا AtomicBoolean محافظت کنید.
  • ثبت‌نام قبل‌از محدود کردن بودجه: اگر مصرف حافظه فعلی از قبل از بودجه جدید فراتر رفته باشد، وقتی setProcessBudgetBytes یا setPackageBudgetBytes را فراخوانی می‌کنید، به‌محض اعمال بودجه جدید، رویداد فراتر از بودجه راه‌اندازی می‌شود. شنونده‌تان را قبل‌از تنظیم بودجه ثبت کنید تا برنامه‌تان آن اعلان اولیه فراتر از بودجه را ازدست ندهد.
  • سریع باشید: عملیات بازپس‌گیری باید تسکین فوری ارائه دهد. محاسبات پیچیده درطول فشار، عملکرد را بدتر می‌کند.
  • از تخصیص دادن اجتناب کنید: اشیاء جدید را تخصیص ندهید یا رشته‌های جدید را در داخل callback شروع نکنید، زیرا انجام این کار می‌تواند باعث شود سیستم‌عامل فوراً بازیابی مستقیم را راه‌اندازی کند.
  • تمرکز بر اهداف با بازده بالا: بیرون کردن Bitmapهای بزرگ، میان‌گیرهای رندر، یا بستن فایل‌های نگاشت‌شده در حافظه بسیار مؤثرتر از آزاد کردن تعداد زیادی شیء کوچک است.

میانای برنامه‌سازی کاربردی NDK بومی (<android/memory_budget_manager.h>)

برنامه‌های بومی می‌توانند از C NDK API که توسط libandroid.so ارائه شده است، از Android 17 QPR2 (سطح API 37.2) استفاده کنند.

پیکربندی CMake

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

استفاده از سرصفحه و پُرسمان را اضافه کنید

#include <android/memory_budget_manager.h>

// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();

// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
    // Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
    // No budget is currently active
}

پیکربندی پویا برای بودجه بومی

// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
    const char* error_msg = AMemoryBudgetManager_resultToString(result);
    // Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}

// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();

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

‫NDK دو روش برای نظارت بر رویدادهای حافظه ارائه می‌دهد:

  1. ناظر سطح بالا (AMemoryBudgetManager_Watcher_create): رویدادها را در ALooper با لرزش‌گیری خودکار نظارت می‌کند.
  2. توصیف‌گر فایل سطح پایین: AMemoryBudgetManager_getProcessMemoryPressureFd یک توصیف‌گر فایل بومی برمی‌گرداند که می‌تواند مستقیماً در یک حلقه موتور epoll سفارشی ادغام شود.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
    // High-yield eviction of unused native textures or geometry caches
    purgeNativeTextureCaches();
}

// Register watcher on a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    background_looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

ناظر را به‌جای رشته اصلی میانای کاربر یا رشته پردازش به ALooper رشته عامل پس‌زمینه متصل کنید تا تخلیه حافظه نهان با پردازش فریم تداخل نداشته باشد. قبل‌از محدود کردن بودجه با AMemoryBudgetManager_setProcessBudget یا AMemoryBudgetManager_setPackageBudget، ناظر را ایجاد کنید تا برنامه‌تان رویداد اولیه فراتر از بودجه را ازدست ندهد. تا زمانی که فرایند یا بسته بیش‌از بودجه باقی بماند و سیستم‌عامل به بازیابی حافظه ادامه دهد، ناظر به‌طور مکرر (حداکثر یک‌بار در هر فاصله زمانی debounce_ms) فراخوانی برگشتی را فراخوانی می‌کند. فراخوانی برگشتی‌تان را یکسان‌توان کنید، یا اگر برنامه‌تان فقط به اعلان یک‌باره نیاز دارد، AMemoryBudgetManager_Watcher_destroy را فراخوانی کنید (که فراخوانی آن از داخل فراخوانی برگشتی ایمن است).

نمونه‌های Runtime API

مثال‌های زیر نحوه پیاده‌سازی میاناهای برنامه‌سازی کاربردی زمان اجرا را بااستفاده از میانای برنامه‌سازی کاربردی کیت توسعه نرم‌افزار Android (نوشته‌شده در Kotlin) و میانای برنامه‌سازی کاربردی NDK بومی (نوشته‌شده در ++C) نشان می‌دهد.

نمونه کیت توسعه نرم‌افزار Android: ویرایشگر تصویر تطبیقی

این مثال برنامه ویرایش تصویری (com.example.imageeditor) را نشان می‌دهد که مانیفست آن سقف ۲۵۶ مگابایتی را برای بوم چندلایه‌اش اعلام می‌کند:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

وقتی کاربر درحال مرور گالری ریزعکس سبک است، برنامه از میانای برنامه‌سازی کاربردی کیت توسعه نرم‌افزار Android در Kotlin استفاده می‌کند تا بودجه پردازش خود را به‌طور پویا به ۹۶ مگابایت کاهش دهد. وقتی کاربر بوم ویرایش چندلایه را باز می‌کند، برنامه بودجه پویا را پاک می‌کند تا سقف مانیفست کامل ۲۵۶ مگابایتی را بازیابی کند. همچنین یک OnOverBudgetListener را در HandlerThread پس‌زمینه Looper ثبت می‌کند تا قبل‌از اعمال بودجه گالری برای حذف نقشه‌های بیتی پیش‌نمایش ذخیره‌شده در حافظه نهان تحت فشار، بدون مسدود کردن رشته میانای کاربر، اقدام کند.

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager
    private lateinit var cleanupThread: HandlerThread

    // Thread-safe in-memory cache for rendered preview tiles (32MB limit)
    private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
        override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
    }

    private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
        Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
        previewCache.evictAll()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        budgetManager = getSystemService(MemoryBudgetManager::class.java)
        cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
    }

    override fun onStart() {
        super.onStart()
        // Register listener on a background Looper before tightening the budget
        budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    override fun onDestroy() {
        cleanupThread.quitSafely()
        super.onDestroy()
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     * Clears the dynamic budget, restoring the full 256MB manifest ceiling.
     */
    fun enterEditingCanvas() {
        // Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
        budgetManager.clearProcessBudget()
        Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     * Re-applies the tighter dynamic budget.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        applyGalleryBudget()
    }

    private fun applyGalleryBudget() {
        try {
            // Dynamically tighten budget to 96MB for the lightweight gallery view
            budgetManager.processBudgetBytes = 96L * 1024L * 1024L
            Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

نمونه NDK C++‎: موتور سه‌بعدی بومی

این مثال موتور بازی C++‎ بومی را نشان می‌دهد که بودجه‌های حافظه را براساس سطح کیفیت گرافیک فعال مدیریت می‌کند، با این فرض که مانیفست برنامه سقف ۵۱۲ مگابایت را برای تطبیق با گرافیک با کیفیت بالا (android:maxMb="512") اعلام می‌کند. موتور AMemoryBudgetManager_Watcher_create را در ALooper رشته عامل پس‌زمینه شروع می‌کند قبل‌از اینکه بودجه را برای پیش‌تنظیم‌های کیفیت پایین‌تر محدود کند، و mipmap بافت را هنگام فراتر رفتن از بودجه تخلیه می‌کند.

#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>

#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)

class MemoryGovernor {
public:
    MemoryGovernor() : mWatcher(nullptr) {}

    ~MemoryGovernor() {
        stopMonitoring();
    }

    // Configures process budget based on user graphics quality settings
    bool setQualityBudget(int qualityLevel) {
        int64_t targetBytes = 0;
        switch (qualityLevel) {
            case 0: // Low (budget: 128MB)
                targetBytes = 128LL * 1024 * 1024;
                break;
            case 1: // Medium (budget: 256MB)
                targetBytes = 256LL * 1024 * 1024;
                break;
            case 2: // High (budget: 512MB)
                targetBytes = 512LL * 1024 * 1024;
                break;
            default:
                // Clear dynamic override and restore manifest limit
                AMemoryBudgetManager_clearProcessBudget();
                return true;
        }

        AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
        if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
            LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
            return false;
        }
        return true;
    }

    // Call on a background worker ALooper before tightening the budget with setQualityBudget
    bool startMonitoring(ALooper* backgroundLooper) {
        if (!backgroundLooper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            backgroundLooper,
            AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
            1000,
            &MemoryGovernor::onPressureEvent,
            this
        );
        return mWatcher != nullptr;
    }

    void stopMonitoring() {
        if (mWatcher) {
            AMemoryBudgetManager_Watcher_destroy(mWatcher);
            mWatcher = nullptr;
        }
    }

    void unloadUnusedTextures() {
        LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
        // Fast, idempotent, high-yield eviction without allocating memory
    }

private:
    static void onPressureEvent(
        int32_t event_mask,
        const AMemoryBudgetEvents* events,
        void* userdata
    ) {
        auto* governor = static_cast<MemoryGovernor*>(userdata);
        governor->unloadUnusedTextures();
    }

    AMemoryBudgetManagerWatcher* mWatcher;
};