العمل في الخلفية والخيوط
العمل في الخلفية والخيوط
يبدأ كل تطبيق Android بخيط واحد يُعرف بـ الخيط الرئيسي أو خيط واجهة المستخدم. يرسم Android الإطارات ويعالج أحداث اللمس ويستدعي دوال دورة الحياة (مثل onCreate وonResume) على هذا الخيط. والقاعدة مطلقة: إذا أُوقف الخيط الرئيسي لأكثر من بضع مئات من المللي ثانية، عرض Android مربع حوار التطبيق لا يستجيب (ANR) وضغط المستخدم على "إغلاق التطبيق". إذا أجريت أي استدعاء شبكة أو استعلام قاعدة بيانات أو قراءة ملف على الخيط الرئيسي، فستصطدم بهذا الجدار.
يعلّمك هذا الدرس كيفية نقل العمل بعيدًا عن الخيط الرئيسي باستخدام ExecutorService — الأسلوب الحديث الجاهز للإنتاج في تطوير Android بلغة Java — وكيفية إعادة النتائج بأمان إلى خيط واجهة المستخدم عند اكتمال العمل.
AsyncTask المساعد الأصلي لـ Android للعمل خارج الخيط الرئيسي، لكنها أُهملت في API 30 (Android 11) وأُزيلت رسميًا في API 33. كانت تعاني من أخطاء دقيقة في دورة الحياة تسبّب تسرّب الذاكرة والتعطّل عند تدوير الشاشة. البديل الحديث هو ExecutorService مقترنًا بـ Handler أو LiveData.
قانونا الخيوط في Android
- لا تحجب الخيط الرئيسي أبدًا. يجب أن تعمل الشبكة وعمليات الإدخال/الإخراج على القرص والعمليات المكثّفة للمعالج على خيط في الخلفية.
- لا تلمس أي عنصر واجهة من خيط خلفية أبدًا. يحق للخيط الرئيسي فقط تحديث عناصر التحكم في واجهة المستخدم. انتهاك هذا القانون يرمي استثناء
CalledFromWrongThreadException.
كل ما يأتي في هذا الدرس هو نتيجة لهذين القانونين.
ExecutorService — تشغيل العمل في الخلفية
ExecutorService هي واجهة تزامن معيارية في Java ضمن حزمة java.util.concurrent. تدير مجموعة من خيوط العمل (thread pool). تُقدّم لها Runnable أو Callable فتُشغّله على أحد خيوطها. عند استخدامها في Android تتجنّب إنشاء كائنات new Thread() خام لكل عملية، وهو أمر مكلف وصعب التحكم فيه.
توفّر فئة المصنع Executors عدة مجموعات جاهزة:
Executors.newSingleThreadExecutor()— خيط خلفية واحد، تعمل المهام بالترتيب. مثالي للكتابة المتسلسلة في قاعدة البيانات.Executors.newFixedThreadPool(n)— بالضبط n من الخيوط تعمل جميعها بالتوازي. استخدمه عندما يكون لديك مهام مستقلة متعددة.Executors.newCachedThreadPool()— يُنشئ خيوطًا حسب الطلب ويعيد استخدام الخيوط الخاملة. جيد للعمل المتقطّع القصير لكنه محفوف بالمخاطر إذا قدّمت مهام كثيرة جدًا.
نشر النتائج إلى الخيط الرئيسي
بعد انتهاء العمل في الخلفية تحتاج إلى تحديث واجهة المستخدم. تفعل ذلك بكائن Handler مرتبط بـ Looper الخيط الرئيسي. استدعاء handler.post(runnable) يضع ذلك الـ runnable في طابور تشغيله على حلقة رسائل الخيط الرئيسي — بأمان تام.
executor.shutdown() في onDestroy(). إذا أهملت هذا، تستمر خيوط الخلفية في التشغيل بعد اختفاء النشاط — مما يُهدر الذاكرة ويُحتمل أن يُسبّب تعطّلًا عند محاولتها الوصول إلى Views مُدمَّرة.
مشاركة المُنفّذ عبر التطبيق
إنشاء ExecutorService جديد لكل نشاط مقبول في الحالات البسيطة، لكن التطبيقات الأكبر ينبغي أن تشترك في مُنفّذ واحد على مستوى التطبيق للحدّ من العدد الإجمالي للخيوط. النمط الشائع تخزينه في فئة Application الفرعية:
الاستخدام من أي نشاط أو fragment:
الإلغاء ودورة حياة النشاط
خطأ شائع: يدوّر المستخدم الشاشة فيُدمَّر النشاط ويُعاد إنشاؤه، لكن المهمة في الخلفية لا تزال تحمل مرجعًا إلى Views النشاط القديم. عندما تُعيد المهمة النشر إلى الخيط الرئيسي تلمس Views مُدمَّرة — مما يُسبّب تعطّلًا أو فقدان بيانات صامتًا.
الأسلوب الأكثر أمانًا عند استخدام ExecutorService الخام هو التحقق مما إذا كان النشاط لا يزال حيًا قبل لمس واجهة المستخدم:
this (النشاط) داخل lambda تعمل لعدة ثوانٍ يمنع جمع القمامة من النشاط بأكمله — بما يشمل Views وDrawables وجميع البيانات المحتفظ بها. فضّل تمرير البيانات التي تحتاجها واجهتك فقط، لا كائن النشاط نفسه.
استخدام Future للحصول على قيم مُعادة
عندما تحتاج إلى قيمة مُعادة من عمل في الخلفية، استخدم executor.submit(callable) الذي يمنحك Future<T>. غير أن استدعاء future.get() يحجب الخيط المُستدعي — لذا لا تستدعِه أبدًا على الخيط الرئيسي. النمط الأفضل يلفّ تسليم النتيجة داخل المهمة نفسها باستخدام mainHandler.post() كما هو موضح أعلاه.
للسيناريوهات التي تُغذّي فيها مهام خلفية متعددة تحديثًا واحدًا لواجهة المستخدم، تُعدّ CountDownLatch وCompletableFuture (API 24+) أدوات قوية، لكنها تتّبع نفس العقد: نفّذ الانتظار المحجوب بعيدًا عن الخيط الرئيسي، وأعد نشر النتيجة النهائية فقط.
كيف تتعامل Room وRetrofit مع هذا تلقائيًا
البشرى الطيّبة: المكتبات التي ستستخدمها في هذا البرنامج التعليمي — Room (الدرس الثالث) وRetrofit (الدرس السادس) — مصمّمة لإبقاء خيوط الخلفية بعيدًا عن ناظريك. تفرض Room عدم تشغيل الاستعلامات على الخيط الرئيسي (وترمي استثناءً إذا حاولت)، وتُعيد أنواع LiveData النتائج إلى الخيط الرئيسي تلقائيًا. تُوزّع طريقة enqueue() في Retrofit العمل على خيط خلفية وتُسلّم ردّ النداء على الخيط الرئيسي.
فهم ExecutorService الخام لا يزال مهمًا لأنه الأساس الذي تبني عليه تلك المكتبات، وستواجه دائمًا سيناريوهات — مهمة منفردة أو قاعدة كود قديمة أو عملية ملف بسيطة — لا يتولّى فيها إطار العمل التعامل مع الخيوط نيابةً عنك.
الخلاصة
يمتلك نموذج الخيوط في Android قانونين لا يمكن انتهاكهما: لا تحجب الخيط الرئيسي، ولا تحدّث Views من خيط خلفية. توفّر ExecutorService من java.util.concurrent طريقة نظيفة وجاهزة للإنتاج لتشغيل العمل بعيدًا عن الخيط الرئيسي. أعد نشر تحديثات واجهة المستخدم عبر Handler(Looper.getMainLooper()). شارك المُنفّذات على مستوى التطبيق بنمط سينغلتون مثل AppExecutors، وأوقفها دائمًا عند تدمير المكوّن المالك لها. المكتبات ذات المستوى الأعلى مثل Room وRetrofit تؤتمت هذا النمط — لكن معرفتك به تجعلك مطوّر Android أكثر كفاءة بمراحل.