أخطاء ANR (مرات الظهور)

مفاهيم وتنفيذ في Jetpack Compose

عندما يتم حظر سلسلة التعليمات الخاصة بواجهة مستخدم أحد تطبيقات Android لفترة طويلة جدًا، يحدث خطأ "التطبيق لا يستجيب" (ANR). إذا كان التطبيق في المقدّمة، يعرض النظام مربّع حوار للمستخدم، كما هو موضّح في الشكل 1. يمنح مربّع حوار ANR المستخدم فرصة إيقاف التطبيق بالقوة.

مربّع حوار خطأ ANR الذي يظهر للمستخدم
الشكل 1. مربّع حوار ANR المعروض للمستخدم

تُعدّ أخطاء ANR مشكلة لأنّ سلسلة التعليمات الرئيسية في التطبيق، المسؤولة عن تعديل واجهة المستخدم، لا يمكنها معالجة بيانات أدخلها المستخدم أو رسمها، ما يؤدي إلى إحباط المستخدم. لمزيد من المعلومات عن سلسلة التعليمات الرئيسية في التطبيق، يُرجى الاطّلاع على نظرة عامة على العمليات و سلاسل التعليمات.

يحدث خطأ ANR في تطبيقك عند استيفاء أحد الشروط التالية:

  • انتهاء مهلة إرسال الإدخال: إذا لم يستجب تطبيقك لحدث إدخال (مثل الضغط على مفتاح أو لمس الشاشة) في غضون 5 ثوانٍ.
  • تنفيذ خدمة: إذا تعذّر على خدمة يعلن عنها تطبيقك إكمال تنفيذ Service.onCreate() و Service.onStartCommand()/Service.onBind() في غضون بضع ثوانٍ.
  • **Service.startForeground() لم يتم استدعاؤها**: إذا كان تطبيقك يستخدم Context.startForegroundService() لبدء خدمة جديدة في المقدّمة ولكن لم تستدعِ الخدمة startForeground() في غضون 5 ثوانٍ.
  • بثّ الغرض: إذا لم يكتمل تنفيذ BroadcastReceiver في غضون فترة زمنية محدّدة. إذا كان لدى التطبيق أي نشاط في المقدّمة، تكون مهلة الانتظار هذه 5 ثوانٍ.
  • **JobScheduler تفاعلات**: إذا لم يتم عرض قيمة من JobService JobService.onStartJob()JobService.onStopJob() في غضون بضع ثوانٍ، أو إذا بدأت مهمة بدأها المستخدم ولم يستدعِ تطبيقك JobService.setNotification() في غضون بضع ثوانٍ بعد استدعاء JobService.onStartJob(). بالنسبة إلى التطبيقات التي تستهدف الإصدار Android 13 والإصدارات الأقدم، تكون أخطاء ANR غير مرئية ولا يتم إبلاغ التطبيق بها. أما بالنسبة إلى التطبيقات التي تستهدف الإصدار Android 14 والإصدارات الأحدث، فتكون أخطاء ANR واضحة ويتم إبلاغ التطبيق بها.

إذا كان تطبيقك يعاني من أخطاء ANR، يمكنك استخدام الإرشادات الواردة في هذه المقالة لتشخيص المشكلة وحلّها.

حلّ المشاكل

بعد تحديد المشكلة، يمكنك استخدام النصائح الواردة في هذا القسم لحلّ المشاكل الشائعة.

رمز بطيء على سلسلة التعليمات الرئيسية

حدِّد المواضع في الرمز البرمجي التي تكون فيها سلسلة التعليمات الرئيسية في التطبيق مشغولة لأكثر من 5 ثوانٍ. ابحث عن حالات الاستخدام المشبوهة في تطبيقك وحاوِل إعادة إنتاج خطأ ANR.

على سبيل المثال، يعرض الشكل 2 مخططًا زمنيًا في Traceview تكون فيه سلسلة التعليمات الرئيسية مشغولة لأكثر من 5 ثوانٍ.

الشكل 2: المخطط الزمني في Traceview الذي يعرض سلسلة محادثات رئيسية مشغولة

الشكل 2. مخطط زمني في Traceview يعرض سلسلة التعليمات الرئيسية المشغولة

يوضّح الشكل 2 أنّ معظم الرمز البرمجي الذي يسبب المشكلة يحدث في الـ onClick(View)، كما هو موضّح في مثال الرمز البرمجي التالي:

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

في هذه الحالة، عليك نقل العمل الذي يتم تنفيذه في سلسلة التعليمات الرئيسية إلى سلسلة تعليمات عاملة. يتضمّن إطار عمل Android فئات يمكن أن تساعد في نقل المهمة إلى سلسلة تعليمات عاملة. لمزيد من المعلومات، يُرجى الاطّلاع على سلاسل التعليمات العاملة.

وحدات الإدخال والإخراج في سلسلة التعليمات الرئيسية

إنّ تنفيذ عمليات الإدخال والإخراج على سلسلة التعليمات الرئيسية هو سبب شائع للعمليات البطيئة على سلسلة التعليمات الرئيسية، ما قد يؤدي إلى حدوث أخطاء ANR. في Compose، غالبًا ما يفعّل المطوّرون عن غير قصد عمليات قراءة من القرص (مثل SharedPreferences أو طلبات قاعدة البيانات) أثناء محاولة استنتاج الحالة الأولية.

نفِّذ عمليات الإدخال والإخراج الطويلة بعيدًا عن طبقة واجهة المستخدم. استخدِم withContext(Dispatchers.IO) في ViewModel أو، والأفضل من ذلك، استخدِم مستودعًا في طبقة البيانات. يُنصح بنقل جميع عمليات الإدخال والإخراج إلى سلسلة تعليمات عاملة، كما هو موضّح في القسم السابق.

من أمثلة عمليات الإدخال والإخراج عمليات الشبكة والتخزين. لمزيد من المعلومات، يُرجى الاطّلاع على تنفيذ عمليات الشبكة وحفظ البيانات.

تزايد الطلب على دالة الاستبعاد المتبادل

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

على سبيل المثال، يعرض الشكل 3 مخططًا زمنيًا في Traceview يتم فيه تنفيذ معظم العمل على سلسلة الوحدات العاملة (worker thread).

الشكل 3 المخطط الزمني في Traceview الذي يعرض العمل الذي يتم تنفيذه على سلسلة
تعليمات عاملة

الشكل 3. مخطط زمني في Traceview يعرض العمل الذي يتم تنفيذه على سلسلة تعليمات عاملة

إذا كان المستخدمون لا يزالون يواجهون أخطاء ANR، عليك الاطّلاع على حالة سلسلة التعليمات الرئيسية في Android Device Monitor. عادةً ما تكون سلسلة التعليمات الرئيسية في حالة RUNNABLE إذا كانت جاهزة لتعديل واجهة المستخدم وتستجيب بشكل عام.

إذا تعذّر على سلسلة التعليمات الرئيسية استئناف التنفيذ، تكون في حالة BLOCKED ولا يمكنها الاستجابة للأحداث. تظهر الحالة في Android Device Monitor على أنّها Monitor أو Wait، كما هو موضّح في الشكل 5.

الشكل 4. سلسلة التعليمات الرئيسية في حالة Monitor

الشكل 4. سلسلة التعليمات الرئيسية في حالة "مراقبة"

يعرض التتبُّع التالي سلسلة التعليمات الرئيسية في أحد التطبيقات التي تم حظرها أثناء انتظار مورد:

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

يمكن أن يساعدك مراجعة التتبُّع في تحديد الرمز البرمجي الذي يحظر سلسلة التعليمات الرئيسية. الرمز البرمجي التالي مسؤول عن الاحتفاظ بدالة الاستبعاد المتبادل التي تحظر سلسلة التعليمات الرئيسية في التتبُّع السابق:

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

    synchronized(lockedResource) {
        // The main thread requires lockedResource here
        // but it has to wait until LockTask finishes using it.
    }
}

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
  new LockTask().execute(data);

  synchronized (lockedResource) {
      // The main thread requires lockedResource here
      // but it has to wait until LockTask finishes using it.
  }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
  @Override
  protected Long doInBackground(Integer[]... params) {
      synchronized (lockedResource) {
          // This is a long-running operation, which makes
          // the lock last for a long time
          BubbleSort.sort(params[0]);
      }
  }
}

مثال آخر هو سلسلة التعليمات الرئيسية في أحد التطبيقات التي تنتظر نتيجة من سلسلة الوحدات العاملة (worker thread)، كما هو موضّح في الرمز البرمجي التالي. يُرجى العِلم أنّ استخدام wait() وnotify() ليس نمطًا مقترَحًا في Kotlin، التي تتضمّن آليات خاصة للتعامل مع التزامن. عند استخدام Kotlin، عليك استخدام الآليات الخاصة بلغة Kotlin إن أمكن.

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread's notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

public void onClick(View v) {
  WaitTask waitTask = new WaitTask();
  synchronized (waitTask) {
      try {
          waitTask.execute(data);
          // Wait for this worker thread’s notification
          waitTask.wait();
      } catch (InterruptedException e) {}
  }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
  @Override
  protected Long doInBackground(Integer[]... params) {
      synchronized (this) {
          BubbleSort.sort(params[0]);
          // Finished, notify the main thread
          notify();
      }
  }
}

هناك بعض الحالات الأخرى التي يمكن أن تحظر سلسلة التعليمات الرئيسية، بما في ذلك سلاسل التعليمات التي تستخدم Lock وSemaphore، بالإضافة إلى مجموعة موارد (مثل مجموعة من اتصالات قاعدة البيانات) أو آليات أخرى للاستبعاد المتبادل (mutex) .

عليك تقييم دوال الاستبعاد المتبادل التي يحتفظ بها تطبيقك على الموارد بشكل عام، ولكن إذا كنت تريد تجنُّب أخطاء ANR، عليك الاطّلاع على دوال الاستبعاد المتبادل المحتفظ بها للموارد التي تحتاجها سلسلة التعليمات الرئيسية.

تأكَّد من الاحتفاظ بدوال الاستبعاد المتبادل لأقل فترة زمنية ممكنة، أو الأفضل من ذلك، قيِّم ما إذا كان التطبيق يحتاج إلى الاحتفاظ بها في المقام الأول. إذا كنت تستخدم دالة الاستبعاد المتبادل لتحديد وقت تعديل واجهة المستخدم استنادًا إلى معالجة سلسلة تعليمات عاملة، استخدِم آليات مثل onProgressUpdate() و onPostExecute() للتواصل بين سلسلة التعليمات العاملة وسلسلات التعليمات الرئيسية.

مستقبِلات البث البطيئة

يمكن للتطبيقات الاستجابة لرسائل البث، مثل تفعيل وضع الطائرة أو إيقافه أو تغيير حالة الاتصال، من خلال مستقبِلات البث. يحدث خطأ ANR عندما يستغرِق التطبيق وقتًا طويلاً جدًا لمعالجة الإعلان على جميع الأجهزة.

يحدث خطأ ANR في الحالات التالية:

  • لم يكتمل تنفيذ طريقة onReceive لمستقبِل البث في غضون فترة زمنية كبيرة.
  • يستدعي مستقبِل البث goAsync ويتعذّر عليه استدعاء finish على عنصر PendingResult.

يجب أن ينفّذ تطبيقك عمليات قصيرة فقط في طريقة onReceive الخاصة بـ BroadcastReceiver. ومع ذلك، إذا كان تطبيقك يتطلّب معالجة أكثر تعقيدًا نتيجةً لرسالة بث، عليك تأجيل المهمة إلى ViewModel (الاستفادة من إمكانات الروتينات الفرعية والنطاقات والموزّعات في Kotlin) إذا كان من المتوقّع أن تستغرِق المهمة بضع ثوانٍ على الأكثر، أو إلى أي نوع من أدوات الاحتفاظ بالحالة، أو إلى WorkManager للمهام التي من المتوقّع أن تستغرِق أكثر من بضع ثوانٍ.

يمكنك استخدام أدوات مثل Traceview لتحديد ما إذا كان مستقبِل البث ينفّذ عمليات طويلة على سلسلة التعليمات الرئيسية في التطبيق. على سبيل المثال، يعرض الشكل 6 المخطط الزمني لمستقبِل بث يعالج رسالة على سلسلة التعليمات الرئيسية لمدة 100 ثانية تقريبًا.

الشكل 5. المخطّط الزمني في Traceview الذي يعرض عمل BroadcastReceiver على سلسلة التعليمات الرئيسية

الشكل 5. مخطط زمني في Traceview يعرض عمل BroadcastReceiver على سلسلة التعليمات الرئيسية

يمكن أن يحدث هذا السلوك بسبب تنفيذ عمليات طويلة في طريقة onReceive() الخاصة بـ BroadcastReceiver، كما هو موضّح في المثال التالي:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

في حالات مثل هذه، يُنصح بنقل العملية الطويلة إلى IntentService لأنّها تستخدم سلسلة الوحدات العاملة (worker thread) لتنفيذ عملها. يوضّح الرمز البرمجي التالي كيفية استخدام IntentService لمعالجة عملية طويلة:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
  @Override
  protected void onHandleIntent(@Nullable Intent intent) {
      BubbleSort.sort(data);
  }
}

نتيجةً لاستخدام IntentService، يتم تنفيذ العملية الطويلة على سلسلة تعليمات عاملة بدلاً من سلسلة التعليمات الرئيسية. يعرض الشكل 7 العمل المؤجّل إلى سلسلة التعليمات العاملة في المخطط الزمني في Traceview.

الشكل 6. المخطّط الزمني في Traceview الذي يعرض معالجة الإعلان على جميع الأجهزة على سلسلة محادثات عاملة

الشكل 6. مخطط زمني في Traceview يعرض رسالة البث التي تتم معالجتها على سلسلة تعليمات عاملة

يمكن أن يستخدم مستقبِل البث goAsync() للإشارة إلى النظام بأنّه يحتاج إلى مزيد من الوقت لمعالجة الرسالة. ومع ذلك، عليك استدعاء finish() على عنصر PendingResult. يوضّح المثال التالي كيفية استدعاء finish() للسماح للنظام بإعادة استخدام مستقبِل البث وتجنُّب حدوث خطأ ANR:

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
  @Override
  protected Long doInBackground(Integer[]... params) {
      // This is a long-running operation
      BubbleSort.sort(params[0]);
      pendingResult.finish();
  }
}.execute(data);

ومع ذلك، لن يؤدي نقل الرمز البرمجي من مستقبِل بث بطيء إلى سلسلة تعليمات أخرى و استخدام goAsync() إلى حلّ خطأ ANR إذا كان البث في الخلفية. لا تزال مهلة الانتظار لخطأ ANR سارية.