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

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

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

کادر محاوره‌ای ANR به کاربر نمایش داده می‌شود.
شکل ۱. پنجره ANR که به کاربر نمایش داده می‌شود

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

زمانی که یکی از شرایط زیر رخ دهد، یک ANR برای برنامه شما فعال می‌شود:

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

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

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

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

کند بودن کد در نخ اصلی

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

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

شکل 2. جدول زمانی Traceview که یک نخ اصلی مشغول را نشان می‌دهد

شکل 2. جدول زمانی 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);
}

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

ورودی/خروجی روی نخ اصلی

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

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

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

مشاجره قفل

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

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

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

شکل 3. جدول زمانی Traceview که کار در حال اجرا روی یک نخ کارگر را نشان می‌دهد

اما اگر کاربران شما هنوز با ANR مواجه هستند، باید وضعیت thread اصلی را در Android Device Monitor بررسی کنید. معمولاً اگر thread اصلی آماده به‌روزرسانی رابط کاربری باشد و به‌طورکلی پاسخگو باشد، در حالت RUNNABLE قرار دارد.

اما اگر نخ اصلی نتواند اجرا را از سر بگیرد، در حالت BLOCKED قرار می‌گیرد و نمی‌تواند به رویدادها پاسخ دهد. این وضعیت در مانیتور دستگاه اندروید به صورت Monitor یا Wait نشان داده می‌شود، همانطور که در شکل 5 نشان داده شده است.

شکل ۴. رشته اصلی در وضعیت مانیتور

شکل ۴. رشته اصلی در وضعیت مانیتور

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

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

کاتلین

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

گیرنده‌های پخش کند

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

ANR در موارد زیر رخ می‌دهد:

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

برنامه شما فقط باید عملیات کوتاه را در متد onReceive از BroadcastReceiver انجام دهد. با این حال، اگر برنامه شما به دلیل یک پیام پخش به پردازش پیچیده‌تری نیاز دارد، باید اگر انتظار می‌رود که این کار حداکثر چند ثانیه طول بکشد، آن را به یک ViewModel (با استفاده از قدرت Coroutineها، Scopeها و dispatchersهای کاتلین) یا هر نوع نگهدارنده وضعیت (state holder) یا به WorkManager برای کارهایی که انتظار می‌رود بیش از چند ثانیه طول بکشد، موکول کنید.

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

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

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

شکل ۶. جدول زمانی Traceview که پیام پخش پردازش شده در یک نخ کارگر را نشان می‌دهد

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