التجميد (العلقة) هو حالة يتوقف فيها التطبيق المحمول عن الاستجابة لأي إجراءات المستخدم لفترة طويلة. على عكس البطء (تباطؤ العمل) والأخطاء (سلوك غير صحيح)، فإن التجميد يحظر واجهة المستخدم بالكامل: لا تتم معالجة اللمسات، ويتوقف الرسوم المتحركة، وتتجمد الشاشة. السبب هو حظر الخيط الرئيسي بعملية متزامنة، أو deadlock في كود متعدد الخيوط، أو جمع القمامة الطويل بشكل غير طبيعي. وفقاً لوثائق Apple Main Thread Checker، فإن أكثر من 40% من تقارير الأعطال في iOS مرتبطة بحظر الخيط الرئيسي. على Android، يؤدي موقف مماثل إلى ANR — حوار النظام «التطبيق لا يستجيب».
الرئيسي
التجميد (العلقة) في التطبيق المحمول هو حالة يتوقف فيها التطبيق عن معالجة أحداث الإدخال وتحديث الواجهة لعدة ثوانٍ أو أكثر. تقنياً، هذا يعني أن الخيط الرئيسي محظور ولا يمكنه تنفيذ التكرار التالي لحلقة التشغيل.
البطء هو تأخير يصل إلى 500 مللي ثانية حيث يلاحظ المستخدم التباطؤ لكن التطبيق يستمر في العمل. التجميد يستمر من ثانية واحدة إلى عشرات الثواني. ANR على Android هو حالة خاصة من التجميد استمرت أكثر من 5 ثوانٍ وتم اكتشافها من قبل النظام. ليس كل تجميد يؤدي إلى ANR، لكن كل ANR هو تجميد موثق من قبل النظام.
على Android، التجميد لأكثر من 5 ثوانٍ يؤدي إلى ظهور حوار ANR مع اقتراح إغلاق التطبيق. على iOS، النظام لديه watchdog — إذا لم يستجب التطبيق للأحداث لمدة 10–20 ثانية، يقوم Watchdog بإنهاء العملية بالرمز 0x8badf00d (ate bad food). يرى المستخدم فقط إغلاق التطبيق فجأة إلى الشاشة الرئيسية.
أي عملية تستغرق أكثر من 100 مللي ثانية وتُطلق على الخيط الرئيسي يمكن أن تسبب تجميداً. دعنا ننظر إلى المصادر الرئيسية للحظر.
قراءة ملف كبير، طلب شبكة بدون تزامن، حفظ البيانات في SharedPreferences بطريقة متزامنة apply متبوعة بـ commit — كل هذه العمليات تحظر الخيط الرئيسي. على Android، قراءة ملف بحجم 10 ميجابايت بشكل متزامن قد تستغرق 200–500 مللي ثانية حسب سرعة الذاكرة الوميضية. على iOS، تحميل URLSession متزامن بدون completionHandler يحظر واجهة المستخدم لمدة استجابة الخادم.
عندما ينتظر خيطان تحرير الموارد المحتجزة من قبل بعضهما البعض، يحدث deadlock. في التطبيقات المحمولة، السيناريو النموذجي هو أن الخيط A يحجب Lock1 وينتظر Lock2، بينما الخيط B يحجب Lock2 وينتظر Lock1. كلا الخيطين يتجمدان إلى الأبد. إذا كان أحدهما هو الخيط الرئيسي، يتجمد التطبيق بالكامل.
خطأ في المنطق — على سبيل المثال، while(true) بدون شرط خروج أو تكرار بدون حالة أساسية — يؤدي إلى تنفيذ لا نهائي على الخيط الرئيسي. Android يكتشف هذا عبر ANR بعد 5 ثوانٍ، iOS — عبر Stackshot، الذي يلتقط مكدس استدعاءات متكرر بلا نهاية.
تشخيص التجميد يتطلب أدوات قادرة على التقاط حالة جميع الخيوط في لحظة الحظر.
عند كل ANR، يحفظ نظام Android ملف /data/anr/traces.txt الذي يحتوي على تفريغ مكدس لكل خيط في التطبيق. تحليل هذا الملف هو طريقة التشخيص الرئيسية: ابحث عن الخيط main وانظر أي طريقة توقف عليها. إذا انتهى المكدس بـ Thread.sleep أو InputStream.read أو Lock.lock — تم العثور على السبب.
Xcode يمكنه أخذ Stackshot — لقطة لمكدسات جميع الخيوط — عندما يتجمد التطبيق (إشارة SIGSTOP). فعّل “Logging” → “Include Stackshot Logs” في المخطط. عند تعطل بالرمز 0x8badf00d، استخرج سجل العطل من Devices & Simulators وابحث عن الخيط com.apple.main-thread بالمكدس العالق.
Main Thread Checker يكتشف تلقائياً استدعاءات UIKit من خيوط الخلفية أثناء تشغيل التطبيق. فعّله في المخطط (Diagnostics → Main Thread Checker). كل تحذير هو سبب محتمل للتجميد، خاصة إذا حدث في إغلاق completionHandler لطلب شبكة.
مثال على اكتشاف الحظر عبر StrictMode على Android:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
إزالة التجميد تبدأ بنقل جميع العمليات الطويلة المحتملة إلى خيوط الخلفية. دعنا ننظر إلى تقنيات محددة لكل منصة.
Kotlin Coroutines مع viewModelScope.launch(Dispatchers.IO) تضمن أن عمليات الشبكة أو قراءات قاعدة البيانات تنفذ في خيط الخلفية. Dispatchers.Main يُستخدم فقط لتحديث واجهة المستخدم. مهم: جميع وظائف suspend يجب أن تكون مهيكلة — يتم إلغاء coroutines الفرعية عند إلغاء الأصل، مما يمنع تسرب الخيوط.
Grand Central Dispatch مع DispatchQueue.global(qos: .userInitiated) للمهام الخلفية وDispatchQueue.main.async لتحديث واجهة المستخدم هو النمط القياسي. تجنب sync() على قائمة الانتظار الرئيسية — هذا deadlock مضمون. استخدم async/await (Swift 5.5+) لكود غير متزامن أكثر قابلية للقراءة مع عودة تلقائية إلى الخيط الرئيسي عبر MainActor.
كتل synchronized في Kotlin و@synchronized في Swift على الخيط الرئيسي خطيرة: إذا كان خيط آخر قد اكتسب هذا القفل بالفعل، فإن الخيط الرئيسي سيتجمد في الانتظار. استخدم أنواعاً ذرية (AtomicInteger، خصائص ذرية في Swift) أو قوائم انتظار تسلسلية بدلاً من الأقفال.
مثال على تحميل بيانات غير متزامن مع coroutines على Android:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
مزيج من الأدوات والمبادئ المعمارية وعمليات مراجعة الكود يساعد في منع التجميد بشكل منهجي.
اضبط StrictMode مع penaltyDeath لسياسات الخيوط — سيؤدي هذا إلى تعطل فوري للتطبيق عند اكتشاف استدعاء شبكة أو إدخال/إخراج قرص على الخيط الرئيسي. لن يتمكن المطور من تجاهل المشكلة. في إصدارات الإنتاج، استخدم penaltyLog لجمع الإحصائيات دون أعطال.
على iOS، فعّل Main Thread Checker في مخطط Debug واضبط CI لتشغيل الاختبارات مع هذا الخيار. إذا احتوى الاختبار على استدعاء UIKit من خيط خلفية — يجب أن يفشل. هذه هي الطريقة الوحيدة الموثوقة لتحديد المشكلة قبل الإرسال إلى TestFlight.
أضف بنداً إلزامياً إلى عملية مراجعة الكود: التحقق من أن أي استدعاء شبكة أو عملية ملفات أو وصول إلى قاعدة بيانات أو حساب ثقيل يتم تنفيذه في خيط خلفية. Deadlock يمكن اكتشافه باستخدام محللات ثابتة: Infer من Facebook وThread Safety Checker من Xcode يجدان الأقفال المحتملة قبل التشغيل.
الأسئلة الشائعة
ANR (Application Not Responding) هو إشعار نظام Android يظهر عندما يتجمد الخيط الرئيسي لأكثر من 5 ثوانٍ. التجميد مفهوم أوسع: أي حظر لواجهة المستخدم بأي مدة. iOS ليس لديه ANR، لكن لديه Watchdog بمهلة 10–20 ثانية.
الملف موجود في /data/anr/traces.txt. الوصول يتطلب صلاحيات الجذر أو adb shell: نفّذ adb shell cat /data/anr/traces.txt \> traces.txt بصلاحيات الجذر. في المكدس، ابحث عن الخيط “main” — آخر طريقة تم استدعاؤها تشير إلى سبب الحظر.
إذا استمر التجميد أقل من 10 ثوانٍ، لا يتم تفعيل Watchdog ويتجمد التطبيق ببساطة حتى اكتمال العملية الحاجزة. لا يرى المستخدم تعطلاً لكنه يشعر بالإحباط. لاكتشاف هذه الحالات، استخدم MetricKit مع تتبعات زمن تنفيذ مخصصة.
استخدم اختبارات واجهة المستخدم مع التحقق من أن الشاشة تفتح في < 1 ثانية. أضف إلى CI قياس الوقت بين اللمس وظهور الشاشة التالية. على Android، استخدم Espresso مع IdlingResource لانتظار العمليات غير المتزامنة. على iOS، استخدم XCTest مع XCTWaiter للتحقق من وقت التحميل.
SwiftUI بحد ذاته لا يسبب تجميداً، لكن الحسابات المعقدة في الخاصية body تفعل. إذا استغرق حساب body 500 مللي ثانية بسبب عمليات ثقيلة، تتجمد واجهة المستخدم. الحل هو تفريغ الحسابات إلى Task.detached وتحديث @State بشكل غير متزامن على الممثل الرئيسي.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.