التأخير هو وصف من المستخدم لموقف حيث يعمل التطبيق المحمول ببطء وغير منتظم: فقد يستجيب بطريقة طبيعية، وفجأة يتجمد لعدة ثوانٍ. في السياق التقني، «التأخير» يعني مزيجًا من التأخيرات والتجمدات الدقيقة الناجمة عن توقفات GC المتكررة، وحظر الخيط الرئيسي بواسطة العمليات المتزامنة وهياكل البيانات غير المثلى. وفقًا لدليل Android Performance Benchmarking Guide، فإن تقليل وقت الاستجابة من 300 مس إلى 100 مس يزيد معدل احتفاظ المستخدمين بنسبة 25%. يتطلب تشخيص التأخير مزيجًا من تحليل CPU والذاكرة مع تحليل تردد جمع القمامة.
النقاط الرئيسية
التأخير هو مصطلح غير رسمي يستخدمه المستخدمون لوصف أداء التطبيق البطيء ذاتيًا. على عكس اللاج الذي يظهر كتأخير ثابت، فإن التأخير يتكون من تجمدات غير منتظمة: قد يعمل التطبيق بشكل مثالي لعدة ثوانٍ ثم «يفكر» لمدة 1–3 ثوانٍ.
من وجهة نظر التحليل، يظهر التأخير كسلسلة من الإطارات المفقودة (jank) مع تأخيرات ذروة تتجاوز 100 مس. على رسم FPS، يبدو هذا كانخفاضات حادة: 60 → 20 → 55 → 10 إطارًا في الثانية. على عكس اللاج ذي FPS المنخفض بانتظام، فإن التأخير له تباين واضح.
عندما يتأخر التطبيق، لا يفهم المستخدم منطق الإبطاء: قد تنزلق الشاشة بسلاسة ثم تتوقف فجأة لثانية. يسبب هذا الإحباط ويقلل الثقة في التطبيق. وفقًا لجوجل، يترك 53% من المستخدمين الموقع أو التطبيق إذا استغرق التحميل أكثر من 3 ثوانٍ.
يشير الطابع المتقطع للتأخير إلى أن المشكلة ناجمة عن عوامل قائمة على الأحداث وليس الحمل الزائد المستمر. دعنا نتفحص السيناريوهات النموذجية.
على Android، في بيئة ART، يوقف جمع القمامة جميع خيوط التطبيق. إذا قام الكود بإنشاء العديد من الكائنات المؤقتة — على سبيل المثال، إنشاء String جديد عبر السلسلة في كل استدعاء onBindViewHolder — يتم تشغيل GC بشكل أكثر تكرارًا. قد تدوم التوقفة 5–50 مس حسب حجم الكومة وجيل الكائنات. يشعر المستخدم بهذا ك«تفكير» مفاجئ.
يدعم Room على Android و Core Data على iOS استعلامات غير متزامنة، ولكن المطورين غالبًا ما يستخدمون getValue() أو ينفذون الاستعلامات عبر runBlocking للبساطة. قد يستغرق SELECT ثقيل بالانضمامات على جدول من 10,000 صف ما بين 200–500 مس، مما يحجز UI بالكامل خلال هذه الفترة.
يستغرق تحميل صورة كاميرا (12 MP، 4000x3000 بكسل) دون تقليس ما يصل إلى 200 مس لفك تشفير إلى Bitmap. إذا تم تحميل الصور بشكل غير متزامن ولكن دون مجموعة خيوط محدودة، فإن تشغيل 5–6 عمليات فك متزامنة يمكن أن يحمّل CPU، مما يسبب تباطؤات متنقلة.
تشخيص التباطؤات المتقطعة أصعب من تشخيص التأخيرات الثابتة لأن المشكلة قد لا تتكرر في كل تشغيل. يتطلب جمع إحصاءات على مدار فترة طويلة.
Android Studio Memory Profiler يظهر ليس فقط استخدام الذاكرة ولكن أيضًا أحداث GC: التردد، النوع (Concurrent، Full)، المدة. إذا حدث GC أكثر من مرة واحدة كل 5 ثوانٍ في حالة الخمول — هذا علامة على التخصيص المفرط. يسمح أخذ heap dump في لحظة التأخير برؤية الكائنات التي تشغل الذاكرة.
على iOS، استخدم قالب Allocations في Instruments لتتبع إنشاء الكائنات وتحريرها. قم بتمكين الأجيال (Generations) — فهي تسمح بأخذ لقطات للكومة بين الإجراءات ورؤية الكائنات التي تبقى في الذاكرة. الكائنات الثابتة التي لا يتم تحريرها هي مصدر تراكم الذاكرة والتوقفات اللاحقة.
JankStats هي مكتبة Android تجمع مقاييس الإطارات المفقودة في الوقت الفعلي. تربط كل jank بالسيناريو الحالي (على سبيل المثال، «تمرير القائمة»، «فتح الشاشة»)، مما يجعل من الممكن فهم الإجراء المحدد الذي يثير التأخير.
مثال دمج JankStats لتتبع التجمدات على Android:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
يتطلب القضاء على التأخير عملًا مركزًا على كل سبب. لا يوجد حل عام — يلزم تحليل ملفات أداء محددة.
إذا كانت القائمة تحتوي على 1000+ عنصر وتم تحميل الكل مرة واحدة — هذا تأخير مضمون. Paging 3 على Android و NSFetchedResultsController على iOS يقومان تحميل البيانات بأجزاء عندما يتمرر المستخدم. يرى المستخدم فقط أول 10–20 عنصرًا؛ يتم تحميل الباقي في الخلفية.
Room يسمح بتحليل الاستعلامات عبر أداة الفحص في Android Studio: يمكن رؤية وقت التنفيذ، عدد الصفوف المعادة وخطة الاستعلام. إضافة فهارس على أعمدة WHERE و ORDER BY يمكن أن تقلل وقت الاستعلام من 300 مس إلى 5 مس. على iOS، يقوم Core Data Profiler في Instruments بفحص مماثل.
المزامنة الخلفية، تنزيل الملفات، معالجة البيانات — كل هذا يجب أن يتم تنفيذه عبر WorkManager (Android) أو Background Tasks (iOS). إذا تم تشغيل المزامنة على خيط UI، سيتأخر التطبيق أثناء التنفيذ. يضمن WorkManager التنفيذ على خيط خلفي مع مراعاة حالة البطارية والشبكة.
مثال المزامنة الخلفية عبر WorkManager على Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "Syncing data in background thread")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
يمكن منع التأخير في مرحلة الكتابة باتباع مبادئ إدارة الذاكرة والخيوط بكفاءة.
Baseline Profiles هي قائمة من الفئات والطرق التي يقوم Android بتجميعها مسبقًا (AOT) بدلاً من JIT. بدون ملف تعريف، يتم تجميع كل شاشة جديدة عند الفتح الأول، مما يسبب تأخيرًا مدته 100–500 مس. قم بإعداد Baseline Profile للشاشات الرئيسية وفعل التوليد في Gradle عبر إضافة baseline-profile-gradle-plugin.
المسار الساخن هو كود يتم تنفيذه في كل إطار: onBindViewHolder، draw، layoutSubviews. تجنب إنشاء الكائنات في هذه الطرق: استخدم مجموعات الكائنات، StringBuilder بدلاً من السلسلة، قم بتخزين النصوص المنسقة والمنسقات. كل تخصيص إضافي يقرب GC التالي.
أضف Macrobenchmark إلى خط أنابيب CI الخاص بك مع سيناريو تمرير القائمة وفتح الشاشة. حدد عتبة: نسبة المئوي 99 لوقت الإطار يجب ألا تتجاوز 16 مس. إذا تم تجاوز العتبة — يتم رفض البناء حتى التحسين.
الأسئلة الشائعة
اللاج هو تأخير ثابت (على سبيل المثال، 200 مس على كل نقرة). التأخير متقطع: التطبيق يعمل بطريقة طبيعية، ثم يبطأ فجأة لمدة 1–3 ثوانٍ، ثم يعود إلى الوضع الطبيعي. السبب هو عوامل قائمة على الأحداث مثل توقفات GC أو استعلامات قاعدة البيانات المتزامنة.
استخدم Memory Profiler في Android Studio: تعرض علامة التبويب Memory أحداث GC مع المدة. للمراقبة في الإنتاج، قم بدمج Firebase Performance Monitoring مع تتبعات مخصصة. على iOS، قم بتمكين Malloc Debug ووسم أجيال التخصيصات في Instruments.
بشكل غير مباشر — نعم. إذا تأخر رد الخادم وكانت UI تنتظره بشكل متزامن، يتجمد التطبيق. إذا كان الطلب غير متزامن ولكن معالجة الرد تتم في خيط UI — فسيتسبب أيضًا في التأخير. الحل هو المعالجة غير المتزامنة مع coroutines ومؤشرات التقدم.
عند استخدامه بشكل غير صحيح، يمكن لـ KMP إنشاء كائنات غلاف زائدة للقابلية التشغيلية. على iOS، يزيد هذا من تردد التخصيصات ونتيجة ذلك، توقفات ARC. استخدم @ObjCName، قم بتحسين expect/actual وتجنب الاستدعاءات المتكررة للكود المشترك من المسارات الساخنة للواجهة.
زيادة الكومة عبر android:largeHeap=”true” تؤخر GC ولكنها لا تلغي سبب التخصيصات. عندما يتم تشغيل GC في نهاية المطاف، ستكون التوقفة أطول لأنه يجب اجتياز المزيد من الكائنات. الحل هو تقليل عدد التخصيصات، وليس توسيع الكومة.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.