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

מושגים והטמעה של Jetpack Compose

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

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

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

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

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

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

איך פותרים את הבעיות

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

איור 4. השרשור הראשי במצב מעקב

המעקב הבא מציג את ה-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)
...

בדיקת המעקב יכולה לעזור לכם לאתר את הקוד שחוסם את ה-thread הראשי. הקוד הבא אחראי להחזקת הנעילה שחוסמת את ה-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();
      }
  }
}

יש מצבים אחרים שיכולים לחסום את ה-thread הראשי, כולל threads שמשתמשים ב-Lock, ב-Semaphore, במאגר משאבים (כמו מאגר של חיבורים למסד נתונים) או במנגנונים אחרים של הדרה הדדית (mutex).

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

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

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

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

שגיאת ANR מתרחשת במקרים הבאים:

  • מקלט שידור לא סיים להפעיל את השיטה onReceive שלו בפרק זמן סביר.
  • מקלט שידור קורא ל-goAsync ולא מצליח לקרוא ל-finish באובייקט PendingResult.

האפליקציה צריכה לבצע רק פעולות קצרות ב-method‏ onReceive של BroadcastReceiver. עם זאת, אם האפליקציה דורשת עיבוד מורכב יותר כתוצאה מהודעה לכולם, צריך לדחות את המשימה ל-ViewModel (תוך ניצול היכולות של פונקציות ה-coroutine, הסקופים והדיספצ'רים של Kotlin) אם המשימה צפויה להימשך כמה שניות לכל היותר, לכל סוג של מאחסן מצב, או ל-WorkManager למשימות שצפויות להימשך יותר מכמה שניות.

אתם יכולים להשתמש בכלים כמו Traceview כדי לזהות אם מקלט השידור מבצע פעולות ארוכות בשרשור הראשי של האפליקציה. לדוגמה, באיור 6 מוצג ציר הזמן של מקלט שידור שמעבד הודעה ב-thread הראשי במשך כ-100 שניות.

איור 5. ציר הזמן של Traceview שמציג את העבודה של `BroadcastReceiver` בשרשור הראשי

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

ההתנהגות הזו יכולה להיגרם מהרצת פעולות ממושכות ב-method‏ 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 שבו מוצגת ההודעה לכולם שעובדה ב-Thread עובד

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

מקלט השידור יכול להשתמש ב-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 עדיין חל.