ANRs

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

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

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

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

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

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

אבחון של מקרי ANR

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

  • האפליקציה מבצעת פעולות איטיות שכוללות קלט/פלט ב-thread הראשי.
  • האפליקציה מבצעת חישוב ארוך בשרשור הראשי.
  • ה-thread הראשי מבצע הפעלה סינכרונית של Binder לתהליך אחר, והתהליך האחר הזה לוקח הרבה זמן לחזור.
  • ה-thread הראשי חסום. הוא נמצא בהמתנה לבלוק מסונכרן של פעולה ארוכה שמתבצעת ב-thread אחר.
  • ה-thread הראשי נמצא במצב של קיפאון עם thread אחר, בתהליך או באמצעות קריאת binder. ה-thread הראשי לא רק נמצא בהמתנה לסיום של פעולה ארוכה, אלא נמצא במצב של קיפאון.

הטכניקות הבאות יכולות לעזור לכם לקבוע את הגורם ל-ANR.

HealthStats

HealthStats מספק מדדים לגבי תקינות האפליקציה על ידי תיעוד של סה"כ זמן המשתמש והמערכת, זמן המעבד, נתוני רשת, נתוני רדיו, זמן הפעלה/השבתה של המסך ואזעקות התעוררות. כך תוכלו למדוד את השימוש הכולל ב-CPU ואת קצב ניקוז הסוללה.

ניפוי באגים

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

ApplicationExitInfo

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

מצב קפדני

השימוש ב-StrictMode עוזר לכם למצוא פעולות קלט/פלט לא מכוונות בשרשור הראשי בזמן שאתם מפתחים את האפליקציה. אתם יכולים להשתמש ב-StrictMode ברמת האפליקציה או הפעילות.

הפעלת תיבות דו-שיח של ANR ברקע

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

צווארי בקבוק ברה-קומפוזיציה

אפשר להשתמש בכלי לניתוח ביצועים ב-Android Studio ובכלי Layout Inspector כדי לאתר צווארי בקבוק של יצירה מחדש. מידע נוסף זמין במאמר בנושא ביצועים של Jetpack Compose.

שליפת קובץ מעקב

חנויות Android שומרות מידע על מעקב כשהן נתקלות ב-ANR. בגרסאות ישנות יותר של מערכת ההפעלה, יש קובץ /data/anr/traces.txt אחד במכשיר. בגרסאות חדשות יותר של מערכת ההפעלה, יש כמה קבצים מסוג /data/anr/anr_*. אפשר לגשת לנתוני ANR ממכשיר או מאמולטור באמצעות ממשק הגישור של Android‏ (ADB) כמשתמש root:

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

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

פתרון הבעיות

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

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

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

בעיה נפוצה היא משימה שפועלת לאורך זמן ישירות ברכיב שאפשר להרכיב:

@Composable
fun BadList(rawStrings: List<String>) {
    // Math or sorting inside the composable runs on EVERY recomposition pass!
    val heavilyProcessedList = rawStrings
        .filter { it.isNotBlank() }
        .map { it.uppercase().reversed() }
        .map { it.computationallyHeavyFunction() }
.sortedBy { it.length }
    LazyColumn { items(sortedList) { Text(it) } }
}

// Modern Compose-first fix
@Composable
fun GoodList(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // UI simply renders state; no heavy processing allowed here
    LazyColumn { items(uiState.sortedData) { Text(it) } }
}

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

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

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

קיפאון (deadlock)

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

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

מידע נוסף זמין במאמרים Deadlock ו-Deadlock prevention algorithms בוויקיפדיה.

כשמשתמשים ב-Kotlin וב-Compose, אפשר להחליף את הנעילות הפרימיטיביות ב-Mutexes של קורוטינות לא חוסמות (Mutex.withLock) כדי למנוע חסימה של השרשור על ידי השהיה של הקשר הביצועי במקום הקפאה של השרשור של ממשק המשתמש. לדוגמה:

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

// Modern non-blocking concurrency state architecture
class SecureDataRepository {
    private val mutex = Mutex()

    suspend fun safeUIAccess() {
        // If locked, the main thread suspends seamlessly, preventing an ANR
        mutex.withLock {
            performSafeOperation()
        }
    }
}

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

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

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

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

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

GameActivity

הספרייה GameActivity הפחיתה את מספר השגיאות מסוג ANR במקרי לדוגמה של משחקים ואפליקציות שנכתבו ב-C או ב-C++. אם תחליפו את הפעילות Native הקיימת ב-GameActivity, תוכלו להפחית את החסימה של שרשור ה-UI ולמנוע חלק מהשגיאות מסוג ANR.

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

מקורות מידע נוספים

צפיות בתוכן