ضبط ميزانيات ذاكرة التطبيقات

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

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

  1. يتم أولاً إخلاء الصفحات المستندة إلى ملفات (مثل الرموز غير النشطة والأصول التي تم ربطها) لأنّه يمكن إعادة قراءتها من مساحة التخزين عند الحاجة.
  2. تتم إعادة كتابة الصفحات غير النظيفة التي يتم الاحتفاظ بنسخ احتياطية منها في الملفات إلى مساحة التخزين وإزالتها.
  3. يتم ضغط صفحات الذاكرة المجهولة (مثل عمليات تخصيص الذاكرة المؤقتة) وتبديلها إلى zRAM.

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

ماذا يحدث عندما يتجاوز تطبيق ما ميزانيته؟

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

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

للتعرّف على كيفية إدارة نظام التشغيل للذاكرة، يمكنك الاطّلاع على دليل بنية الذاكرة، وتحديدًا القسم المتعلّق باستعادة الذاكرة والتبديل.

تحديد الميزانيات في ملف بيان Android

يُعدّ تحديد ميزانيات الذاكرة في AndroidManifest.xml الطريقة الأساسية والموصى بها لتحديد الميزانيات. ولا يتطلّب ذلك أي رمز وقت التشغيل، ويسري مفعوله فور بدء العملية، كما يوفّر عقدًا واضحًا لنظام التشغيل.

تسري إشعارات <memory-budget> على الأجهزة التي تعمل بالإصدار Android 17 QPR2 (المستوى 37.2 لواجهة برمجة التطبيقات) والإصدارات الأحدث. في إصدارات 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>

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

اختلاف الميزانيات حسب حالة العملية

يحتاج التطبيق إلى مقادير مختلفة من الذاكرة حسب مستوى ظهوره للمستخدمين:

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

يمكنك تعريف عدة عبارات <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.

كيفية ربط حالات الميزانية بحالات العملية

تربط المنصة حالات ميزانية ملف البيان بحالات عملية وقت التشغيل استنادًا إلى RunningAppProcessInfo.importance:

  • foreground: تفاعلات المستخدم النشطة والمرئية، مثل استضافة نشاط تم استئنافه أو الحفاظ على حالة التطبيق في المقدّمة عند إيقاف تشغيل الشاشة
  • perceptible: أحمال العمل التي يمكن للمستخدم إدراكها بدون نافذة مرئية، مثل تشغيل الوسائط النشط أو اتجاهات مفصّلة أو تسجيل الكاميرا أو الميكروفون أو عمليات التنزيل النشطة في الخلفية أو خدمات المزامنة النشطة للبيانات في المقدّمة على الرغم من أنّ مقاييس المنصة الداخلية قد تصنّف بعض أحمال العمل هذه على أنّها 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
IMPORTANCE_CACHED
المهام في الخلفية، وأدوات الاستقبال، والمزامنة في الخلفية، والعمليات المخزّنة مؤقتًا

فحص حالة عملية تطبيقك وميزانيته

إحدى طرق فحص حالة العملية النشطة وأهميتها في تطبيقك أثناء عملية التطوير هي طلب البحث من &quot;مدير الأنشطة&quot; باستخدام 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.
  • في جدول التحكّم في عملية OOM، يشير الرمز prcp إلى أنّه يتم تقييم العملية ضمن مستوى الأولوية المحسوس، والذي يتوافق مع ميزانية perceptible.

لفحص ميزانية الذاكرة المفروضة حاليًا واستخدام الذاكرة المقيم، اتّبِع الخطوات التالية:

adb shell dumpsys meminfo <package-name>

بدءًا من الإصدار الثاني من Android 17 QPR، يتضمّن هذا الناتج قسم ميزانية الذاكرة الذي يعرض الحدّ الأقصى النشط ومصدر الحدّ (مثل AndroidManifest أو MemoryBudgetManager) والذاكرة المقيمة الحالية.

التطبيقات المتعددة العمليات

إذا كان تطبيقك يقسّم عمله على عدة عمليات، اضبط ميزانيات مخصّصة للعمليات باستخدام العلامة <process> داخل <processes>.

على سبيل المثال، لنفترض أنّ لديك تطبيقًا لبث الموسيقى (com.example.radio):

  1. العملية الرئيسية: تستضيف واجهة المستخدم المرئية ومحرّك تشغيل الصوت (MediaSessionService مع mediaPlayback خدمة تعمل في المقدّمة). وعندما تكون مرئية، تعمل العملية ضمن ميزانية المقدّمة البالغة 180 ميغابايت. عندما يغادر المستخدم التطبيق أثناء استمرار تشغيل الموسيقى، تنتقل العملية إلى الحالة perceptible، حيث تكون ميزانية 64 ميغابايت كافية لمشغّل الموسيقى ومخزن الصوت المؤقت.
  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" />

        <!-- Dedicated background sync process -->
        <processes>
            <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>

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

تعديل الميزانيات لتناسب الشاشات العالية الكثافة

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

  • التحجيم حسب مجموعة كثافة العرض (android:additionalMbPerDensity): تتم إضافة ميغابايت بشكل متناسب مع نسبة الكثافة للشاشة مقارنةً بـ mdpi (1.0x / 160 نقطة لكل بوصة). يكون هذا الخيار مناسبًا عندما يتناسب استخدام الذاكرة مع حِزم كثافة واجهة المستخدم، مثل التخزين المؤقت للرسومات النقطية ذات الدقة العالية أو مواد عرض واجهة المستخدم:

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    على شاشة mdpi (1.0x)، تكون الميزانية 180 + 16 × 1 = 196 ميغابايت. على شاشة xxhdpi (3.0x)، يتم تغيير حجم الميزانية إلى 180 + 16 × 3 = 228 ميغابايت.

  • القياس حسب دقة العرض المادية (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" />
    

    على شاشة عرض بدقة 1080p (1080 × 2400 بكسل، أي حوالي 2.59 مليون بكسل)، يضيف ذلك حوالي 41.4 ميغابايت إلى الميزانية الأساسية. على شاشة بدقة 1440 بكسل (1440 × 3120 بكسل، أي حوالي 4.49 مليون بكسل)، تتم إضافة حوالي 71.8 ميغابايت.

هاتان السمتان بديلتان. اختَر السمة التي تتطابق مع عامل القياس الأساسي لتطبيقك، وتجنَّب الجمع بينهما في البند البرمجي نفسه.

التخصّص في أشكال الأجهزة

عند شحن حزمة APK على الهواتف والأجهزة اللوحية وأجهزة Wear OS، استخدِم السمة android:feature لتعديل الميزانيات بما يتناسب مع الأجهزة المختلفة المستهدَفة.

على ساعات Wear OS، تكون ذاكرة الوصول العشوائي (RAM) محدودة، كما أنّ واجهة المستخدم ومجموعة الميزات في التطبيق تكونان أبسط بكثير. يمكنك تحديد ميزانية أكثر صرامة مخصّصة لميزة 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) ويتم ربطها بـ cgroup memory.current في نظام التشغيل Linux (الذي يستبعد الذاكرة المشتركة مثل Zygote).

السمة التنسيق تلقائي الوصف
android:maxMb عدد صحيح (> 0) مطلوب الحدّ الأقصى الأساسي لميزانية الذاكرة المقيمة بالميغابايت.
android:state تعداد أي حالة العملية التي تنطبق عليها هذه الميزانية: foreground أو perceptible أو background في حال حذفها، تعمل العبارة كخيار احتياطي لأي حالة غير محدّدة.
android:additionalMbPerDensity عدد صحيح (≥ 0) 0 عدد الميجابايت الإضافية المطلوب إضافتها لكل وحدة من نسبة كثافة العرض مقارنةً بـ mdpi (1.0x).
android:additionalBytesPerDisplayPixel عدد صحيح (≥ 0) 0 عدد البايتات الإضافية المخصّصة لكل بكسل في الشاشة المادية (العرض × الارتفاع)، وهو مفيد لمخازن مؤقتة للسطح والصور النقطية.
android:feature سلسلة أي تقصر هذه العبارة على الأجهزة التي تحدّد ميزات أجهزة معيّنة: watch أو automotive أو leanback.

واجهات برمجة التطبيقات في وقت التشغيل (الخيار الديناميكي الثانوي)

يُعدّ تحديد الميزانيات بشكل ثابت في ملف AndroidManifest.xml هو الحلّ المفضّل لجميع التطبيقات تقريبًا. ومع ذلك، يوفّر نظام التشغيل Android واجهات برمجة تطبيقات لحِزم SDK وNDK في وقت التشغيل كخيار ثانوي للتطبيقات التي تتضمّن أحمال عمل ديناميكية أو للتجارب في وقت التشغيل.

تتيح لك واجهة برمجة التطبيقات لوقت التشغيل إجراء ما يلي:

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

واجهة برمجة التطبيقات لحزمة تطوير البرامج (SDK) لنظام التشغيل Android‏ (MemoryBudgetManager)

تتوفّر خدمة نظام MemoryBudgetManager للتطبيقات المكتوبة بلغتَي Kotlin وJava بدءًا من الإصدار الثاني من Android 17 QPR (إصدار ثانوي من حزمة تطوير البرامج (SDK)، المستوى 37.2 من واجهة برمجة التطبيقات / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

استرداد الخدمة

قبل الوصول إلى MemoryBudgetManager، تأكَّد من أنّ الجهاز لا يعمل بإصدار أقدم من Android 17 QPR2 باستخدام SDK_INT_FULL:

if (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()

الاستماع إلى عمليات معاودة الاتصال المتعلقة بتجاوز الميزانية

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

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

أفضل الممارسات المتعلّقة بعمليات ردّ الاتصال التي تتجاوز الميزانية:

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

واجهة برمجة التطبيقات الأصلية في NDK (<android/memory_budget_manager.h>)

يمكن للتطبيقات الأصلية استخدام واجهة برمجة التطبيقات C NDK التي تم عرضها من خلال libandroid.so بدءًا من Android 17 QPR2 (المستوى 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. High-Level Watcher (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 an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

أمثلة على واجهات برمجة التطبيقات في وقت التشغيل

توضّح الأمثلة التالية كيفية تنفيذ واجهات برمجة التطبيقات في وقت التشغيل باستخدام واجهة برمجة التطبيقات لحزمة تطوير البرامج (SDK) لنظام التشغيل Android (المكتوبة بلغة Kotlin) وواجهة برمجة التطبيقات الأصلية لحزمة NDK (المكتوبة بلغة C++‎).

مثال على حزمة تطوير البرامج (SDK) لنظام التشغيل Android: أداة تعديل الصور التكيّفية

يعرض هذا المثال تطبيقًا لتعديل الصور (com.example.imageeditor) يحدّد ملف البيان الخاص به سقفًا يبلغ 256 ميغابايت لاستيعاب لوحة العرض المتعددة الطبقات:

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

عندما يتصفّح المستخدم معرض الصور المصغّرة الخفيف، يستخدم التطبيق واجهة برمجة التطبيقات لحزمة تطوير البرامج (SDK) لنظام التشغيل Android في Kotlin لتقليل ميزانية العملية بشكل ديناميكي إلى 96 ميغابايت. عندما يفتح المستخدم لوحة العرض متعددة الطبقات، يمحو التطبيق الميزانية الديناميكية لاستعادة الحد الأقصى الكامل لبيان 256 ميغابايت. تسجّل هذه السمة أيضًا OnOverBudgetListener لإزالة الصور النقطية المخزّنة مؤقتًا للمعاينات عند الحاجة.

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

    // 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)
    }

    override fun onStart() {
        super.onStart()
        // Register listener for process-level memory breaches
        budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

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

    /**
     * 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"
    }
}

مثال على C++‎ في NDK: محرّك ثلاثي الأبعاد أصلي

يوضّح هذا المثال محرك ألعاب C++‎ أصليًا يدير ميزانيات الذاكرة استنادًا إلى مستوى جودة الرسومات النشط، على افتراض أنّ بيان التطبيق يحدّد سقفًا يبلغ 512 ميغابايت لاستيعاب الرسومات عالية الجودة (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;
    }

    bool startMonitoring(ALooper* looper) {
        if (!looper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            looper,
            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, 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;
};