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

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

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

  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، تعمل كخيار تلقائي للحالات غير المحدّدة (مثل foreground)، والتي تلغيها العبارات اللاحقة الأكثر تقييدًا عندما ينتقل التطبيق إلى حالتَي perceptible أو background.

عند تحديد حجم الميزانيات لكل حالة، استهدِف مجموعة العمل النشطة لتلك الحالة بدلاً من الحد الأقصى للذاكرة المقيمة (الحد الأقصى لمجموعة العمل):

  • تحديد حجم ميزانيات 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
الخدمات التي تعمل في المقدّمة لتشغيل الوسائط أو التنقّل أو عمليات التنزيل أو المزامنة النشطة، والخدمات المرتبطة بعلامات foreground-service أو علامات visible
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
الوظائف في الخلفية، وأجهزة الاستقبال، والمزامنة في الخلفية، والنشاط السابق أو تطبيق Home في الخلفية

لمعرفة التفاصيل حول كيفية رفع عمليات ربط العميل لعملية مضيف الخدمة أو مقدّم الخدمة وكيفية عودة العملية إلى background أو cached بعد إلغاء ربط العميل أو الانتقال خارج الشاشة، يُرجى الاطّلاع على عمليات ربط الخدمة وحالات العملية.

عندما تدخل عملية ما في حالة التخزين المؤقت (IMPORTANCE_CACHED) بدون أي مكوّنات نشطة، يحرّر النظام الأساسي ميزانية الذاكرة النشطة ويدير الذاكرة من خلال استعادة العمليات المخزّنة مؤقتًا بشكل عادي.

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

من بين طرق فحص حالة العملية النشطة لتطبيقك وأهميتها أثناء التطوير هي الاستعلام عن &quot;مدير النشاط&quot; باستخدام ADB:

adb shell dumpsys activity processes <package-name>

يوضّح المثال المختصر التالي سجلّ العملية وإدخالات التحكّم في نفاد الذاكرة (OOM) لتطبيق يشغّل خدمة تعمل في المقدّمة:

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 QPR2، يتضمّن هذا الناتج قسم ميزانية الذاكرة الذي يعرض الحدّ الأقصى النشط ومصدر الحدّ (مثل 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" />

        <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 آليتَين بديلتَين لتغيير الميزانيات ديناميكيًا حسب مواصفات الشاشة:

  • Scale by display density bucket (android:additionalMbPerDensity): Adds megabytes proportionally to the square of the display's density ratio relative to mdpi (1.0x / 160 dpi), because 2D raster drawables and UI assets scale across both width and height:

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

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

  • التحجيم حسب دقة العرض المادية (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 ميغابايت تقريبًا إلى الميزانية الأساسية. على شاشة بدقة 1440p (1440 × 3120 &approx; 4.49 مليون بكسل)، يضيف &approx; 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) ويتم ربطها برسوم Linux cgroup memory.current (التي تستثني الذاكرة المشتركة مثل Zygote).

السمة التنسيق تلقائي الوصف
android:maxMb عدد صحيح (> 0) مطلوبة الحدّ الأقصى الأساسي لميزانية الذاكرة المقيمة بالميغابايت
android:state Enum أي حالة العملية التي تنطبق عليها هذه الميزانية: 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 QPR2 (إصدار ثانوي من حزمة تطوير البرامج (SDK)، المستوى 37.2 من واجهة برمجة التطبيقات / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

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

قبل الوصول إلى "MemoryBudgetManager"، تأكَّد من أنّ الجهاز يعمل بنظام التشغيل Android 17 QPR2 أو إصدار أحدث من خلال الاطّلاع على SDK_INT_FULL. إذا كان minSdk في تطبيقك أقل من 36، يُرجى التحقّق من SDK_INT أولاً حتى لا تظهر رسالة الخطأ NoSuchFieldError على الأجهزة التي تعمل بنظام التشغيل Android 15 (المستوى 35 من واجهة برمجة التطبيقات) أو الإصدارات الأقدم عند قراءة SDK_INT_FULL:

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) بدلاً من استطلاعها في حلقات مستمرة.

الاستماع إلى عمليات معاودة الاتصال بشأن الضغط الناتج عن تجاوز الميزانية

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

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

واجهة برمجة تطبيقات 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 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 لسلسلة الوحدات العاملة (worker thread) في الخلفية، بدلاً من سلسلة واجهة المستخدم الرئيسية أو سلسلة العرض، حتى لا يتعارض إخلاء ذاكرة التخزين المؤقت مع عرض اللقطات. أنشئ المراقب قبل تضييق الميزانية باستخدام AMemoryBudgetManager_setProcessBudget أو AMemoryBudgetManager_setPackageBudget حتى لا يفوت تطبيقك الحدث الأولي لتجاوز الميزانية. While the process or package remains over budget and the operating system continues reclaiming memory, the watcher invokes the callback repeatedly (at most once per debounce_ms interval). اجعل دالة الاستدعاء متكررة، أو استدعِ AMemoryBudgetManager_Watcher_destroy (التي يمكن استدعاؤها بأمان من داخل دالة الاستدعاء) إذا كان تطبيقك يحتاج فقط إلى إشعار لمرة واحدة.

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

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

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