خطاهای ANR (بازدیدها)

مفاهیم و پیاده‌سازی Jetpack Compose

وقتی رشته میانای کاربر برنامه Android برای مدت طولانی مسدود شود، خطای «برنامه پاسخ نمی‌دهد» (ANR) راه‌اندازی می‌شود. اگر برنامه در پیش‌زمینه باشد، سیستم کادر گفتگویی را به کاربر نشان می‌دهد، همان‌طور که در شکل ۱ نشان داده شده است. کادر گفتگوی ANR به کاربر این امکان را می‌دهد که برنامه را به‌اجبار ببندد.

کادر گفتگوی ANR به کاربر نشان داده شد.
شکل ۱. کادر گفتگوی ANR به کاربر نشان داده شد

خطاهای «برنامه پاسخ نمی‌دهد» مشکل‌ساز هستند زیرا رشته اصلی برنامه که مسئول به‌روزرسانی واسط کاربر است نمی‌تواند رویدادهای ورودی کاربر را پردازش کند یا ترسیم کند، که این امر باعث نارضایتی کاربر می‌شود. برای اطلاعات بیشتر درباره رشته اصلی برنامه، به نمای کلی فرایندها و رشته‌ها مراجعه کنید.

وقتی یکی از شرایط زیر رخ دهد، ANR برای برنامه‌تان راه‌اندازی می‌شود:

  • زمان ارسال ورودی تمام شده است: اگر برنامه شما ظرف ۵ ثانیه به رویداد ورودی (مثل فشار دادن کلید یا لمس کردن صفحه) پاسخ نداده باشد.
  • اجرای سرویس: اگر سرویسی که توسط برنامه شما اعلام شده است نتواند اجرای Service.onCreate() و Service.onStartCommand()/Service.onBind() را ظرف چند ثانیه به‌پایان برساند.
  • **Service.startForeground() فراخوانی نشده است**: اگر برنامه‌تان از Context.startForegroundService() برای شروع سرویس جدیدی در پیش‌زمینه استفاده می‌کند اما سرویس startForeground() را ظرف ۵ ثانیه فراخوانی نمی‌کند.
  • پخش هدف: اگر BroadcastReceiver در مدت زمان تعیین‌شده اجرا نشود. اگر برنامه فعالیتی در پیش‌زمینه داشته باشد، این مهلت زمانی ۱۰ ثانیه است.
  • **JobScheduler تعامل**: اگر JobService از JobService.onStartJob() یا JobService.onStopJob() ظرف چند ثانیه برنگردد، یا اگر کار شروع‌شده توسط کاربر شروع شود و برنامه شما JobService.setNotification() را ظرف چند ثانیه پس‌از فراخوانی JobService.onStartJob() فراخوانی نکند. برای برنامه‌هایی که Android 13 و نسخه‌های پایین‌تر را هدف‌یابی می‌کنند، ANRها بی‌صدا هستند و به برنامه گزارش نمی‌شوند. برای برنامه‌هایی که Android 14 و نسخه‌های بالاتر را هدف‌یابی می‌کنند، ANRها صریح هستند و به برنامه گزارش می‌شوند.

اگر برنامه‌تان با خطای «برنامه پاسخ نمی‌دهد» مواجه است، می‌توانید از راهنمایی‌های این مقاله برای تشخیص و رفع مشکل استفاده کنید.

برطرف کردن مشکلات

پس‌از شناسایی مشکل، می‌توانید از نکته‌های این بخش برای رفع مشکلات رایج استفاده کنید.

کد کند در رشته اصلی

مکان‌هایی را در کدتان شناسایی کنید که رشته اصلی برنامه برای بیش‌از ۵ ثانیه مشغول است. موارد استفاده مشکوک را در برنامه‌تان پیدا کنید و سعی کنید ANR را بازتولید کنید.

برای مثال، شکل ۲ خط زمان Traceview را نشان می‌دهد که در آن رشته اصلی برای بیش‌از ۵ ثانیه مشغول است.

شکل ۲.‏ خط زمان Traceview که رشته اصلی مشغول را نشان می‌دهد

شکل ۲. خط زمان Traceview که رشته اصلی مشغول را نشان می‌دهد

شکل ۲ نشان می‌دهد که بیشتر کد متخلف در گرداننده onClick(View) اتفاق می‌افتد، همان‌طور که در نمونه کد زیر نشان داده شده است:

کاتلین

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

جاوا

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

در این حالت، باید کاری را که در رشته اصلی اجرا می‌شود به رشته عامل منتقل کنید. «چارچوب Android» شامل کلاس‌هایی است که می‌توانند به انتقال تکلیف به رشته عامل کمک کنند. برای اطلاعات بیشتر، به رشته‌های عامل مراجعه کنید.

ورودی/خروجی در رشته اصلی

اجرای عملیات ورودی/خروجی در رشته اصلی یکی از دلایل رایج عملکرد آهسته در رشته اصلی است که می‌تواند موجب بروز خطای ANR شود. در Compose، توسعه‌دهندگان اغلب به‌طور تصادفی خواندن دیسک را (مانند SharedPreferences یا فراخوانی‌های پایگاه داده) هنگام تلاش برای استخراج وضعیت اولیه فعال می‌کنند.

عملیات ورودی/خروجی طولانی‌مدت را دور از لایه واسط کاربر اجرا کنید. از withContext(Dispatchers.IO) در ViewModel استفاده کنید یا، حتی بهتر، از «مخزن» در لایه داده استفاده کنید. توصیه می‌شود همه عملیات IO به رشته عامل منتقل شود، همان‌طور که در بخش قبلی نشان داده شده است.

برخی‌از نمونه‌های عملیات IO عبارت‌اند از عملیات شبکه و فضای ذخیره‌سازی. برای اطلاعات بیشتر، به انجام عملیات شبکه و ذخیره داده‌ها مراجعه کنید.

درگیری بر سر قفل

در برخی سناریوها، کاری که باعث ANR می‌شود مستقیماً در رشته اصلی برنامه اجرا نمی‌شود. اگر رشته عامل قفل منبعی را نگه دارد که رشته اصلی برای تکمیل کارش به آن نیاز دارد، ممکن است خطای ANR رخ دهد.

برای مثال، شکل ۳ خط زمان Traceview را نشان می‌دهد که در آن بیشتر کارها در رشته عامل انجام می‌شود.

شکل ۳. خط زمان Traceview که کار درحال اجرا روی رشته عامل را نشان می‌دهد

شکل ۳. خط زمان Traceview که کار درحال اجرا روی رشته عامل را نشان می‌دهد

اما اگر کاربران شما همچنان با ANR مواجه می‌شوند، باید وضعیت رشته اصلی را در «پایشگر دستگاه Android» بررسی کنید. معمولاً رشته اصلی در وضعیت RUNNABLE است اگر آماده به‌روزرسانی واسط کاربر باشد و به‌طور کلی پاسخگو باشد.

اما اگر رشته اصلی نتواند اجرا را ازسر بگیرد، در وضعیت BLOCKED قرار می‌گیرد و نمی‌تواند به رویدادها پاسخ دهد. وضعیت در «نمایشگر دستگاه Android» به‌صورت نمایشگر یا انتظار نشان داده می‌شود، همان‌طور که در شکل ۵ نشان داده شده است.

شکل ۴. رشته اصلی در وضعیت
«پایشگر»

شکل ۴. رشته اصلی در وضعیت «پایش»

ردگیری زیر رشته اصلی برنامه‌ای را نشان می‌دهد که به‌دلیل انتظار برای یک منبع مسدود شده است:

...
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)
...

بررسی ردگیری می‌تواند به شما کمک کند کدی را که رشته اصلی را مسدود می‌کند پیدا کنید. کد زیر مسئول نگه‌داشتن قفلی است که رشته اصلی را در ردگیری قبلی مسدود می‌کند:

کاتلین

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])
            }
}

جاوا

@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]);
      }
  }
}

مثال دیگر رشته اصلی برنامه است که منتظر نتیجه‌ای از رشته عامل است، همان‌طور که در کد زیر نشان داده شده است. توجه داشته باشید که استفاده از 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()
        }
    }
}

جاوا

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() برای برقراری ارتباط بین رشته عامل و رشته اصلی استفاده کنید.

گیرنده‌های رویداد ثبت‌شده کند

برنامه‌ها می‌توانند ازطریق گیرنده‌های رویداد ثبت‌شده به پیام‌های همه‌فرستی پاسخ دهند، مثلاً فعال یا غیرفعال کردن حالت هواپیما یا تغییر در وضعیت اتصال‌پذیری. وقتی برنامه برای پردازش پیام همه‌فرستی بیش‌ازحد طول بکشد، خطای «برنامه پاسخ نمی‌دهد» رخ می‌دهد.

خطای «برنامه پاسخ نمی‌دهد» در موارد زیر رخ می‌دهد:

  • گیرنده رویداد ثبت‌شده اجرای روش onReceive خود را در مدت زمان قابل‌توجهی به‌پایان نرسانده است.
  • گیرنده رویداد ثبت‌شده goAsync را فراخوانی می‌کند و نمی‌تواند finish را در شیء PendingResult فراخوانی کند.

برنامه شما باید فقط عملیات کوتاه را در روش onReceive BroadcastReceiver انجام دهد. بااین‌حال، اگر برنامه‌تان درنتیجه پیام همه‌فرستی به پردازش پیچیده‌تری نیاز دارد، باید تکلیف را به ViewModel (با بهره‌گیری از قدرت کوروتین‌ها، دامنه‌ها، و توزیع‌کننده‌های Kotlin) واگذار کنید اگر انتظار می‌رود تکلیف حداکثر چند ثانیه طول بکشد، هر نوع نگه‌دارنده حالت، یا به WorkManager برای تکالیفی که انتظار می‌رود بیشتر از چند ثانیه طول بکشد.

می‌توانید از ابزارهایی مانند Traceview برای شناسایی اینکه آیا گیرنده رویداد ثبت‌شده شما عملیات طولانی‌مدت را در رشته اصلی برنامه اجرا می‌کند یا نه استفاده کنید. برای مثال، شکل ۶ خط زمان گیرنده رویداد ثبت‌شده‌ای را نشان می‌دهد که پیامی را در رشته اصلی برای حدود ۱۰۰ ثانیه پردازش می‌کند.

شکل ۵. خط زمان Traceview که کار `BroadcastReceiver` را در رشته اصلی نشان می‌دهد

شکل ۵. خط زمان Traceview که کار BroadcastReceiver را در رشته اصلی نشان می‌دهد

این رفتار می‌تواند ناشی از اجرای عملیات طولانی‌مدت در روش onReceive() از BroadcastReceiver باشد، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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

در چنین شرایطی، توصیه می‌شود عملیات طولانی‌مدت را به IntentService منتقل کنید زیرا از رشته عامل برای اجرای کارش استفاده می‌کند. کد زیر نحوه استفاده از IntentService را برای پردازش عملیات طولانی‌مدت نشان می‌دهد:

کاتلین

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

جاوا

@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، عملیات طولانی‌مدت به‌جای رشته اصلی در رشته عامل اجرا می‌شود. شکل ۷ کار به‌تعویق‌افتاده به رشته عامل را در خط زمان Traceview نشان می‌دهد.

شکل ۶. خط زمان Traceview که پیام همه‌فرستی پردازش‌شده در رشته عامل را نشان می‌دهد

شکل ۶. خط زمان Traceview که پیام همه‌فرستی پردازش‌شده در رشته عامل را نشان می‌دهد

گیرنده رویداد ثبت‌شده شما می‌تواند از goAsync() استفاده کند تا به سیستم علامت دهد که به زمان بیشتری برای پردازش پیام نیاز دارد. اما باید finish() را در شیء PendingResult فراخوانی کنید. مثال زیر نشان می‌دهد که چگونه finish() را فراخوانی کنید تا به سیستم اجازه دهید گیرنده رویداد ثبت‌شده را بازیافت کند و از ANR جلوگیری کند:

کاتلین

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)

جاوا

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 همچنان اعمال می‌شود.