حصلت أدوات بناء Android للتو على دفعة سرعة لـ Kotlin Coroutines. فمع إصدار Android Gradle Plugin (AGP) 9.2.0 ومُصغّر R8 المرفق معه، تمت إعادة كتابة الـ bytecode الذي يدعم معالجة حالة الـ coroutine لتقديم أداء أسرع بمقدار الضعف تقريبًا في أكثر عمليات الـ coroutine شيوعًا. ويمكن للمطورين الذين يطلقون تطبيقات Android تعتمد على الـ coroutines ملاحظة أوقات تشغيل باردة (cold-start) أسرع، ومعدلات إطارات أكثر استقرارًا، وتحسن طفيف في عمر البطارية دون تغيير سطر واحد من كود Kotlin.
لماذا كان أداء الـ coroutine مهمًا
تستخدم Kotlin Coroutines كائنات AtomicFieldUpdater لتعديل الحقول الداخلية بأمان من خيوط معالجة (threads) متعددة. يتجنب هذا النهج تخصيص كائن atomic جديد لكل عملية تحديث، ولكنه يفرض ثلاث تكاليف خفية على Android:
- تأخير تحميل الفئة (Class-loading delay) – يتم إنشاء الـ updater باستخدام الـ reflection، لذا يجب على الـ VM البحث عن الحقل المستهدف أثناء وقت التشغيل.
- هدر في وحدة المعالجة المركزية (CPU waste) – يتحقق كل وصول إلى الحقل من حالة الـ updater قبل الوصول إلى الحقل الفعلي.
- قيود الـ Inlining – لا يستطيع المترجم عمل inlining لاستدعاءات الـ updater، مما يجعل الـ bytecode المولد أكبر وأبطأ.
من الناحية العملية، تظهر هذه التكاليف أثناء المسارات الساخنة (hot paths) مثل إرسال/استقبال القنوات (channel send/receive)، وقفل/إلغاء قفل الـ mutex، وتحديثات StateFlow وتوزيع الـ coroutine (coroutine dispatch). والنتيجة هي استهلاك ملحوظ لوقت وحدة المعالجة المركزية في عمليات الحسابات الإدارية بدلاً من التركيز على عمل التطبيق نفسه.
ما الذي تغير في AGP 9.2.0 و R8
يقوم R8، وهو مُصغّر الكود المرفق مع AGP 9.2.0، الآن بفحص الـ bytecode المترجم بحثًا عن نمط AtomicFieldUpdater القياسي. وعندما يجده، يقوم بأربعة تحويلات:
- تحديد الإزاحة الذاكرية (memory offset) للحقل. يحسب R8 الموقع الدقيق للحقل المستهدف داخل تخطيط الكائن.
- التخلص من كائن الـ updater. يختفي الغلاف (wrapper) القائم على الـ reflection، مما يوفر الذاكرة ويلغي عبء تحميل الفئة.
- إدراج استدعاء مباشر لـ
sun.misc.Unsafe. تقوم هذه الـ API منخفضة المستوى بالكتابة في الحقل باستخدام تعليمات عتادية (hardware instruction) ذرية واحدة. - استبدال كل استدعاء لـ updater بالتعليمات الجديدة من
Unsafeمباشرة، مما يسمح لمترجم JIT بعمل inlining للعملية.
الأثر الصافي هو أن وحدة المعالجة المركزية لم تعد مضطرة لإجراء بحث عبر الـ reflection أو عمليات تحقق أثناء وقت التشغيل؛ بل تنفذ التعليمات الذرية مباشرة. ومن منظور المطور، فإن هذا التغيير غير مرئي – حيث تعمل واجهة برمجة تطبيقات (API) الـ coroutine بنفس الطريقة – ولكن من تحت الغطاء، يعمل الكود بسرعة تقترب من "مستوى العتاد" (metal-level).
مكاسب ملموسة
تُظهر اختبارات الأداء (Benchmarks) على جهاز Android نموذجي ما يلي من تسريع السرعة بعد البناء باستخدام AGP 9.2.0، مع تفعيل R8 وتفعيل خاصية التصغير (minification):
- إرسال/استقبال القنوات (Channel send/receive): أسرع بـ 2.01 ×
- قفل/إلغاء قفل الـ Mutex: أسرع بـ 1.90 ×
- تحديثات
StateFlow: أسرع بـ 2.02 × - توزيع الـ Coroutine (Coroutine dispatch): أسرع بـ 1.68 ×
تترجم هذه الأرقام إلى تحسينات ملموسة في تجربة المستخدم. فعملية التشغيل البارد التي كانت تقضي جزءًا من الثانية في انتظار مزامنة الـ coroutine تنتهي الآن بشكل أسرع، مما يمنح خيط واجهة المستخدم (UI thread) مساحة أكبر لرسم الإطار الأول. كما أن تقليل التنافس على وحدة المعالجة المركزية يسمح للمعالج بالعودة إلى وضع السكون بشكل أسرع، مما قد يحسن عمر البطارية.
كيف تستفيد من هذه الميزة
لا يلزم إجراء أي تغييرات في الكود. لتفعيل عملية إعادة الكتابة هذه، تحتاج إلى:
- AGP 9.2.0 أو أحدث – الإصدار الذي يحتوي على R8 المحدث.
- R8 – يتم استخدامه تلقائيًا عند البناء باستخدام AGP المذكور أعلاه.
- Kotlin Coroutines 1.8.0+ – إصدار المكتبة الذي يأتي مع نمط
AtomicFieldUpdaterالذي يتوقعه المُحسِّن (optimizer). - ضبط
isMinifyEnabled = trueفي نوع بناء الإصدار (release build type) الخاص بك – حيث لا يعمل R8 إلا عندما تكون خاصية التصغير مفعلة.
الخطوة الإضافية الوحيدة هي مراجعة قواعد ProGuard (أو R8) الخاصة بك. إن توجيهات -keep الواسعة التي تحافظ على الحقول المتطايرة (volatile fields) أو فئات الـ updater نفسها ستمنع عملية إعادة الكتابة. تأكد من أن القواعد تسمح لـ R8 بتعديل تلك الحقول؛ وإلا سيعود المُحسِّن إلى التنفيذ الأصلي القائم على الـ reflection.
يمكنك التحقق من عملية التحويل باستخدام APK Analyzer في Android Studio. افتح ملف APK المترجم، وحدد موقع فئة دعم coroutine مثل JobSupport ، وافحص الـ bytecode الذي تم فك تجميعه. إذا نجحت عملية إعادة الكتابة، فستختفي حقول الـ updater الثابتة وسترى استدعاءات مباشرة لـ Unsafe بدلاً منها.
تنبيهات ونقاط مضادة
يعتمد هذا التحسين على شرطين قد لا يستوفيهما كل مشروع:
- يجب تفعيل عملية التصغير (Minification). لن تستفيد نسخ التصحيح (Debug builds)، أو نسخ الإصدار (release builds) التي تُبقي عملية التصغير معطلة لتسهيل عملية التصحيح، من هذه الميزة.
- يجب أن تكون قواعد ProGuard مرنة. قد تحتاج المشاريع التي تستخدم أنماط
-keepصارمة للمكونات الداخلية لـ coroutine إلى تخفيف تلك القواعد، مما قد يعرض الفئات الداخلية (internal classes) لأخطاء متعلقة بعملية التقليص (shrinking) إذا لم يتم اختبارها بعناية.
لا يزال من الممارسات الجيدة الاختبار على مجموعة الأجهزة التي تستهدفها.
ما يجب مراقبته لاحقاً
تُظهر عملية إعادة الكتابة كيف يمكن لتحويلات bytecode أثناء وقت البناء (build-time) استخراج الأداء المخفي وراء تجريدات اللغة (language abstractions). فكر في إجراء تحليل للأداء (profiling) لكودك الخاص الذي يعتمد بكثافة على coroutine للتأكد من المكاسب في عبء العمل الخاص بك.
الخلاصة: تؤدي الترقية إلى AGP 9.2.0 وتفعيل عملية التصغير (minification) في R8 إلى مضاعفة سرعات المزامنة الحرجة لتطبيقات Kotlin التي تعتمد بكثافة على coroutine تقريباً، وكل ذلك دون المساس بالكود المصدري—بشرط أن تسمح إعدادات البناء للمُحسِّن (optimizer) بالقيام بعمله.
