מקרי ANR (תצוגות)

מושגים ויישום ב-Jetpack Compose

כששרשור ה-UI של אפליקציית Android נחסם למשך זמן ארוך מדי, מופעלת שגיאת ANR (האפליקציה לא מגיבה). אם האפליקציה פועלת בחזית, המערכת מציגה למשתמש תיבת דו-שיח, כמו שמוצג באיור 1. בתיבת הדו-שיח של ה-ANR, המשתמש יכול לבצע יציאה ידנית מהאפליקציה.

תיבת דו-שיח של ANR מוצגת למשתמש.
איור 1. תיבת דו-שיח של ANR שמוצגת למשתמש

שגיאות ANR הן בעיה כי השרשור הראשי של האפליקציה, שאחראי לעדכון ממשק המשתמש, לא יכול לעבד אירועי קלט של משתמשים או לצייר, וזה גורם לתסכול בקרב המשתמשים. מידע נוסף על השרשור הראשי של האפליקציה זמין במאמר סקירה כללית על תהליכים ושרשורים.

אירוע ANR מופעל באפליקציה כשמתקיים אחד מהתנאים הבאים:

  • פסק זמן בשליחת קלט: אם האפליקציה לא הגיבה לאירוע קלט (כמו לחיצה על מקש או נגיעה במסך) תוך 5 שניות.
  • הפעלת שירות: אם שירות שהוגדר על ידי האפליקציה לא יכול לסיים את ההפעלה Service.onCreate() וService.onStartCommand()/Service.onBind() תוך כמה שניות.
  • **Service.startForeground() not called**: אם האפליקציה משתמשת ב-Context.startForegroundService() כדי להפעיל שירות חדש בחזית, אבל השירות לא קורא ל-startForeground() תוך 5 שניות.
  • שידור של כוונה: אם BroadcastReceiver לא סיים את ההפעלה בתוך פרק זמן מוגדר. אם לאפליקציה יש פעילות בחזית, הזמן הקצוב לתפוגה הוא 5 שניות.
  • **JobScheduler אינטראקציות**: אם JobService לא חוזר מ-JobService.onStartJob() או מ-JobService.onStopJob() תוך כמה שניות, או אם מתחיל תהליך שהמשתמש יזם והאפליקציה לא קוראת ל-JobService.setNotification() תוך כמה שניות אחרי שבוצעה קריאה ל-JobService.onStartJob(). באפליקציות שמטרגטות ל-Android 13 ומטה, מקרי ה-ANR לא מוצגים למשתמשים ולא מדווחים לאפליקציה. באפליקציות שמטרגטות ל-Android 14 ומעלה, מקרי ה-ANR מוצגים למשתמשים ומדווחים לאפליקציה.

אם האפליקציה נתקלת בבעיות ANR, אפשר להיעזר בהנחיות שבמאמר הזה כדי לאבחן את הבעיה ולפתור אותה.

פתרון הבעיות

אחרי שזיהיתם את הבעיה, תוכלו להיעזר בטיפים שבקטע הזה כדי לפתור בעיות נפוצות.

קוד איטי ב-thread הראשי

מזהים את המקומות בקוד שבהם השרשור הראשי של האפליקציה עסוק יותר מ-5 שניות. מחפשים באפליקציה תרחישי שימוש חשודים ומנסים לשחזר את ה-ANR.

לדוגמה, באיור 2 מוצג ציר זמן של Traceview שבו השרשור הראשי עסוק במשך יותר מ-5 שניות.

איור 2. ציר הזמן של Traceview שמציג שרשור ראשי עמוס

איור 2. ציר הזמן של Traceview שמציג שרשור ראשי עמוס

באיור 2 אפשר לראות שרוב הקוד הבעייתי נמצא ב-handler‏ 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);
}

במקרה כזה, צריך להעביר את העבודה שמתבצעת ב-thread הראשי ל-worker thread. ה-Framework של Android כולל מחלקות שיכולות לעזור להעביר את המשימה ל-Thread עובד. מידע נוסף זמין במאמר בנושא ת'רדים של Worker.

קלט/פלט (I/O) ב-thread הראשי

ביצוע פעולות קלט/פלט (I/O) ב-thread הראשי היא סיבה נפוצה לפעולות איטיות ב-thread הראשי, שיכולות לגרום לשגיאות ANR. ב-Compose, מפתחים מפעילים בטעות קריאות דיסק (כמו SharedPreferences או קריאות מסד נתונים) בזמן שהם מנסים לגזור את המצב הראשוני.

צריך להריץ פעולות קלט/פלט (I/O) ממושכות מחוץ לשכבת ממשק המשתמש. כדאי להשתמש ב-withContext(Dispatchers.IO) ב-ViewModel או, עוד יותר טוב, להשתמש במאגר בשכבת הנתונים. מומלץ להעביר את כל פעולות הקלט/פלט ל-worker thread, כמו שמוצג בקטע הקודם.

דוגמאות לפעולות קלט/פלט הן פעולות ברשת ובאחסון. מידע נוסף זמין במאמרים בנושא ביצוע פעולות ברשת ושמירת נתונים.

תחרות על נעילה

במקרים מסוימים, הפעולה שגורמת ל-ANR לא מבוצעת ישירות בשרשור הראשי של האפליקציה. אם Thread עובד מחזיק נעילה במשאב שנדרש ל-thread הראשי כדי להשלים את העבודה שלו, יכול להיות שתתרחש שגיאת ANR.

לדוגמה, באיור 3 מוצג ציר זמן של Traceview שבו רוב העבודה מתבצעת ב-Thread עובד.

איור 3. ציר זמן של Traceview שבו מוצגת העבודה שמתבצעת ב-worker
thread

איור 3. ציר זמן של Traceview שבו מוצגת העבודה שמתבצעת ב-worker thread

אבל אם המשתמשים שלכם עדיין חווים ANR, כדאי לבדוק את הסטטוס של השרשור הראשי ב-Android Device Monitor. בדרך כלל, השרשור הראשי נמצא במצב RUNNABLE אם הוא מוכן לעדכן את ממשק המשתמש ומגיב בדרך כלל.

אבל אם השרשור הראשי לא יכול להמשיך את הביצוע, הוא נמצא במצב BLOCKED ולא יכול להגיב לאירועים. המצב מוצג ב-Android Device Monitor בתור Monitor או Wait, כמו שמוצג באיור 5.

איור 4. ה-thread הראשי בסטטוס של Monitor

איור 4. ה-thread הראשי בסטטוס של Monitor

במעקב הבא מוצג ה-thread הראשי של אפליקציה שחסום בהמתנה למשאב:

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

בדיקת ה-trace יכולה לעזור לכם לאתר את הקוד שחוסם את ה-thread הראשי. הקוד הבא אחראי להחזקת הנעילה שחוסמת את השרשור הראשי במעקב הקודם:

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

דוגמה נוספת היא ה-thread הראשי של אפליקציה שממתין לתוצאה מ-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, כדאי לבדוק את הנעילות שמתבצעות על משאבים שנדרשים על ידי השרשור הראשי.

חשוב לוודא שהנעילות מוחזקות למשך הזמן הקצר ביותר, או אפילו טוב יותר, לבדוק אם האפליקציה צריכה את ההחזקה מלכתחילה. אם אתם משתמשים בנעילה כדי לקבוע מתי לעדכן את ממשק המשתמש על סמך העיבוד של Thread עובד, השתמשו במנגנונים כמו onProgressUpdate() ו-onPostExecute() כדי ליצור תקשורת בין ה-Thread העובד ל-Thread הראשי.

מקלט שידורים איטי

אפליקציות יכולות להגיב להודעות שידור, כמו הפעלה או השבתה של מצב טיסה או שינוי בסטטוס הקישוריות, באמצעות מקלטי שידור. שגיאת 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 כי הוא משתמש ב-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, הפעולה ארוכת הטווח מבוצעת ב-Thread עובד במקום ב-thread הראשי. איור 7 מציג את העבודה שנדחתה ל-Thread העובד בציר הזמן של Traceview.

איור 6. ציר הזמן של Traceview שבו מוצגת הודעת השידור שעברה עיבוד בשרשור של worker

איור 6. ציר הזמן של Traceview שבו מוצגת הודעת השידור שעברה עיבוד בשרשור של worker

מקלט השידור יכול להשתמש ב-goAsync() כדי לסמן למערכת שהוא צריך עוד זמן לעיבוד ההודעה. עם זאת, צריך לבצע קריאה ל-method‏ 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);

עם זאת, העברת הקוד מ-מקלט שידורים איטי ל-thread אחר ושימוש ב-goAsync() לא יפתרו את הבעיה אם ה-broadcast פועל ברקע. ההמתנה עד לזמן הקצוב לתגובה (timeout) של ANR עדיין חלה.