إنّ الرمز الذي تكتبه هو نفسه شكل من أشكال استخدام الذاكرة. يجب تحميل كل فئة وطريقة وثابت سلسلة في تطبيقك إلى ذاكرة الوصول العشوائي عند تنفيذها. كلما زاد حجم قاعدة الرموز البرمجية لتطبيقك، زاد مقدار الذاكرة التي يستهلكها.
الذاكرة المدعومة بالملفات وتقسيم الذاكرة إلى صفحات عند الطلب
يحمّل نظام التشغيل Android الرمز البرمجي التنفيذي من .apk (مثل ملفات .oat أو .so) باستخدام mmap. وهذا يعني أنّ الرمز يستند إلى ملف.
والأهم من ذلك أنّ نظام التشغيل Android يستخدم التقسيم عند الطلب. عند بدء تشغيل تطبيقك، لا تحمّل النواة حزمة APK بأكملها في ذاكرة الوصول العشوائي (RAM) على الفور. بدلاً من ذلك، يتم ربط الملف بمساحة العنوان الافتراضي للعملية فقط. عندما ينفّذ تطبيقك التعليمات، وينتقل وحدة المعالجة المركزية إلى دالة جديدة، يتم تشغيل "خطأ في الصفحة". توقف النواة مؤقتًا سلسلة التعليمات، وتقرأ صفحة الرمز البرمجي المحدّدة بحجم 4 كيلوبايت من وحدة التخزين إلى ذاكرة الوصول العشوائي (RAM) الفعلية، ثم تستأنف التنفيذ.

وهذا يعني أنّ الرمز الذي تجمّعه ولكن لا تنفّذه لا يستخدم الذاكرة الفعلية لصفحات الرمز نفسها. ومع ذلك، تزيد المكتبات غير المستخدَمة من حجم حزمة APK الإجمالي، ويمكن أن تزيد بشكل كبير من الذاكرة التي تستخدمها البيانات الوصفية الداخلية للنظام (مثل فهارس DEX وأوصاف الفئات)، والتي يجب قراءتها حتى معرفة أنّ الرمز البرمجي متوفّر. بالإضافة إلى ذلك، تحتوي العديد من المكتبات على أدوات تهيئة ثابتة أو تتأثر بأُطر عمل إدخال التبعية أثناء بدء تشغيل التطبيق، ما يؤدي إلى تقسيمها إلى صفحات في ذاكرة الوصول العشوائي على أي حال.
إخلاء الصفحات والتباطؤ
وبما أنّه يمكن دائمًا إعادة قراءة الذاكرة المستندة إلى الملف من مساحة التخزين، تعتبر النواة هذه الصفحات "نظيفة". عندما يواجه النظام ضغطًا على الذاكرة، ستقوم النواة بإخراج (إسقاط) صفحات الرمز النظيف هذه من ذاكرة الوصول العشوائي لإفساح المجال لأشياء أخرى.
إذا احتاج تطبيقك إلى تنفيذ هذا الرمز مرة أخرى لاحقًا، سيحدث خطأ في وحدة المعالجة المركزية، وسيكون على النواة إعادة قراءة الصفحة من وحدة التخزين. كلما زاد حجم الرمز البرمجي لتطبيقك، زادت احتمالية إزالة الرمز البرمجي. عندما يعود المستخدم إلى تطبيقك المتضخّم بعد استخدام تطبيقات أخرى، سيواجه تشويشًا عشوائيًا وبطئًا لأنّ وحدة المعالجة المركزية تتوقف باستمرار في انتظار إعادة تحميل الرمز من مساحة التخزين.
تكلفة خطأ الصفحة: على الرغم من أنّها تختلف بشكل كبير حسب سرعة التخزين في الجهاز (UFS مقابل eMMC) وحالة النواة، يمكن أن تتراوح تكلفة خطأ الصفحة الرئيسي (قراءة 4 كيلوبايت من وحدة التخزين) بين 0.5 مللي ثانية و5 مللي ثانية. إذا كان مسار بدء التشغيل يتضمّن 500 صفحة مختلفة من الرموز غير المحسَّنة، يمكنك بسهولة إضافة عدة مئات من الملّي ثانية من وقت الاستجابة الخالص للإدخال/الإخراج إلى وقت بدء تشغيل تطبيقك.
استكشاف حجم الرمز البرمجي باستخدام Compiler Explorer
لإنشاء فكرة عن كيفية ترجمة رموز Java أو Kotlin إلى رموز برمجية أصلية (وبالتالي وحدات بايت للذاكرة)، يمكنك استخدام Compiler Explorer.
يتوفّر دعم Android مباشرةً في Godbolt. تتيح لك هذه الأداة معرفة كيفية تحويل الأجزاء المختلفة من سلسلة أدوات Android (مثل D8 وR8 وdex2oat) لرمزك المصدر.
كيفية استخدام Compiler Explorer مع Android
- انتقِل إلى godbolt.org.
- اختَر Android Java أو Android Kotlin من القائمة المنسدلة للغة (في أعلى يمين الصفحة).
- في القائمة المنسدلة الخاصة بالمترجم البرمجي (أعلى يسار جزء الرمز)، يمكنك الاختيار
بين أدوات مختلفة:
d8: يعرض رمز Dalvik البايت (.dex)، وهو أقرب تمثيل للرمز الأصلي وأسهل في القراءة.r8: يعرض هذا القسم كيفية تصغير مُحسِّن R8 لرمز البايت وتحسينه.dex2oat: يعرض رمز الجهاز ARM64 النهائي الذي يتم تنفيذه فعليًا على الجهاز. هذا هو المكان الذي يمكنك فيه الاطّلاع على تأثير الذاكرة الفعلي (4 بايت لكل تعليمات). يمكن أن يستهدفdex2oatمجموعات تعليمات مختلفة، ولكن ARM64 هي الأكثر شيوعًا للهواتف الجوّالة.
- تحديد الرمز المصدر<>الناتج: عند تمرير مؤشر الماوس فوق سطر من الرمز البرمجي، سيتم تحديد تعليمات الرمز الثانوي أو رمز الآلة المقابلة، ما يسهّل تتبُّع تأثير عبارات معيّنة.
- مسار التحسين: في عرض التفكيك، يمكنك النقر على إضافة جديد... انقر على -> Opt Pipeline. يتيح لك ذلك الاطّلاع على الخطوات الداخلية التي يتّخذها المترجم. يمكنك فحص طريقة تحويل التمثيل الداخلي (IR) في كل مرحلة (على سبيل المثال، بين الخطوتَين "Inliner (قبل)" و "Inliner (بعد)") قبل تحويله إلى رمز الآلة ARM64 النهائي.

أهمية ذلك بالنسبة إلى الذاكرة
كل تعليمات تراها في ناتج dex2oat تستهدف مجموعة تعليمات ARM64 تستهلك 4 بايت في ملف التنفيذ الخاص بتطبيقك (.odex أو .oat).
جرِّب إدخال رمز يستفيد من ميزات لغوية مختلفة وادرس ناتج المترجم:
- الوصول إلى المصفوفات مقابل أدوات تكرار القوائم:
- قد يتم تجميع حلقة مصفوفة بسيطة على
int[]في حوالي 10 تعليمات (حوالي 40 بايت). - تستخدِم حلقة foreach على
ListضمنيًاIterator. يمكن أن يؤدي ذلك إلى 30 إلى 40 تعليمات (حوالي 160 بايت) بسبب عمليات استدعاء الطريقة الإضافية (hasNext()وnext()) وتخصيص عنصر المكرّر نفسه. - تحسين R8: في الظروف المناسبة (مثلاً، عندما يثبت أنّ
ListهوArrayList)، يمكن لمحسِّن R8 تحويل حلقة foreach إلى حلقة بسيطة مفهرسة، ما يؤدي إلى إلغاء الحمل الزائد للمكرّر وتقليل حجم الرمز البرمجي والاستخدام الزائد للذاكرة أثناء وقت التشغيل.
- قد يتم تجميع حلقة مصفوفة بسيطة على
- استدعاءات الطُرق الافتراضية: تتضمّن تحميل فئة العنصر، والعثور على الطريقة في
vtable، ثم إنشاء فروع. ويستغرق ذلك عادةً 4 أو 5 تعليمات (حوالى 20 بايت). - الاستدعاءات المباشرة/الثابتة: غالبًا ما تتم ترجمتها إلى تعليمات
bl(فرع مع رابط) واحدة (4 بايت). - تعبيرات Lambda في Kotlin: يمكنها إنشاء فئات مجهولة الهوية بالكامل وطرق ربط إضافية، ما يؤدي إلى إضافة مئات البايتات من الرموز والبيانات الوصفية إلى مساحة التخزين المخصّصة لمجموعة رموز بسيطة.
باستخدام Compiler Explorer، يمكنك معرفة تأثير ميزات اللغة المعقّدة (مثل تعبيرات lambda في Kotlin أو واجهات برمجة التطبيقات الخاصة بالتدفق أو الاستخدام المكثّف للأنواع العامة) في الحجم النهائي المجمَّع لتطبيقك، وكيف يمكن لأدوات التحسين، مثل R8، أن تقلّل من تكلفة تجريدات اللغة في بعض الحالات. يمكن أن تساعدك هذه الأداة في اتّخاذ قرارات مدروسة بشأن المفاضلة بين الميزات عند تصميم تطبيق وتنفيذه.
بشكل عام، تؤدي زيادة التعقيد في رمز تطبيقك إلى زيادة استخدام الذاكرة. في المقابل، يؤدي الرمز البرمجي الأبسط، أو الرمز البرمجي الذي تبسّطه أداة R8، إلى تمثيل أصغر كتعليمات لوحدة المعالجة المركزية وبايتات في مساحة التخزين وذاكرة الوصول العشوائي.
قياس تأثير الرمز باستخدام meminfo وshowmap
يمكنك استخدام أدوات ذاكرة Android العادية لمعرفة مقدار الذاكرة التي يستهلكها رمز تطبيقك.
dumpsys meminfo
عند تشغيل adb shell dumpsys meminfo <package>، يقدّم لك فئة الرمز في قسم ملخّص التطبيق نظرة عامة على الذاكرة ذات الصلة بالرمز:
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
للحصول على عرض أكثر تفصيلاً، استخدِم showmap. ويكشف عن المناطق التي لا يتم فيها ربط ملفات معيّنة بالذاكرة.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
ستظهر لك إدخالات للرمز البرمجي المجمَّع لتطبيقك:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
الرموز البرمجية غير المستخدَمة وR8
بما أنّ كل طريقة يتم تنفيذها تستهلك الذاكرة، فإنّ التطبيق "المضخّم" الذي يتضمّن عمليات تهيئة غير ضرورية أو مكتبات غير مستخدَمة يمكن أن يؤثّر بشكل كبير في أداء بدء التشغيل واستخدام الذاكرة الأساسي.
لهذا السبب، تُعدّ أدوات مثل R8 (ProGuard) ضرورية. يحلّل R8 الرمز الثانوي لتطبيقك ويزيل أي فئات أو طرق لا يتم استدعاؤها مطلقًا (يُعرف ذلك باسم "إزالة الرموز غير النشطة").
تمرين عملي: تكلفة التضخّم
لتوضيح تأثير حجم الرمز البرمجي، ضع في اعتبارك تجربة تقارن بين إصدارَين من تطبيق يحتوي على 300 فئة تم إنشاؤها (كل منها يحتوي على 500 طريقة):
- CodeBloat (Unoptimized): الإصدار العادي غير المحسَّن الذي يحتوي على جميع الفئات والسلاسل الفريدة التي تم إنشاؤها.
- CodeBloatOptimized: الرمز المصدر نفسه، ولكن تم تجميعه مع تفعيل تقليص R8.
1. الترجمة المسبقة (AOT)
لتحقيق أقصى استفادة من الذاكرة المستندة إلى الملفات، سنستخدم الأداة cmd package compile
لترجمة التطبيقات مسبقًا (AOT) إلى ملفات .oat.
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
يُرجى العِلم أنّ هذا مثال اصطناعي. عادةً، تستخدم التطبيقات
speed-profile وضع التجميع (راجِع الفقرة أدناه).
2. إطلاق الإعلانات ومقارنتها
للاطّلاع على عملية بدء باردة حقًا حيث يجب أن يقرأ النظام الرمز من وحدة التخزين، سنحذف ذاكرة التخزين المؤقت للصفحات في النواة قبل تشغيل كل تطبيق. ويتطلّب ذلك إذن الوصول إلى الجذر.
افتح التطبيق غير المحسَّن:
adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat
والآن، نفِّذ الإجراء نفسه مع التطبيق المحسَّن:
adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
النتائج
إذا نظرت إلى صف الرمز في القسم App Summary، ستلاحظ فرقًا كبيرًا:
- غير محسّن
Code: حوالي 30,000 كيلوبايت (30 ميغابايت) - محسَّن
Code: حوالي 2,000 كيلوبايت (2 ميغابايت)
ولأنّ أداة R8 رصدت أنّ 500 طريقة داخل تلك الفئات لم تكن تؤدي أي وظيفة مفيدة (فالطريقة doSomething() لا تستدعي سوى method0()، ويتم تجاهل النتائج)، فقد أزالت معظم الرموز البرمجية التي تم إنشاؤها بشكل مصطنع من حزمة APK النهائية.
3- الاطّلاع على التأثير في Perfetto
يمكن ملاحظة تأثير تضخّم الرمز البرمجي بوضوح أثناء مرحلة التحميل الأوّلية للتطبيق. على وجه التحديد، ابحث عن شريحة bindApplication في سلسلة التعليمات الرئيسية، والشرائح المتداخلة التي تبدأ بـ madvising، والتي تشير إلى أنّ النظام يستعد لتحميل الملفات من حزمة APK والرمز البرمجي المجمَّع (.odex).
في عملية التشغيل على البارد التفاعلية، سيحمّل النظام الرمز mmap() والرمز madvise() والبيانات الأخرى من هذه الملفات اللازمة لتحميل التطبيق وتشغيله. تشير القيمة بعد "size=" في شرائح madvising إلى مقدار البيانات التي يجب تحميلها. يتم إجراء عملية الجلب المسبق لرمز التطبيق بهدف تسريع عملية بدء تشغيل التطبيق.
من خلال المقارنة، يمكننا أن نرى أنّ مقدار رمز التطبيق الذي كان يجب تحميله من مساحة التخزين إلى ذاكرة الوصول العشوائي (RAM) كان أكبر بكثير في حالة التطبيق المتضخّم، ما أدّى إلى فترات أطول ساهمت في بطء بدء تشغيل التطبيق. بالإضافة إلى ذلك، يعرض تتبُّع بدء تشغيل التطبيق المتضخّم شرائح لتحميل ملفات DEX الثانوية (classes2.dex وclasses3.dex) التي اضطر التطبيق المتضخّم إلى "توزيعها" لأنّها لم تتسع في ملف DEX واحد.
مقارنةً بذلك (التشغيل على البارد على هاتف Pixel 10a)
| المقياس | غير محسَّن (CodeBloat) | محسَّن (CodeBloatOptimized) |
|---|---|---|
base.odex madvise size |
7.9 ميغابايت تقريبًا (2.0 ملي ثانية) | 16 كيلوبايت تقريبًا (0.003 مللي ثانية) |
base.apk madvise size |
2.4 ميغابايت تقريبًا (2.4 ملي ثانية) | 4 كيلوبايت تقريبًا (0.001 ملي ثانية) |
classes2.dex madvise size |
~7.3 ميغابايت (8.6 ملي ثانية) | لا ينطبق |
classes3.dex madvise size |
7.3 ميغابايت تقريبًا (8.0 ملي ثانية) | لا ينطبق |
إجمالي مدة madvising |
حوالى 21 ملي ثانية | ~0.004 ملي ثانية |
أداء تحميل التطبيقات غير المحسَّن

تحسين أداء تحميل التطبيقات

يختلف تأثير تضخّم الرموز البرمجية حسب حجم التطبيق وخصائص جهاز المستخدم وحِمل النظام.
PerfettoSQL لتحليل التحميل
يمكنك استخدام طلبات البحث التالية لاستخراج هذه المقاييس من عمليات التتبُّع.
1. مدة بدء تشغيل التطبيق
تعرِض هذه السمة الوقت المُستغرَق منذ بدء تشغيل نشاط التطبيق إلى أن يرسم النشاط إطارًا أولاً.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
راجِع: التعرّف على حالات بدء تشغيل التطبيق المختلفة
تتأثر مدة بدء تشغيل التطبيق بالعديد من العوامل الأخرى غير تلك الموضّحة في هذا الدليل.
2. استخراج أحجام ومدة madvising
يركّز طلب البحث هذا على الجزء madvising الذي رأيناه أعلاه.
INCLUDE PERFETTO MODULE slices.with_context;
SELECT
name,
dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
AND name LIKE 'madvising %';
3- تفاصيل حالة سلسلة التعليمات الرئيسية (المدة الإجمالية لكل حالة)
يعرض هذا الطلب مقدار الوقت الذي استغرقه مؤشر الترابط الرئيسي للتطبيق في حالات مختلفة.
SELECT
p.name AS process_name,
state,
sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
GROUP BY p.name, state;
يمكنك تحسين طلب البحث للاطّلاع فقط على حالات سلسلة المحادثات الرئيسية خلال مدة بدء تشغيل التطبيق.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT
p.name AS process_name,
ts.state,
-- Calculate only the duration that falls within the startup window
SUM(
MAX(0,
MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
)
) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
AND t.is_main_thread = 1
-- Only select thread states that overlap with the startup interval
AND ts.ts + ts.dur > s.ts
AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;
يمكن أن يؤدي ذلك إلى ظهور بعض المشاكل المثيرة للاهتمام، مثل:
- وقت طويل في حالة قابلية التشغيل (R) ولكن ليس في حالة التشغيل: يشير ذلك إلى أنّ بدء تشغيل التطبيق تأخّر بسبب التنازع على وحدة المعالجة المركزية، أي أنّ السلسلة الرئيسية للتطبيق لم تتمكّن من التشغيل لأنّ سلاسل أخرى (ربما من تطبيقات أخرى) كانت تستخدم وحدات المعالجة المركزية.
- الوقت الطويل الذي يتم قضاؤه في وضع "السكون القابل للمقاطعة" (D): يشير ذلك عادةً إلى بطء عمليات الإدخال/الإخراج أو زيادة الطلب على الذاكرة، ما يؤدي إلى توقّف بدء تشغيل التطبيق.
- الوقت الطويل الذي تم قضاؤه في وضع السكون (S): يعني ذلك أنّ سلسلة التعليمات الرئيسية كانت تنتظر سلاسل تعليمات أخرى لإكمال عملها. يشير ذلك أحيانًا إلى حدوث تنازع على دالة استبعاد متبادل في مسار بدء تشغيل التطبيق (أي تم حظر سلسلة التعليمات الرئيسية بسبب مورد حصري كانت تستخدمه سلسلة تعليمات أخرى في التطبيق).
4. الحد الأقصى للذاكرة المدعومة بملف (ملف RSS)
يرتبط هذا المقياس بشكل كبير بمقدار الرمز البرمجي والبيانات التي يحمّلها التطبيق عند بدء التشغيل. سيصل التطبيق "المضخّم" إلى رقم أعلى هنا، ما يؤدي إلى زيادة الضغط على الذاكرة في النظام. وقد يؤدي هذا الضغط بدوره إلى تأخير بدء تشغيل التطبيق، لأنّ النظام يواجه صعوبة في تلبية طلبات التخصيص، أو يحوّل وقت وحدة المعالجة المركزية من التركيز على بدء تشغيل التطبيق إلى استرداد الذاكرة من العمليات الأخرى لتلبية الاحتياجات الفورية للتطبيق الذي يتم تشغيله.
SELECT
p.name AS process_name,
max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
AND t.name = 'mem.rss.file'
GROUP BY p.name;
أوضاع تجميع ART والذاكرة
يمكن لوقت تشغيل Android (ART) تجميع الرمز البرمجي لتطبيقك في أحد الأوضاع المختلفة، والمعروفة أيضًا باسم فلاتر المجمّع. يؤثّر فلتر برنامج التجميع الذي تم اختياره بشكل مباشر على استهلاك الذاكرة لتطبيقك.
-
verify: لا ينفّذ ART سوى عملية التحقّق من رمز البايت. لا يتم إجراء تجميع AOT. يتم تنفيذ الرمز البرمجي من خلال المترجم أو تجميعه أثناء التشغيل بواسطة برنامج التجميع JIT.- تأثير الذاكرة: أصغر حجم على القرص يتم نقل استخدام الذاكرة للرمز البرمجي الأصلي إلى
JIT Cache(الذاكرة المتسخة المجهولة).
- تأثير الذاكرة: أصغر حجم على القرص يتم نقل استخدام الذاكرة للرمز البرمجي الأصلي إلى
-
speed: ينفّذ ART عملية تجميع كاملة مسبقة (AOT) لجميع الطرق.- تأثير الذاكرة: أكبر حجم
.odexتزيد من استخدام الذاكرة المستندة إلى الملفات (النظيفة).
- تأثير الذاكرة: أكبر حجم
speed-profile: لا يجمع ART سوى الطرق التي تم وضع علامة "نشط" عليها في ملف JIT.- تأثير الذاكرة: نهج متوازن يتم تجميع الرمز الأكثر أهمية فقط باستخدام AOT.
الفلتر الأكثر شيوعًا هو speed-profile، ويتم استخدامه عند تثبيت تطبيقات المستخدم. يتم ضبط ذلك في سمات النظام pm.dexopt.install وpm.dexopt.bg-dexopt، ويتم عادةً ضبطه في build/make/target/product/runtime_libart.mk.
ستستخدم بعض تطبيقات النظام عملية تجميع speed، وسيتم تجميعها أيضًا في مدّة تصميم صورة النظام. يتم استخدام verify عادةً في حالات استخدام التطوير فقط.
| حالة الاستخدام | فلتر المترجم البرمجي النموذجي |
|---|---|
| تطوير | verify |
| صورة النظام | speed |
| تطبيقات المستخدم | speed-profile |
تمرين عملي: أوضاع التجميع والذاكرة
يمكننا استخدام تطبيق CodeBloat لمعرفة كيفية تأثير هذه الفلاتر في الذاكرة. لإعادة إنتاج هذه القياسات، اتّبِع الخطوات التالية:
- فرض إعادة تجميع التطبيق في الوضع المستهدف
- فرض إيقاف التطبيق وإعادة تشغيله
- انتظِر إلى أن ينتهي مؤشر الترابط في الخلفية من تعديل الفئات (يمكنك الاطّلاع على logcat أو الانتظار لمدة 5 ثوانٍ).
- تشغيل
adb shell dumpsys meminfo com.android.codebloat
الوضع: verify (بدون AOT)
adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
في وضع verify، يعرض "ملخّص التطبيق" ما يلي: * حجم مجموعة بيانات PSS للرمز: 8,000 كيلوبايت تقريبًا * Dalvik
Other (JIT): 25,000 كيلوبايت تقريبًا
بما أنّه لا يتم تجميع أي رمز برمجي مسبقًا، يجب أن يجمّع وقت التشغيل الطرق النشطة في ذاكرة التخزين المؤقت للتجميع في الوقت المناسب، والتي تظهر على أنّها ذاكرة مجهولة غير نظيفة (Dalvik
Other).
الوضع: speed (تجميع مسبق كامل)
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat
في وضع speed، تتغيّر النتائج بشكل كبير: * مجموعة صفحات المشاركة الخاصة بالرمز: 24,000 كيلوبايت تقريبًا *
Dalvik Other (JIT): 5,000 كيلوبايت تقريبًا
يتم الآن ربط رمز التطبيق من الملف .odex باعتباره ذاكرة نظيفة
مدعومة بملف. يقلّل ذلك من الضغط على ذاكرة التخزين المؤقت JIT ويجعل الذاكرة مؤهَّلة للإخلاء عند الضغط، بدلاً من أن تكون "عالقة" كذاكرة وصول عشوائي (RAM) غير نظيفة.
الوضع: speed-profile (الترجمة التلقائية الانتقائية)
قد تتضمّن التطبيقات الحديثة baseline.prof ملفًا شخصيًا أساسيًا. يستخدم ART ذلك
لتجميع الرمز المطلوب فقط بشكل انتقائي من أجل بدء التشغيل بسرعة وبكفاءة في استخدام الذاكرة.
في هذا التمرين، سننشئ ملفًا أساسيًا لإدراج فئات بدء تشغيل التطبيق. ومع ذلك، قد يتلقّى المترجم في الواقع أيضًا ملفات شخصية من مصادر خارجية، مثل متجر التطبيقات ("الملفات الشخصية المستندة إلى السحابة الإلكترونية")، والتي يمكن أن توفّر ملفات شخصية مجمّعة من مصادر متعددة للتشغيل في الوقت المناسب للتطبيقات بغض النظر عمّا إذا كان المطوّر قد ضمّن أيضًا ملفًا شخصيًا أساسيًا أنشأه.
إنشاء ملفات شخصية على الجهاز فقط واستخدامها
للاطّلاع على تأثير speed-profile، يمكنك إنشاء ملفك الشخصي على الجهاز فقط:
إعادة الضبط والبدء:
adb shell am force-stop com.android.codebloatالتفاعل: ابدأ تشغيل التطبيق واتركه ينفّذ تسلسل بدء التشغيل.
تفريغ الملف الشخصي:
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)(يفرض ذلك على التطبيق كتابة ملفه الشخصي الحالي على القرص).
ملف التثبيت:
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profتجميع:
adb shell cmd package compile -m speed-profile -f com.android.codebloat
عند إعادة التشغيل، سيظهر لك رصيد: سيكون Code PSS أقل من
speed (على سبيل المثال، 16,000 كيلوبايت تقريبًا) لأنّه تم تجميع طرق بدء التشغيل "السريعة" فقط،
مع ترك الباقي ليتم التعامل معه بواسطة المترجم أو JIT فقط إذا تم استخدامه
فعليًا.
يُرجى الاطّلاع على:
- نظرة عامة على "ملفات Baseline Profile"
- الفرق بين "الملفات الشخصية للمرجع" و"ملفات تعريف بدء التشغيل"
نظرة متعمّقة على الرمز البرمجي المجمَّع
إذا أردت الاطّلاع على التعليمات التي ينشئها "مساعد Android" بالضبط، يُرجى الرجوع إلى
art/DISASSEMBLY_GUIDE.md.
تقدّم هذه الأداة تعليمات مفصّلة حول استخدام:
oatdump: للاطّلاع على تعليمات ARM64 داخل ملف.odexحاليdex2oat: لمحاكاة التجميع باستخدام علامات تصحيح الأخطاء التفصيلية.
التمرين: تضمين الرمز البرمجي
أحد أسباب زيادة حجم الرمز البرمجي المجمَّع بشكل غير متوقّع هو تضمين الرمز البرمجي للدالة. قد يقرّر المترجم البرمجي نسخ نص دالة صغيرة يتم استدعاؤها بشكل متكرّر مباشرةً إلى الدوال التي تستدعيها.
في تطبيق CodeBloat، تستدعي الطريقة doSomething() في كل فئة تم إنشاؤها الطريقة method0(). عند التجميع في وضع speed، من المرجّح أنّ يضمّن برنامج التجميع Optimizing في ART الرمز method0() في الرمز doSomething().
التمرين: تحقَّق من ذلك باستخدام oatdump على جهازك:
# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk
adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]
# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
--class-filter=com.android.codebloat.GeneratedClass0
ابحث عن طريقة doSomething في الناتج. إذا تم تضمينها، ستظهر لك التعليمات لتحميل السلسلة الثابتة الطويلة مباشرةً ضمن doSomething، بدلاً من تعليمات bl التي تستهدف method0.
تصوُّر عملية التحسين (CFG)
لمعرفة الوقت الذي قرّر فيه المحول البرمجي تضمين الدالة، يمكنك إنشاء رسم بياني لتدفّق التحكّم (CFG). تعرض هذه السمة حالة الرمز في كل مرحلة من مراحل مسار التحسين، مع كل عملية تحويل على مستوى التمثيل الوسيط (IR) للمترجم إلى أن يتم تحويل الرمز إلى مجموعة تعليمات بنية (ISA) المستهدَفة (مثل ARM64).
تشغيل
dex2oatباستخدام علامات التفريغ: استخدِم العلامة--verbose-methodsلحصر الناتج على طرق معيّنة، وإلا قد يصل حجم ملف.cfgلتطبيق كبير إلى عدة غيغابايت.# Substitution of actual paths required: adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \ --oat-file=/data/local/tmp/dump.odex \ --compiler-filter=speed \ --dump-cfg=/data/local/tmp/codebloat.cfg \ --verbose-methods=doSomethingالسحب والعرض: اسحب ملف
.cfgإلى محطة العمل وافتحه باستخدام IR Hydra.العثور على Inliner: في IR Hydra، حمِّل عناصر تجميع البيانات وابحث عن
doSomething. قارِن بين التمثيل قبل عملية Inliner وبعدها. سيظهر الرسم البياني موسّعًا عند دمج التعليمات منmethod0في المتصل.
بدلاً من ذلك، استخدِم أداة Opt Pipeline في Compiler Explorer (كما هو موضّح في القسم أعلاه) وأدخِل رمزًا مشابهًا للاطّلاع على عملية تحويل مشابهة يتم تنفيذها في خطوة Inliner.
تمرين: الحقول المتغيرة وحواجز الذاكرة
في تطبيق MemoryLab، يتم وضع علامة volatile على الحقل mGarbageSink. يضمن ذلك ألا يحسّن المترجم عمليات تخصيص الذاكرة غير الضرورية.
public volatile byte[] mGarbageSink;
في تفكيك ARM64، ستلاحظ أنّ كل عملية تخزين في هذا الحقل تكون مصحوبة بحاجز ذاكرة (dmb ish) أو استخدام تعليمات Load-Acquire/Store-Release (ldar/stlr). ويضمن ذلك إمكانية الوصول إلى البيانات من خلال سلسلة التعليمات، ولكنّه يضيف بعض التعليمات الإضافية إلى كل عملية وصول، ما يؤدي إلى زيادة حجم الرمز قليلاً مقارنةً بالحقل العادي.
التمرين: ابحث عن عمليات الوصول إلى الحقول وحواجز الذاكرة المرتبطة بها في تفكيك الرمز.
التمرين: عمليات التحقّق من التعليق الضمني
إذا فكّكت حلقة تكرار، مثل تلك الموجودة في generateAllocationChurn، ستلاحظ تعليمات غريبة في نهاية نص الحلقة:
ldr x21, [x21]
هذا هو فحص التعليق الضمني. يستخدم ART ذلك للسماح لبرنامج تجميع البيانات المهملة بإيقاف سلاسل التنفيذ مؤقتًا بأمان. يشير السجلّ x21 عادةً إلى نفسه.
عندما تحتاج عملية جمع البيانات غير الضرورية إلى تعليق مؤقت للسلسلة، فإنّها "تسمّم" موقع الذاكرة هذا. في المرة التالية التي يتم فيها تنفيذ ldr في سلسلة التعليمات البرمجية، سيؤدي ذلك إلى حدوث خطأ، وسيرصده وقت التشغيل ويستخدمه لنقل سلسلة التعليمات البرمجية إلى حالة معلّقة.
يتم تكرار هذا النمط في كل حلقة وفي بداية كل طريقة، ما يساهم في الحجم الإجمالي لرمز تطبيقك.
التمرين: ابحث عن جميع عمليات التحقّق الضمنية من التعليق المؤقت في تفكيك الطريقة، وحاوِل ربطها برمز المصدر الأصلي.
← WebView | ↑ للأعلى | سلاسل المحادثات →