أخطاء 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 خلال فترة زمنية محدّدة إذا كان التطبيق يتضمّن أي نشاط في المقدّمة، تكون مهلة الانتظار هذه 10 ثوانٍ.
  • تفاعلات **JobScheduler**: إذا لم يتم عرض JobService من JobService.onStartJob() أو JobService.onStopJob() في غضون بضع ثوانٍ، أو إذا بدأت مهمة بدأها المستخدم ولم يستدعِ تطبيقك JobService.setNotification() في غضون بضع ثوانٍ بعد استدعاء JobService.onStartJob(). بالنسبة إلى التطبيقات التي تستهدف الإصدار 13 من نظام التشغيل Android والإصدارات الأقدم، تكون أخطاء ANR صامتة ولا يتم إبلاغ التطبيق بها. أما بالنسبة إلى التطبيقات التي تستهدف الإصدار 14 من نظام التشغيل Android والإصدارات الأحدث، فتكون أخطاء 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);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

الشكل 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

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، بالإضافة إلى مجموعة الموارد (مثل مجموعة من اتصالات قاعدة البيانات) أو آليات الاستبعاد المتبادل الأخرى.

يجب تقييم عمليات القفل التي يحتفظ بها تطبيقك على الموارد بشكل عام، ولكن إذا أردت تجنُّب أخطاء 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، يتم تنفيذ العملية الطويلة على سلسلة الوحدات العاملة (worker thread) بدلاً من سلسلة الوحدات الرئيسية. يعرض الشكل 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 سارية.