קריסות מחשב

אפליקציה ל-Android קורסת בכל פעם שקיימת יציאה לא צפויה שנגרמת חריג או אות שלא טופלו. אפליקציה שנכתבת באמצעות Java או Kotlin קורס אם הוא גורם לחריגה לא מטופלת, המיוצגת על ידי Throwable. אפליקציה שנכתבת באמצעות קוד מכונה או C++ קורסת אם יש שגיאה לא מטופלת אות, כמו SIGSEGV, במהלך הביצוע שלו.

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

קריסת אפליקציה במכשיר Android

איור 1. קריסת אפליקציה במכשיר Android

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

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

זיהוי הבעיה

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

תפקוד האפליקציה

התכונה 'תפקוד האפליקציה' יכולה לעזור לך לעקוב אחר שיעור הקריסות של האפליקציה ולשפר אותו. התכונה 'תפקוד האפליקציה' מודדת כמה שיעורי קריסות:

  • שיעור הקריסות: אחוז המשתמשים הפעילים ביום (DAU) קריסה מכל סוג שהוא.
  • שיעור הקריסות שהשפיעו על המשתמשים: אחוז המשתמשים הפעילים ביום (DAU) קריסה אחת לפחות בזמן שהם השתמשו באפליקציה באופן פעיל (קריסה שהשפיעו על המשתמשים). אפליקציה נחשבת בשימוש פעיל אם הוא מציג פעילות או מבצע פעולה כלשהי שירות שפועל בחזית.

  • שיעור קריסות מרובות: אחוז המשתמשים הפעילים ביום (DAU) אירעו שתי קריסות לפחות.

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

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

מערכת Play הגדירה שני ערכי סף של התנהגות לא תקינה במדד הזה:

  • סף התנהגות לא תקינה: לפחות 1.09% מהמשתמשים הפעילים ביום קריסה שהשפיעה על המשתמשים בכל דגמי המכשירים.
  • סף התנהגות לא תקינה לפי מכשיר: לפחות 8% מהמשתמשים הפעילים ביום מתרחשת קריסה שהשפיעו על המשתמשים, בדגם מכשיר אחד.

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

התכונה 'תפקוד האפליקציה' יכולה לשלוח לך התראה דרך Play Console כשבאפליקציה יש יותר מדי קריסות.

כדי לקבל מידע על אופן האיסוף של נתוני תפקוד האפליקציה ב-Google Play, אפשר לעיין במאמר Play Console התיעוד.

אבחון הקריסות

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

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

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

איך קוראים דוח קריסות

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

במעקב הבא מוצגת דוגמה לקריסה באפליקציה שנכתבה באמצעות Java שפת תכנות:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
at android.view.View.performClick(View.java:6134)
at android.view.View$PerformClick.run(View.java:23965)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6440)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
--------- beginning of system

בדוח הקריסות מוצגים שני פרטים שחיוניים לניפוי באגים לקרוס:

  • סוג החריגה שנדחתה.
  • הקטע בקוד שבו זורקת החריגה.

סוג החריגה הוא בדרך כלל רמז ברור מאוד למה שהתרחש שגוי. בדקו אם מדובר IOException, OutOfMemoryError, או משהו אחר, ומוצאים את התיעוד לגבי הכיתה החריגה.

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

מעקבי קריסות באפליקציות עם קוד C ו-C++ פועלים באופן דומה.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp  >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000007da81396c0  x1  0000007fc91522d4  x2  0000000000000001  x3  000000000000206e
    x4  0000007da8087000  x5  0000007fc9152310  x6  0000007d209c6c68  x7  0000007da8087000
    x8  0000000000000000  x9  0000007cba01b660  x10 0000000000430000  x11 0000007d80000000
    x12 0000000000000060  x13 0000000023fafc10  x14 0000000000000006  x15 ffffffffffffffff
    x16 0000007cba01b618  x17 0000007da44c88c0  x18 0000007da943c000  x19 0000007da8087000
    x20 0000000000000000  x21 0000007da8087000  x22 0000007fc9152540  x23 0000007d17982d6b
    x24 0000000000000004  x25 0000007da823c020  x26 0000007da80870b0  x27 0000000000000001
    x28 0000007fc91522d0  x29 0000007fc91522a0
    sp  0000007fc9152290  lr  0000007d22d4e354  pc  0000007cba01b640

backtrace:
  #00  pc 0000000000042f89  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
  #01  pc 0000000000000640  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
  #02  pc 0000000000065a3b  /system/lib/libc.so (__pthread_start(void*))
  #03  pc 000000000001e4fd  /system/lib/libc.so (__start_thread)

אם מידע ברמת המחלקה והפונקציה לא מופיע בדוחות קריסות נייטיב: יכול להיות שתצטרכו יצירת קובץ סמלים מקורי של ניפוי באגים ולהעלות אותו ל-Google Play Console. מידע נוסף זמין במאמר הבא: פענוח קוד מעורפל של דוחות קריסה. מידע כללי על קריסות נייטיב זמין בכתובת אבחון קריסות מקוריות.

טיפים לשחזור תאונה

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

שגיאות זיכרון

אם יש לך OutOfMemoryError תוכלו ליצור אמולטור עם קיבולת זיכרון נמוכה כדי לבצע בדיקה. דמות 2 מציגה את ההגדרות של מנהל ה-AVD שבהן אפשר לשלוט בנפח הזיכרון מופעל במכשיר.

הגדרת הזיכרון במנהל ה-AVD

איור 2. הגדרת הזיכרון במנהל ה-AVD

חריגים ברשתות

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

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

אפשרות נוספת היא להפחית את איכות הרשת באמולטור על ידי בחירה של אמולציית מהירות רשת ו/או עיכוב ברשת. אפשר להשתמש אפשר להגדיר מהירות וזמן אחזור במנהל ה-AVD. אפשר גם להפעיל את האמולטור עם הדגלים -netdelay ו--netspeed, כפי שמוצג למטה דוגמה לשורת פקודה:

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

בדוגמה הזו מוגדרת השהייה של 20 שניות בכל בקשות הרשת והעלאה ומהירות ההורדה היא 14.4Kbps. מידע נוסף על אפשרויות שורת הפקודה לאמולטור: מפעילים את האמולטור משורת הפקודה.

קריאה עם Logcat

אחרי שיהיו לכם את השלבים לשחזור הקריסה, תוכלו להשתמש בכלי כמו logcat כדי לקבל מידע נוסף.

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

מניעת קריסות שנגרמו עקב חריגים במצבי null

חריגים מסוג null (מזוהים לפי סוג השגיאה בסביבת זמן הריצה) NullPointerException) מתרחשת כשמנסים לגשת לאובייקט null, בדרך כלל על ידי הפעלת השיטות שלו או גישה לחברים שנכללים בו. סמן null חריגות הן הסיבה הגדולה ביותר לקריסות של אפליקציות ב-Google Play. המטרה של הערך null מציין שהאובייקט חסר – לדוגמה, הוא לא קיים נוצרו או הוקצו עדיין. כדי להימנע מהחרגות של מצביע null, צריך לוודא שההפניות של האובייקט שאתם עובדים איתו אינן null לפני הקריאה שיטות בהם או לנסות לגשת לחברים שלהם. אם ההפניה לאובייקט היא null, טפל במקרה זה היטב (לדוגמה, יציאה מ-method לפני ביצוע פעולות בהפניה לאובייקט וכתיבת מידע ביומן ניפוי באגים).

מפני שאתם לא רוצים לבצע בדיקות null לכל פרמטר בכל שיטה. אפשר להסתמך על סביבת הפיתוח המשולבת (IDE) או על סוג האובייקט יכולת אפס.

שפת תכנות Java

הסעיפים הבאים חלים על שפת התכנות Java.

הידור האזהרות על הזמן

הוספת הערות לשיטות שלך ותחזיר ערכים עם @Nullable ו- @NonNull כדי לקבל זמן הידור אזהרות מסביבת הפיתוח המשולבת (IDE). האזהרות הבאות מאפשרות לצפות לאובייקט שאינו יכול להיות ריק (null):

אזהרה לגבי חריג של סמן null

בדיקות ה-null האלה מיועדות לאובייקטים שידוע לכם שהם יכולים להיות null. חריג לכלל אובייקט @NonNull הוא אינדיקציה לשגיאה בקוד שצריך טופל.

הידור שגיאות הזמן

ליכולת ה-null צריכה להיות משמעות, אפשר להטמיע אותה בסוגים שבהם משתמשים כך שיש בדיקת זמן הידור ל-null. אם יודעים שאובייקט יכול להיות null וצריך לטפל בהן, אפשר לעטוף אותו באובייקט כמו Optional תמיד צריך להעדיף סוגים שמעבירים ערך null.

Kotlin

ב-Kotlin, ערך null הוא חלק ממערכת הסוגים. לדוגמה, צריך להצהיר על משתנה מ- הערך של ההתחלה מוגדר כ-null או כ-null. סוגים של ערכי null מסומנים ב-?:

// non-null
var s: String = "Hello"

// null
var s: String? = "Hello"

לא ניתן להקצות ערך null ומשתנים שאינם ערכי null צריך לבדוק אם קיימת יכולת null לפני שמשתמשים בהן כערכי null.

אם לא רוצים לבדוק באופן מפורש את הערך null, אפשר להשתמש בקריאה הבטוחה של ?.. אופרטור:

val length: Int? = string?.length  // length is a nullable int
                                   // if string is null, then length is null

השיטה המומלצת היא לטפל באותיות ה-null של אובייקט שהוא אפס (null). אחרת האפליקציה עלולה להגיע למצבים לא צפויים. אם האפליקציה לא קורסת עם NullPointerException, לא תדעו שהשגיאות האלה קיימות.

יש כמה דרכים לבדוק אם הערך הוא null:

  • if בדיקות

    val length = if(string != null) string.length else 0
    

    הודות להעברה חכמה ובדיקת ה-null, מהדר של Kotlin יודע ערך המחרוזת אינו null ולכן הוא מאפשר להשתמש ישירות בהפניה, ללא צורך במוקד השיחה הבטוח.

  • ?: אופרטור של Elvis

    האופרטור הזה מאפשר לציין "אם האובייקט הוא לא null, צריך להחזיר את אובייקט; אחרת, מחזיר משהו אחר".

    val length = string?.length ?: 0
    

עדיין אפשר לקבל NullPointerException ב-Kotlin. אלה הסוגים הנפוצים ביותר מצבים נפוצים:

  • כשמריצים NullPointerException באופן מפורש.
  • בזמן השימוש אופרטור null טענת נכוֹנוּת (assertion) !!. האופרטור הזה ממיר כל ערך לסוג שאינו null, NullPointerException אם הערך הוא null.
  • כשניגשים להפניה אפס של סוג פלטפורמה.

סוגי פלטפורמות

סוגי הפלטפורמות הם הצהרות אובייקטים שמגיעות מ-Java. סוגים אלה מטופלים באופן מיוחד; בדיקות null לא נאכפות באותה מידה, כך שהאחריות שאינה אפסית זהה Java. כשניגשים להפניה לסוג פלטפורמה, Kotlin לא יוצר הידור (compile) אבל הפניות אלה יכולות להוביל לשגיאות בזמן ריצה. כדאי לעיין בנושאים הבאים דוגמה מתוך מסמכי התיעוד של Kotlin:

val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
                                                       // exception if item == null

שיטת Kotlin מסתמכת על סוג ההסקה כאשר ערך פלטפורמה מוקצה ל-Kotlin משתנה, או להגדיר מה הסוג של הנתונים. הדרך הטובה ביותר לוודא מצב אפסי (nullability) של הפניה שמגיעה מ-Java הוא שימוש ביכולת ה-null (לדוגמה, @Nullable) בקוד ה-Java. המהדר של Kotlin שמייצג את ההפניות האלו כסוגים אמיתיים כערכי null או ככאלה שאינם null, בסוגי הפלטפורמות השונות.

לממשקי API של Java Jetpack נוספו הערות עם @Nullable או @NonNull לפי הצורך, וננקטה גישה דומה Android 11 SDK. סוגים שמגיעים מה-SDK הזה, שנמצאים בשימוש ב-Kotlin, מיוצגים בתור סוגים נכונים של ערכי null או null.

בזכות מערכת הסוגים של Kotlin, ראינו ירידה משמעותית NullPointerException קריסות. לדוגמה, אפליקציית Google Home ראתה 30% ירידה במספר הקריסות שנגרמו עקב חריגים של מצביע null במהלך השנה שבה העבירה את פיתוח התכונות החדשות ל-Kotlin.