تتيح ميزانيات ذاكرة التطبيقات للتطبيقات تحديد ميزانية ذاكرة خاصة بها، ما يوجّه النظام إلى تقليل استخدام الذاكرة عندما يستخدم التطبيق أكثر من الميزانية المحدّدة له. ويُعدّ ذلك مفيدًا بشكل خاص لتطبيقات النظام والتطبيقات المجمّعة أو التطبيقات التي تستهدف الأجهزة ذات الذاكرة المحدودة، حيث يعرف المطوّر مجموعة العمل المتوقّعة للذاكرة (الذاكرة التي يحتاجها التطبيق بشكل نشط لما يفعله المستخدم حاليًا) ويريد التأكّد من أنّ تطبيقه لا يستخدم الكثير من موارد ذاكرة الوصول العشوائي المشتركة في النظام.
يتم الحفاظ على توازن الميزانية من خلال إخلاء الذاكرة وتبديلها لإزالة صفحات الذاكرة التي لم يتم استخدامها مؤخرًا، مع التركيز على استهلاك الذاكرة في التطبيق على مجموعة العمل الحالية. عندما يتجاوز تطبيق ما الميزانية المحدّدة له، يستهدف نظام التشغيل عملية الاسترداد في هذا التطبيق تحديدًا:
- يتم أولاً إخلاء الصفحات النظيفة المستندة إلى ملفات (مثل الرموز غير النشطة ومواد العرض التي تم ربطها) لأنّه يمكن إعادة قراءتها من مساحة التخزين إذا لزم الأمر.
- تتم إعادة كتابة الصفحات غير الصالحة المستندة إلى الملفات في مساحة التخزين وإزالتها.
- يتم ضغط صفحات الذاكرة المجهولة المصدر (مثل عمليات تخصيص الذاكرة الديناميكية) ونقلها إلى zRAM.
طالما أنّ مجموعة العمل لا تتجاوز الميزانية، سيعمل التطبيق بشكل جيد بدون استخدام ذاكرة أكثر من الميزانية المحدّدة. ويزيل نظام التشغيل الذاكرة غير المستخدَمة ويضغط صفحات الذاكرة الديناميكية غير النشطة لتبديلها حتى تظل عمليات تخصيص الذاكرة محدودة بدون إنهاء العملية.
ماذا يحدث عندما يتجاوز تطبيق ميزانيته؟
لا يؤدي تجاوز ميزانية الذاكرة إلى إيقاف التطبيق أو تعطّله، بل عندما يستخدم التطبيق ذاكرة أكبر من الميزانية المسموح بها، يبحث نظام التشغيل عن الذاكرة التي لم يستخدمها التطبيق منذ فترة ويخزّنها بأمان إلى أن يحتاج إليها مرة أخرى. يؤدي ذلك إلى إتاحة مساحة لتخصيص ذاكرة جديدة للتطبيق والحفاظ على استخدام الذاكرة الإجمالي للتطبيق ضمن الميزانية.
طالما أنّ هناك مساحة بين ما يحتاجه التطبيق في أي وقت (مجموعة العمل الخاصة به) والميزانية المحدّدة، سيستمر التطبيق في العمل بشكل طبيعي. إذا حدّد تطبيق ميزانية أصغر من مجموعة العمل، قد يتباطأ أداء التطبيق أثناء التشغيل نتيجةً لذلك.
لمعرفة كيفية تتبُّع نظام التشغيل للذاكرة واستعادتها، يُرجى الاطّلاع على دليل بنية الذاكرة:
- استعادة الذاكرة والمبادلة: يوضّح هذا القسم كيفية احتساب نظام التشغيل لصفحات الذاكرة الديناميكية الخاصة المجهولة المصدر والصفحات المستندة إلى الملفات في تطبيقك واستعادة الصفحات غير النشطة عند تجاوز الميزانية.
- المفاهيم الأساسية: توضّح سبب استبعاد ذاكرة Zygote المشتركة من ميزانية تطبيقك وكيفية ارتباط ميزانيات الذاكرة المقيمة بمجموعة الصفحات المجهولة + مساحة التخزين المؤقت في بيانات القياس عن بُعد في المجال.
- الصور النقطية والذاكرة: يشرح هذا القسم كيفية احتساب الصور النقطية للأجهزة المستندة إلى وحدة معالجة الرسومات وعمليات تخصيص DMA-BUF خارج نطاق الذاكرة الديناميكية وميزانية الذاكرة في تطبيقك.
تحديد الميزانيات في ملف بيان 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>
لا يلزم استخدام عبارة "foreground" أو "omitted-state". إذا أعلنت عن عبارات perceptible أو background فقط، ستظل الحالات ذات الأهمية الأعلى، مثل foreground، غير مقيّدة بميزانية التطبيق. يؤدي حذف 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_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
نشاط مرئي تم استئنافه، أو تطبيق في المقدّمة مع قفل الشاشة، أو خدمة أو أداة توفير محتوى مرتبطة بالتطبيق في المقدّمة |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
الخدمات التي تعمل في المقدّمة لتشغيل الوسائط أو التنقّل أو عمليات التنزيل أو المزامنة النشطة، والخدمات المرتبطة بعلامات الخدمة التي تعمل في المقدّمة أو العلامات المرئية |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICEIMPORTANCE_CACHED |
المهام في الخلفية وأجهزة الاستقبال والمزامنة في الخلفية والعمليات المخزّنة مؤقتًا |
For details on how client bindings elevate a service or provider host process
and how the process transitions back to background or cached after the
client unbinds or moves off-screen, see Service bindings and process
states.
عندما تدخل عملية ما في حالة التخزين المؤقت (IMPORTANCE_CACHED) بدون أي مكوّنات نشطة، تواصل المنصة فرض ميزانية الذاكرة background إلى جانب تجميد العمليات المخزّنة مؤقتًا واستعادتها، ما يحافظ على تقييد العملية حتى في حال عدم حدوث تجميد أو ضغط.
فحص حالة عملية تطبيقك وميزانيته
من بين طرق فحص حالة العملية النشطة لتطبيقك وأهميتها أثناء التطوير هي الاستعلام عن "مدير النشاط" باستخدام 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>
التطبيقات المتعددة العمليات
إذا كان تطبيقك يقسّم عمله على عمليات متعددة، يمكنك ضبط
ميزانيات العمليات المخصّصة باستخدام علامة <process> داخل <processes>.
على سبيل المثال، لنفترض أنّ لديك تطبيقًا لبث الموسيقى (com.example.radio):
- العملية الرئيسية: تستضيف واجهة المستخدم المرئية ومحرّك تشغيل الصوت
(
MediaSessionServiceمع خدمةmediaPlaybackتعمل في المقدّمة). عندما تكون العملية مرئية، تعمل ضمن ميزانية العمليات التي تعمل في المقدّمة والتي تبلغ 180 ميغابايت. عندما يغادر المستخدم التطبيق أثناء استمرار تشغيل الموسيقى، تدخل العملية في الحالةperceptible، حيث تكون الميزانية التي تبلغ 64 ميغابايت كافية لمحرّك التشغيل ومخزن الصوت المؤقت. - عملية المزامنة (
: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 tomdpi(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): يضيف وحدات بايت لكل بكسل في حدود النافذة النشطة (WindowMetrics.getBoundsالعرض × الارتفاع). ويناسب هذا الإعداد التطبيقات التي تخصص مساحات رسومات بملء النافذة أو مخازن مؤقتة للعرض أو مخازن مؤقتة للصور العالية الدقة، حيث يتناسب استهلاك الذاكرة بشكل مباشر مع عدد وحدات البكسل الأولية بدلاً من مجموعات كثافة واجهة المستخدم:<!-- Baseline 128MB + 16 bytes per window pixel --> <!-- For example, a 4-byte RGBA full-window buffer with double or quadruple buffering --> <memory-budget android:maxMb="128" android:additionalBytesPerDisplayPixel="16" />في نافذة بملء الشاشة بدقة 1080p (1080 × 2400 بكسل، أي ما يعادل 2.59 مليون بكسل تقريبًا)، يضيف ذلك 41,472,000 بايت (أي ما يعادل 39.6 ميغابايت تقريبًا، حيث 1 ميغابايت = 1,048,576 بايت) إلى الميزانية الأساسية. في نافذة بدقة 1440p (1440 × 3120 ≈ 4.49 مليون بكسل)، يضيف 71,884,800 بايت (≈ 68.6 ميغابايت).
هاتان السمتان بديلتان. اختَر السمة التي تتطابق مع عامل القياس الأساسي لتطبيقك، وتجنَّب الجمع بينهما في البند نفسه.
التخصيص حسب أشكال الأجهزة
عند شحن حزمة 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> متعددة لتطبيق أو عملية، يقيّمها النظام بالترتيب الذي تظهر به في البيان ويفرض بند الميزانية الأخير الساري.
يتم ضبط القيمة التلقائية لسمة android:state التي تم حذفها على foreground، ويتم تطبيقها أيضًا على perceptible وbackground ما لم يتم إلغاؤها من خلال عبارة لاحقة لهذه الحالات. يجب ترتيب البنود من foreground (أو android:state المحذوفة) إلى
perceptible إلى background، ووضع عمليات إلغاء android:feature ضمن
مجموعة الحالة المطابقة حتى لا يؤدي إلغاء الحالة المحذوفة اللاحقة إلى حجب بنود
perceptible أو background السابقة.
مرجع سمات XML
تستخدم جميع سمات الميغابايت (MB) وحدات ثنائية (1 ميغابايت = 1,048,576 بايت)
ويتم ربطها برسوم memory.current لمجموعة التحكم (cgroup) في نظام التشغيل Linux (التي تستبعد الذاكرة
المشتركة من Zygote).
| السمة | التنسيق | تلقائي | الوصف |
|---|---|---|---|
android:maxMb |
عدد صحيح (> 0) | مطلوبة | الحدّ الأقصى الأساسي لميزانية الذاكرة المقيمة بالميغابايت |
android:state |
Enum | foreground |
حالة العملية التي تنطبق عليها هذه الميزانية: foreground أو perceptible أو background. في حال حذفها، يتم ضبطها تلقائيًا على foreground وتتدرج إلى perceptible وbackground ما لم يتم إلغاؤها من خلال بند لاحق. |
android:additionalMbPerDensity |
عدد صحيح (≥ 0) | 0 |
الميغابايت الإضافية مضروبة في مربّع نسبة كثافة العرض بالنسبة إلى mdpi (1.0x). |
android:additionalBytesPerDisplayPixel |
عدد صحيح (≥ 0) | 0 |
وحدات البايت الإضافية المخصّصة لكل بكسل في النافذة (WindowMetrics.getBounds العرض × الارتفاع)، وهي مفيدة لذاكرة التخزين المؤقت للأسطح والصور النقطية. |
android:feature |
سلسلة | أي | يفرض قيودًا على البند للأجهزة التي تعلن عن ميزات أجهزة معيّنة: watch أو automotive أو leanback. |
قياس مجموعة العمل ومعايرة الميزانيات
تكون الميزانية جيدة عندما تكون أعلى بقليل من مجموعة العمل في تطبيقك لحالة معيّنة، وليس عند الحد الأقصى غير المقيّد. للعثور على هذه النقطة، يمكنك تشغيل مسار مستخدم واقعي، وخفض الميزانية خطوة بخطوة، ومراقبة اللحظة التي يبدأ فيها تطبيقك في الدفع مقابل ذلك.
التعرّف على الفرق بين مجموعة العمل والذاكرة المقيمة
بدون ميزانية، تحتفظ مساحة الذاكرة الدائمة لتطبيقك (memory.current) بنوعَين من الذاكرة:
- مجموعة العمل: هي ذاكرة Java وKotlin والذاكرة الأصلية والصفحات النشطة المدعومة بالملفات (رمز DEX والمكتبات المجمّعة من رموز برمجية أصلية ومواد العرض) التي يتفاعل معها مسار المستخدم الحالي مرارًا وتكرارًا
- الذاكرة الاحتياطية غير النشطة: ذاكرة التخزين المؤقت للصور النقطية من الشاشات السابقة، ومخازن بدء التشغيل المؤقتة التي تُستخدم لمرة واحدة، والذاكرة الاحتياطية المتبقية بعد عملية تخصيص الذاكرة، وصفحات الملفات التي تمت قراءتها مرة واحدة ولم يتم استخدامها مرة أخرى
لا تستعيد الميزانية في ذروتك غير المقيّدة أبدًا الركود البارد. إذا كانت الميزانية أقل من مجموعة العمل، ستستبدل النواة صفحات الذاكرة المؤقتة وتزيل صفحات الرموز التي يعيد تطبيقك تحميلها على الفور. تستهلك أخطاء الصفحات المتكررة في مجموعة العمل وقت وحدة المعالجة المركزية والبطارية، وعندما تصل إلى سلسلة واجهة المستخدم، تستهلك الإطارات أيضًا.
ما تبحث عنه هو الحد الأدنى: أقل ميزانية تزيل فيها عملية الاسترداد الذاكرة غير المستخدَمة فقط، وبالتالي تظل الأخطاء المتكررة ووقت وحدة المعالجة المركزية وأوقات عرض اللقطات كما هي بدون ميزانية.
إعداد بيئة الاختبار
إعداد اختبار قابل للتكرار يعمل مثل الإنتاج:
- معايرة بنية الإصدار نفسها غير القابلة للتصحيح والمحسّنة باستخدام R8 التي يتم شحنها تستخدم الإصدارات المخصصة لتصحيح الأخطاء قدرًا أكبر من الذاكرة، لذا فإنّ الميزانية التي يتم ضبطها على إصدار مخصص لتصحيح الأخطاء تكون مرتفعة جدًا بالنسبة إلى الإصدار النهائي.
- اختبِر التطبيق على جهاز أو محاكي يعمل بالإصدار Android 17 QPR2 (مستوى واجهة برمجة التطبيقات 37.2) أو إصدار أحدث. من الأفضل استخدام إصدار
userdebugأو صورة محاكي بدون Google Play، لأنّ بعض الأجهزة تتيح لك قراءة ملفات cgroup في الأقسام التالية فقط بعد تشغيلadb root. - اكتب نصًا برمجيًا لمسار مستخدم مدته من 30 إلى 60 ثانية باستخدام Jetpack
Macrobenchmark أو UI Automator أو
adb shell input، مرة واحدة لكل حالة تخطّط لوضع ميزانية لها. لا يخبرك التطبيق الخامل بأي شيء عن مجموعة العمل الخاصة به.-
foreground: تتيح لك التنقّل في القوائم الكثيفة وشبكات الوسائط، والتنقّل بين وجهاتك الرئيسية، وفتح القوائم ومربّعات الحوار، وتشغيل الصور المتحركة perceptible: اترك النافذة المرئية أثناء استمرار العمل الملحوظ (على سبيل المثال، تشغيل الصوت أو التنزيل)، واتركها تعمل لمدة تتراوح بين 10 و15 ثانية، ثم ارجع إلى التطبيقbackground: تشغيل مزامنة أو مهمةWorkManager(على سبيل المثال، باستخدامadb shell cmd jobscheduler run -f <package-name> <job-id>) أو خدمة تم تشغيلها أو جهاز استقبال بث عندما لا يتضمّن التطبيق واجهة مستخدم مرئية
-
يجب قياس ميزانية background أثناء تشغيل العمل في الخلفية بشكل نشط،
بدلاً من قياسها أثناء خمول العملية في حالة التخزين المؤقت (IMPORTANCE_CACHED).
على الرغم من أنّ ميزانية 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 في حلقة أثناء قياس وقت عرض اللقطة. فهي تفرض تجميع المهملات في تطبيقك وتتتبّع جميع عمليات ربط الذاكرة، ما يؤدي إلى تغيير ما تحاول قياسه.
ميزانيات المرشحين في Sweep
يمكنك تجربة الميزانيات المرشحة بدون إعادة إنشاء تطبيقك. استخدِم
am memory-limiter manual لضبط ميزانية
لعملية قيد التشغيل، وnone لإزالتها:
adb shell am memory-limiter manual <PID> 120MBadb shell am memory-limiter manual <PID> none
يستمر التجاوز اليدوي طوال عمر مثيل العملية هذا، عبر
تغييرات حالة العملية، لذا امسحها أو أعد تشغيل العملية قبل المتابعة.
ويضبط أيضًا الحد الأقصى للعملية (memory.max) على القيمة نفسها، لذا فإنّ
الميزانية المرشحة التي تقل عن مجموعة العمل في تطبيقك يمكن أن تؤدي إلى إنهاء العملية
بدلاً من إبطائها فقط. اعتبر ذلك ميزانية فاشلة.
يمكنك أيضًا إجراء عملية مسح من إصدار معياري من خلال قراءة قيمة مرشحة من وسيطة قياس وضبط MemoryBudgetManager.processBudgetBytes. يمكن لميزانية وقت التشغيل
أن تضيّق ميزانية البيان فقط، لذا يمكنك إجراء عملية مسح على إصدار بدون
إعلانات <memory-budget>. ويظل ساريًا أيضًا في جميع تغييرات حالة العملية إلى أن يتم استدعاء clearProcessBudget.
ابدأ بأعلى memory.current من خط الأساس وانخفِض بمقدار 10 إلى 15 ميغابايت في المرة الواحدة. لكل ميزانية مرشحة:
- ابدأ عملية جديدة، ووجِّهها إلى الحالة المستهدَفة، وطبِّق الميزانية.
- شغِّل مسار المستخدم مرة واحدة كعملية إحماء. يؤدي تطبيق ميزانية إلى إزالة صفحات بدء التشغيل على البارد مرة واحدة، وتريد قياس الحالة الثابتة بدلاً من هذا الانتقال.
إعادة ضبط إحصاءات الإطارات وتسجيل عدّادات الأخطاء الأولية ووقت وحدة المعالجة المركزية للعملية (
utimeوstime، بوحدة قياس نبضات الساعة):adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"شغِّل المسار الذي تم قياسه لمسار المستخدم.
سجِّل
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"، وتزداد أوقات عرض اللقطات وأوقات الاستئناف السريع.
مقارنةً بالمرجع غير المقيّد، يتم قبول الميزانية المرشحة إذا كانت تستوفي جميع الشروط التالية، والتي تُعد نقطة بداية معقولة:
- تستمر العملية بعد انتهاء التشغيل
- تبقى الاختلافات في الأخطاء المتكررة قريبة من خط الأساس
- ارتفاع وقت وحدة المعالجة المركزية للعملية بنسبة لا تزيد عن %5
- ارتفاع اللقطات البطيئة بنسبة لا تزيد عن نقطة مئوية واحدة
- ارتفاع وقت عرض اللقطة في المئين 95 بما لا يزيد عن 3 ملي ثانية
- يزيد وقت الاستئناف الدافئ (
TotalTimeمنadb shell am start -W) بنسبة لا تزيد عن %15، لميزانياتperceptibleوbackground
"نقطة الانحناء" هي أدنى ميزانية مرشّحة يتم قبولها. على سبيل المثال، إليك فحص في المقدّمة لتطبيق افتراضي يبلغ الحد الأقصى غير المقيّد له 138 ميغابايت:
| الميزانية المقترَحة | memory.current |
anon |
file |
الأخطاء المتكررة (الصفحات) | وقت عرض اللقطة عند نسبة 95% | النظام |
|---|---|---|---|---|---|---|
| بدون | 138 ميغابايت | 75 ميغابايت | 63 ميغابايت | 0 | 10.5 ملي ثانية | الخط القاعدي |
| 125 ميغابايت | 121 ميغابايت | 74 ميغابايت | 47 ميغابايت | 10 | 10.5 ملي ثانية | فوق الركبة |
| 110 ميغابايت | 110 ميغابايت | 75 ميغابايت | 35 ميغابايت | 15 | 10.8 مللي ثانية | الركبة |
| 95 ميغابايت | 95 ميغابايت | 74 ميغابايت | 21 ميغابايت | 176 | 11.2 مللي ثانية | تحت الركبة |
| 85 ميغابايت | 85 ميغابايت | 68 ميغابايت | 17 ميغابايت | 2,050 | 12.8 ملي ثانية | تحت الركبة |
| 70 ميغابايت | 70 ميغابايت | 54 ميغابايت | 16 ميغابايت | 8,771 | 16.5 ملي ثانية | تحت الركبة بكثير |
اختيار ميزانيتك
اضبط الحدّ الأدنى android:maxMb على نسبة تتراوح بين %10 و%20 أعلى من نقطة الانعطاف، لكي لا تؤدي الاختلافات في بيانات المستخدمين والمناطق والزيادات المفاجئة في تخصيص الموارد إلى خفض أداء تطبيقك إلى ما دون هذا الحدّ. في المثال السابق، يقع المنعطف عند 110 ميغابايت، لذا فإنّ 125 ميغابايت هو خيار جيد.
بعد ذلك، ثبِّت بنية إصدار تتضمّن بيانات <memory-budget> الجديدة، وشغِّل
مسار المستخدم مرة أخرى، وتأكَّد من أنّ معايير النجاح لا تزال سارية.
التحقّق من صحة الميزانيات غير المخصّصة للتشغيل في المقدّمة من خلال العودة إلى التطبيق بعد فترة قصيرة
قد تبدو الميزانية المحدودة لـ perceptible أو background مناسبة عندما يكون تطبيقك في هذه الحالة، ولكنّها قد تكلّفك عندما يعود المستخدم إلى التطبيق. اختبر رحلة العودة
أيضًا:
- شغِّل مسار المقدّمة، وانقِل التطبيق إلى الحالة المستهدَفة، وطبِّق
الميزانية المرشَّحة باستخدام
am memory-limiter manual. - اترك العمل الملحوظ أو في الخلفية يعمل لمدة تتراوح بين 10 و15 ثانية.
- يجب محو الإلغاء (
adb shell am memory-limiter manual <PID> none)، ثم إعادة التطبيق فورًا باستخدامadb shell am start -WوتسجيلTotalTime. إذا ضبطت المرشّح من خلالMemoryBudgetManagerبدلاً من ذلك، يمكنك استدعاءclearProcessBudgetقبل أن يعود التطبيق. - يمكنك إجراء تفاعل قصير في المقدّمة والتحقّق من الأخطاء المتكررة وإحصاءات الإطارات وفقًا لمعايير النجاح.
اختَر أقل ميزانية تنجح في الاختبارَين أثناء تشغيل التطبيق في هذه الحالة وعند العودة إلى التطبيق، وأضِف نسبة تتراوح بين %10 و%20 إلى الحد الأقصى.
معايرة حجم الشاشة
إذا كنت تستخدم android:additionalBytesPerDisplayPixel، يمكنك العثور على نقطة الانحناء في المقدّمة على
شاشتَين بدرجات دقة مختلفة، مثل 1080 × 2400
(≈ 2.59 مليون بكسل) و1440 × 3120 (≈ 4.49 مليون بكسل).
قسِّم الفرق بين نقطتَي الانحناء بالبايت على الفرق في عدد وحدات البكسل للحصول على android:additionalBytesPerDisplayPixel. اطرح هذا المقدار لكل بكسل للشاشة الأصغر من نقطة الانحناء للحصول على الأساس المستقل عن الدقة، وأضِف المساحة الإضافية للحصول على android:maxMb.
واجهات برمجة التطبيقات لوقت التشغيل (الخيار الديناميكي الثانوي)
يُعدّ تحديد الميزانيات بشكل ثابت في AndroidManifest.xml الحلّ المفضّل لجميع التطبيقات تقريبًا. ومع ذلك، بالنسبة إلى التطبيقات التي تتضمّن أعباء عمل ديناميكية أو للتجربة في وقت التشغيل، يوفّر Android واجهة برمجة تطبيقات لحزمة تطوير البرامج (SDK) في وقت التشغيل كخيار ثانوي.
تتيح لك واجهة برمجة التطبيقات لوقت التشغيل ما يلي:
- الاستعلام عن استخدام الذاكرة الحالي والميزانيات الفعّالة
- عدِّل ميزانية العملية ديناميكيًا نحو الأسفل.
- Listen for over-budget events to proactively trim caches before the operating system triggers direct reclaim.
واجهة برمجة التطبيقات لحزمة تطوير البرامج (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: يمكنك تمرير خلفيةHandlerThreadLooperإلىregisterProcessOverBudgetListenerأوregisterPackageOverBudgetListenerكلما أمكن ذلك. يؤدي تشغيل إخلاء ذاكرة التخزين المؤقت أو تنظيف الملفات في سلسلة مهام في الخلفية إلى تجنب التنافس مع عرض الإطارات في سلسلة واجهة المستخدم أثناء تعرض العملية لضغط الذاكرة. يجب حجزLooper.getMainLooperللمستمعين الذين يتفاعلون فقط مع حالة واجهة المستخدم المرتبطة بسلسلة التعليمات الرئيسية. - توقُّع عمليات استدعاء متكررة: لا تقتصر عمليات الاستدعاء عند تجاوز الميزانية على إشعارات لمرة واحدة. عندما يتجاوز الاستخدام الميزانية، يستعيد نظام التشغيل
الصفحات غير النشطة أولاً، وإذا استمر الضغط، يستمر النظام في استعادة
الذاكرة واستدعاء المستمع مرة واحدة على الأكثر كل ثانية. يجب أن يكون ردّ الاتصال
متكررًا. إذا كان تطبيقك يحتاج إلى إشعار واحد فقط أو
يريد فترة تهدئة أطول بين عمليات التنظيف، يمكنك استدعاء
unregisterProcessOverBudgetListener(أوunregisterPackageOverBudgetListener) داخل دالة الاستدعاء، أو حماية عملية التنظيف باستخدام طابع زمني لفترة التهدئة أوAtomicBoolean. - التسجيل قبل تقليل الميزانية: إذا كان استخدام الذاكرة الحالي يتجاوز ميزانية جديدة عند استدعاء
setProcessBudgetBytesأوsetPackageBudgetBytes، يتم تشغيل حدث تجاوز الميزانية فور تطبيق الميزانية الجديدة. يجب تسجيل المستمع قبل تحديد الميزانية حتى لا يفوت تطبيقك الإشعار الأولي بتجاوز الميزانية. - السرعة: يجب أن توفّر عمليات الاسترداد راحة فورية. وتؤدي العمليات الحسابية المعقّدة أثناء الضغط إلى تراجع الأداء.
- تجنُّب عمليات التخصيص: لا تخصِّص عناصر جديدة أو تبدأ سلاسل محادثات جديدة داخل دالة الاستدعاء، لأنّ ذلك قد يؤدي إلى استعادة نظام التشغيل المباشرة على الفور.
- التركيز على الأهداف ذات العائد المرتفع: إنّ إخلاء مساحة من الذاكرة من خلال إزالة ملفات Bitmap الكبيرة أو مخازن العرض المؤقتة أو إغلاق الملفات المعيّنة في الذاكرة أكثر فعالية من إخلاء مساحة من الذاكرة من خلال إزالة العديد من العناصر الصغيرة.
مثال على واجهة برمجة التطبيقات في وقت التشغيل
يوضّح المثال التالي كيفية تنفيذ واجهة برمجة التطبيقات لوقت التشغيل باستخدام واجهة برمجة التطبيقات لحزمة تطوير البرامج (SDK) لنظام التشغيل Android في لغة Kotlin:
مثال على حزمة تطوير البرامج (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"
}
}