التجميد في التطوير — الجوهر والأسباب والوقاية

المؤلف: IT Sectr نُشر: 2026-07-28 وقت القراءة: 9 دق

التجميد (العلقة) هو حالة يتوقف فيها التطبيق المحمول عن الاستجابة لأي إجراءات المستخدم لفترة طويلة. على عكس البطء (تباطؤ العمل) والأخطاء (سلوك غير صحيح)، فإن التجميد يحظر واجهة المستخدم بالكامل: لا تتم معالجة اللمسات، ويتوقف الرسوم المتحركة، وتتجمد الشاشة. السبب هو حظر الخيط الرئيسي بعملية متزامنة، أو deadlock في كود متعدد الخيوط، أو جمع القمامة الطويل بشكل غير طبيعي. وفقاً لوثائق Apple Main Thread Checker، فإن أكثر من 40% من تقارير الأعطال في iOS مرتبطة بحظر الخيط الرئيسي. على Android، يؤدي موقف مماثل إلى ANR — حوار النظام «التطبيق لا يستجيب».

الرئيسي

  • التجميد — حظر كامل لواجهة المستخدم لفترة طويلة (ثوانٍ وعشرات الثواني)، يختلف عن البطء والأخطاء
  • الأسباب الرئيسية — حظر الخيط الرئيسي بالإدخال/الإخراج، deadlock بين الخيوط، حلقة لا نهائية وتسرب ذاكرة مع GC طويل
  • التشخيص يشمل Main Thread Checker على iOS، وسجلات ANR /data/anr/traces.txt على Android وتحليل تفريغ الخيوط
  • المعالجة — تفريغ جميع العمليات الطويلة المحتملة إلى خيوط الخلفية، استخدام Structured Concurrency وتجنب synchronized في خيط واجهة المستخدم
  • الوقاية — StrictMode، Main Thread Checker في مخطط Debug، تحليل ثابت للـ deadlock واختبارات دورية لقياس زمن الاستجابة

ما هو التجميد في التطوير المحمول

التجميد (العلقة) في التطبيق المحمول هو حالة يتوقف فيها التطبيق عن معالجة أحداث الإدخال وتحديث الواجهة لعدة ثوانٍ أو أكثر. تقنياً، هذا يعني أن الخيط الرئيسي محظور ولا يمكنه تنفيذ التكرار التالي لحلقة التشغيل.

الفرق بين التجميد والبطء وANR

البطء هو تأخير يصل إلى 500 مللي ثانية حيث يلاحظ المستخدم التباطؤ لكن التطبيق يستمر في العمل. التجميد يستمر من ثانية واحدة إلى عشرات الثواني. ANR على Android هو حالة خاصة من التجميد استمرت أكثر من 5 ثوانٍ وتم اكتشافها من قبل النظام. ليس كل تجميد يؤدي إلى ANR، لكن كل ANR هو تجميد موثق من قبل النظام.

عواقب التجميد

على Android، التجميد لأكثر من 5 ثوانٍ يؤدي إلى ظهور حوار ANR مع اقتراح إغلاق التطبيق. على iOS، النظام لديه watchdog — إذا لم يستجب التطبيق للأحداث لمدة 10–20 ثانية، يقوم Watchdog بإنهاء العملية بالرمز 0x8badf00d (ate bad food). يرى المستخدم فقط إغلاق التطبيق فجأة إلى الشاشة الرئيسية.

أسباب التجميد على Android وiOS

أي عملية تستغرق أكثر من 100 مللي ثانية وتُطلق على الخيط الرئيسي يمكن أن تسبب تجميداً. دعنا ننظر إلى المصادر الرئيسية للحظر.

الإدخال/الإخراج المتزامن في خيط واجهة المستخدم

قراءة ملف كبير، طلب شبكة بدون تزامن، حفظ البيانات في SharedPreferences بطريقة متزامنة apply متبوعة بـ commit — كل هذه العمليات تحظر الخيط الرئيسي. على Android، قراءة ملف بحجم 10 ميجابايت بشكل متزامن قد تستغرق 200–500 مللي ثانية حسب سرعة الذاكرة الوميضية. على iOS، تحميل URLSession متزامن بدون completionHandler يحظر واجهة المستخدم لمدة استجابة الخادم.

Deadlock في كود متعدد الخيوط

عندما ينتظر خيطان تحرير الموارد المحتجزة من قبل بعضهما البعض، يحدث deadlock. في التطبيقات المحمولة، السيناريو النموذجي هو أن الخيط A يحجب Lock1 وينتظر Lock2، بينما الخيط B يحجب Lock2 وينتظر Lock1. كلا الخيطين يتجمدان إلى الأبد. إذا كان أحدهما هو الخيط الرئيسي، يتجمد التطبيق بالكامل.

حلقة لا نهائية أو تكرار

خطأ في المنطق — على سبيل المثال، while(true) بدون شرط خروج أو تكرار بدون حالة أساسية — يؤدي إلى تنفيذ لا نهائي على الخيط الرئيسي. Android يكتشف هذا عبر ANR بعد 5 ثوانٍ، iOS — عبر Stackshot، الذي يلتقط مكدس استدعاءات متكرر بلا نهاية.

  • Android — Cursor بدون إغلاق، طلب متزامن عبر execute() بدلاً من enqueue()، FileInputStream.read() في خيط واجهة المستخدم
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES، تشغيل NSURLConnection sendSynchronousRequest، تحميل صورة باستخدام dataWithContentsOfURL
  • متعدد المنصات — Flutter compute بدون isolate مخصص، React Native NativeModule متزامن

كيفية تشخيص التجميد

تشخيص التجميد يتطلب أدوات قادرة على التقاط حالة جميع الخيوط في لحظة الحظر.

سجلات ANR على Android

عند كل ANR، يحفظ نظام Android ملف /data/anr/traces.txt الذي يحتوي على تفريغ مكدس لكل خيط في التطبيق. تحليل هذا الملف هو طريقة التشخيص الرئيسية: ابحث عن الخيط main وانظر أي طريقة توقف عليها. إذا انتهى المكدس بـ Thread.sleep أو InputStream.read أو Lock.lock — تم العثور على السبب.

Stackshot على iOS

Xcode يمكنه أخذ Stackshot — لقطة لمكدسات جميع الخيوط — عندما يتجمد التطبيق (إشارة SIGSTOP). فعّل “Logging” → “Include Stackshot Logs” في المخطط. عند تعطل بالرمز 0x8badf00d، استخرج سجل العطل من Devices & Simulators وابحث عن الخيط com.apple.main-thread بالمكدس العالق.

Main Thread Checker في Xcode

Main Thread Checker يكتشف تلقائياً استدعاءات UIKit من خيوط الخلفية أثناء تشغيل التطبيق. فعّله في المخطط (Diagnostics → Main Thread Checker). كل تحذير هو سبب محتمل للتجميد، خاصة إذا حدث في إغلاق completionHandler لطلب شبكة.

مثال على اكتشاف الحظر عبر StrictMode على Android:

kotlin
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())
    }
}

طرق إزالة حظر واجهة المستخدم

إزالة التجميد تبدأ بنقل جميع العمليات الطويلة المحتملة إلى خيوط الخلفية. دعنا ننظر إلى تقنيات محددة لكل منصة.

التزامن المهيكل مع coroutines

Kotlin Coroutines مع viewModelScope.launch(Dispatchers.IO) تضمن أن عمليات الشبكة أو قراءات قاعدة البيانات تنفذ في خيط الخلفية. Dispatchers.Main يُستخدم فقط لتحديث واجهة المستخدم. مهم: جميع وظائف suspend يجب أن تكون مهيكلة — يتم إلغاء coroutines الفرعية عند إلغاء الأصل، مما يمنع تسرب الخيوط.

قوائم الانتظار غير المتزامنة على iOS

Grand Central Dispatch مع DispatchQueue.global(qos: .userInitiated) للمهام الخلفية وDispatchQueue.main.async لتحديث واجهة المستخدم هو النمط القياسي. تجنب sync() على قائمة الانتظار الرئيسية — هذا deadlock مضمون. استخدم async/await (Swift 5.5+) لكود غير متزامن أكثر قابلية للقراءة مع عودة تلقائية إلى الخيط الرئيسي عبر MainActor.

تجنب synchronized في خيط واجهة المستخدم

كتل synchronized في Kotlin و@synchronized في Swift على الخيط الرئيسي خطيرة: إذا كان خيط آخر قد اكتسب هذا القفل بالفعل، فإن الخيط الرئيسي سيتجمد في الانتظار. استخدم أنواعاً ذرية (AtomicInteger، خصائص ذرية في Swift) أو قوائم انتظار تسلسلية بدلاً من الأقفال.

مثال على تحميل بيانات غير متزامن مع coroutines على Android:

kotlin
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

اضبط StrictMode مع penaltyDeath لسياسات الخيوط — سيؤدي هذا إلى تعطل فوري للتطبيق عند اكتشاف استدعاء شبكة أو إدخال/إخراج قرص على الخيط الرئيسي. لن يتمكن المطور من تجاهل المشكلة. في إصدارات الإنتاج، استخدم penaltyLog لجمع الإحصائيات دون أعطال.

Main Thread Checker في مخطط Debug

على iOS، فعّل Main Thread Checker في مخطط Debug واضبط CI لتشغيل الاختبارات مع هذا الخيار. إذا احتوى الاختبار على استدعاء UIKit من خيط خلفية — يجب أن يفشل. هذه هي الطريقة الوحيدة الموثوقة لتحديد المشكلة قبل الإرسال إلى TestFlight.

مراجعة الأقران مع التحقق من تعدد الخيوط

أضف بنداً إلزامياً إلى عملية مراجعة الكود: التحقق من أن أي استدعاء شبكة أو عملية ملفات أو وصول إلى قاعدة بيانات أو حساب ثقيل يتم تنفيذه في خيط خلفية. Deadlock يمكن اكتشافه باستخدام محللات ثابتة: Infer من Facebook وThread Safety Checker من Xcode يجدان الأقفال المحتملة قبل التشغيل.

  • Android — StrictMode, Infer, Android Lint Multithread, Kotlin Coroutines مع viewModelScope
  • iOS — Main Thread Checker, TSAN (Thread Sanitizer), Xcode Analyze, Swift async/await مع MainActor
  • متعدد المنصات — Flutter compute isolate, React Native interaction manager مع requestAnimationFrame

الأسئلة الشائعة

ما الفرق بين التجميد وANR؟

ANR (Application Not Responding) هو إشعار نظام Android يظهر عندما يتجمد الخيط الرئيسي لأكثر من 5 ثوانٍ. التجميد مفهوم أوسع: أي حظر لواجهة المستخدم بأي مدة. iOS ليس لديه ANR، لكن لديه Watchdog بمهلة 10–20 ثانية.

كيفية قراءة traces.txt على Android؟

الملف موجود في /data/anr/traces.txt. الوصول يتطلب صلاحيات الجذر أو adb shell: نفّذ adb shell cat /data/anr/traces.txt \> traces.txt بصلاحيات الجذر. في المكدس، ابحث عن الخيط “main” — آخر طريقة تم استدعاؤها تشير إلى سبب الحظر.

لماذا يتجمد التطبيق على iOS لكن لا يتعطل؟

إذا استمر التجميد أقل من 10 ثوانٍ، لا يتم تفعيل Watchdog ويتجمد التطبيق ببساطة حتى اكتمال العملية الحاجزة. لا يرى المستخدم تعطلاً لكنه يشعر بالإحباط. لاكتشاف هذه الحالات، استخدم MetricKit مع تتبعات زمن تنفيذ مخصصة.

كيفية اختبار التطبيق ضد التجميد؟

استخدم اختبارات واجهة المستخدم مع التحقق من أن الشاشة تفتح في < 1 ثانية. أضف إلى CI قياس الوقت بين اللمس وظهور الشاشة التالية. على Android، استخدم Espresso مع IdlingResource لانتظار العمليات غير المتزامنة. على iOS، استخدم XCTest مع XCTWaiter للتحقق من وقت التحميل.

هل يمكن لـ SwiftUI التسبب في تجميد؟

SwiftUI بحد ذاته لا يسبب تجميداً، لكن الحسابات المعقدة في الخاصية body تفعل. إذا استغرق حساب body 500 مللي ثانية بسبب عمليات ثقيلة، تتجمد واجهة المستخدم. الحل هو تفريغ الحسابات إلى Task.detached وتحديث @State بشكل غير متزامن على الممثل الرئيسي.

الخلاصة

  • التجميد — حظر كامل لواجهة المستخدم لثوانٍ أو عشرات الثواني، ناتج عن حظر الخيط الرئيسي أو deadlock أو حلقة لا نهائية
  • التشخيص — /data/anr/traces.txt على Android، Stackshot وMain Thread Checker على iOS
  • الأسباب الرئيسية — إدخال/إخراج متزامن، deadlock بين الخيوط، تكرار لا نهائي، GC طويل
  • المعالجة — coroutines مع المرسلين الصحيحين، async/await مع MainActor، تفريغ جميع عمليات الإدخال/الإخراج إلى خيوط الخلفية
  • الوقاية — StrictMode مع penaltyDeath، Main Thread Checker، تحليل ثابت للـ deadlock (Infer, TSAN)
  • على Android تجميد > 5 ث = ANR؛ على iOS > 10–20 ث = Watchdog crash (0x8badf00d)
  • توصية: فعّل Thread Sanitizer في مخطط Debug واضبط CI لتشغيل اختبارات مع TSAN لاكتشاف data races وdeadlocks

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا