تصادف می کند

یک برنامه اندروید هر زمان که یک خروج غیرمنتظره ناشی از یک استثنا یا سیگنال مدیریت نشده رخ دهد، از کار می‌افتد. برنامه‌ای که با استفاده از جاوا یا کاتلین نوشته شده است، اگر یک استثنای مدیریت نشده، که توسط کلاس Throwable نمایش داده می‌شود، ایجاد کند، از کار می‌افتد. برنامه‌ای که با استفاده از کد ماشین یا ++C نوشته شده است، اگر در طول اجرا یک سیگنال مدیریت نشده، مانند SIGSEGV ، وجود داشته باشد، از کار می‌افتد.

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

خرابی برنامه در دستگاه دارای سیستم عامل اندروید
شکل ۱. از کار افتادن برنامه در یک دستگاه اندروید.

لازم نیست یک برنامه در پیش‌زمینه اجرا شود تا از کار بیفتد. هر مؤلفه برنامه، حتی مؤلفه‌هایی مانند گیرنده‌های پخش یا ارائه‌دهندگان محتوا که در پس‌زمینه اجرا می‌شوند، می‌توانند باعث از کار افتادن برنامه شوند. این از کار افتادن‌ها اغلب برای کاربران گیج‌کننده هستند زیرا آنها به طور فعال با برنامه شما درگیر نبوده‌اند.

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

مشکل را تشخیص دهید

ممکن است همیشه ندانید که کاربرانتان هنگام استفاده از برنامه شما دچار خرابی می‌شوند. اگر قبلاً برنامه خود را منتشر کرده‌اید، می‌توانید از Android Vitals برای مشاهده نرخ خرابی برنامه خود استفاده کنید.

نکات مهم اندروید

Android Vitals می‌تواند به شما در نظارت و بهبود نرخ خرابی برنامه‌تان کمک کند. Android Vitals چندین نرخ خرابی را اندازه‌گیری می‌کند:

  • نرخ خرابی: درصد کاربران فعال روزانه شما که هر نوع خرابی را تجربه کرده‌اند.
  • نرخ خرابی درک شده توسط کاربر: درصد کاربران فعال روزانه شما که حداقل یک خرابی را در حین استفاده فعال از برنامه شما تجربه کرده‌اند (خرابی درک شده توسط کاربر). یک برنامه در صورت نمایش هرگونه فعالیت یا اجرای هرگونه سرویس پیش‌زمینه، در حال استفاده فعال در نظر گرفته می‌شود.

  • نرخ خرابی چندگانه: درصد کاربران فعال روزانه شما که حداقل دو خرابی را تجربه کرده‌اند.

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

نرخ خرابی درک شده توسط کاربر، یک عامل حیاتی و مهم است که بر قابلیت کشف برنامه شما در گوگل پلی تأثیر می‌گذارد. این مهم است زیرا خرابی‌هایی که در نظر گرفته می‌شوند، همیشه زمانی رخ می‌دهند که کاربر با برنامه درگیر است و بیشترین اختلال را ایجاد می‌کند.

پلی دو آستانه رفتار بد را در این معیار تعریف کرده است:

  • آستانه کلی رفتار بد: حداقل ۱.۰۹٪ از کاربران فعال روزانه، در تمام مدل‌های دستگاه، دچار خرابی از نظر کاربر می‌شوند.
  • آستانه رفتار بد به ازای هر دستگاه: حداقل ۸٪ از کاربران فعال روزانه، برای یک مدل دستگاه واحد ، دچار خرابی ادراک‌شده توسط کاربر می‌شوند.

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

وقتی برنامه شما بیش از حد دچار مشکل می‌شود، Android Vitals می‌تواند در Play Console به شما هشدار دهد.

برای اطلاعات بیشتر در مورد نحوه جمع‌آوری داده‌های حیاتی اندروید توسط گوگل پلی، به مستندات کنسول پلی مراجعه کنید.

تشخیص خرابی‌ها

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

موقعیت‌های زیادی وجود دارد که می‌تواند باعث خرابی برنامه شما شود. برخی از دلایل واضح هستند، مانند بررسی مقدار تهی یا رشته خالی، اما برخی دیگر ظریف‌تر هستند، مانند ارسال آرگومان‌های نامعتبر به یک API یا حتی تعاملات پیچیده چند رشته‌ای.

کرش‌ها در اندروید یک رد پشته ایجاد می‌کنند که تصویری از توالی توابع تو در تو فراخوانی شده در برنامه شما تا لحظه کرش کردن آن است. می‌توانید رد پشته کرش را در Android Vitals مشاهده کنید.

نحوه خواندن ردپای پشته

اولین قدم برای رفع مشکل خرابی، شناسایی محل وقوع آن است. در صورت استفاده از کنسول Play یا خروجی ابزار logcat ، می‌توانید از ردیابی پشته موجود در جزئیات گزارش استفاده کنید. اگر ردیابی پشته در دسترس ندارید، باید خرابی را به صورت محلی، یا با آزمایش دستی برنامه یا با تماس با کاربران آسیب‌دیده، و هنگام استفاده از logcat، دوباره ایجاد کنید.

ردگیری زیر نمونه‌ای از خرابی در برنامه‌ای که با استفاده از Jetpack Compose نوشته شده است را نشان می‌دهد:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException
    at com.android.developer.crashsample.ComposableSingletons$MainActivityKt.lambda$0(MainActivity.kt:27)
    at androidx.compose.foundation.ClickableNode.handleUpEvent(Clickable.kt:958)
    at androidx.compose.foundation.ClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:895)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:446)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:181)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:118)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:2650)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2969)
    at android.app.Activity.dispatchTouchEvent(Activity.java:4683)
    at android.os.Looper.loop(Looper.java:398)
    at android.app.ActivityThread.main(ActivityThread.java:9569)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:918)

ردیابی پشته دو قطعه اطلاعات را نشان می‌دهد که برای اشکال‌زدایی از خرابی بسیار مهم هستند:

  • نوع استثنای ایجاد شده.
  • بخشی از کد که استثنا در آن رخ می‌دهد.

نوع استثنای ایجاد شده معمولاً اشاره بسیار محکمی به مشکل پیش آمده دارد. بررسی کنید که آیا یک 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 آپلود کنید. برای اطلاعات بیشتر، به ردگیری‌های پشته خرابی Deobfuscate مراجعه کنید. برای اطلاعات کلی در مورد خرابی‌های بومی، به تشخیص خرابی‌های بومی مراجعه کنید.

نکاتی برای تکرار یک تصادف

این امکان وجود دارد که شما نتوانید مشکل را صرفاً با راه‌اندازی یک شبیه‌ساز یا اتصال دستگاه خود به رایانه، به‌طور کامل بازتولید کنید. محیط‌های توسعه معمولاً منابع بیشتری مانند پهنای باند، حافظه و فضای ذخیره‌سازی دارند. از نوع استثنا برای تعیین منبع کمیاب استفاده کنید، یا همبستگی بین نسخه اندروید، نوع دستگاه یا نسخه برنامه خود را پیدا کنید.

خطاهای حافظه

اگر با OutOfMemoryError مواجه شدید، می‌توانید یک شبیه‌ساز با ظرفیت حافظه کم برای آزمایش ایجاد کنید. شکل 2 تنظیمات مدیر AVD را نشان می‌دهد که در آن می‌توانید میزان حافظه دستگاه را کنترل کنید.

تنظیم حافظه در AVD manager
شکل ۲. تنظیمات حافظه در AVD manager.

استثنائات شبکه

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

اگر نیاز به بازتولید یک خطای شبکه، مانند UnknownHostException ، دارید، سعی کنید در حالی که برنامه شما سعی در استفاده از شبکه دارد، حالت هواپیما را روشن کنید.

گزینه دیگر، کاهش کیفیت شبکه در شبیه‌ساز با انتخاب شبیه‌سازی سرعت شبکه، تأخیر شبکه یا هر دو است. می‌توانید از تنظیمات سرعت و تأخیر در AVD manager استفاده کنید، یا می‌توانید شبیه‌ساز را با پرچم‌های -netdelay و -netspeed ، همانطور که در مثال خط فرمان زیر نشان داده شده است، شروع کنید:

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

این مثال، تأخیر ۲۰ ثانیه‌ای را برای تمام درخواست‌های شبکه و سرعت آپلود و دانلود ۱۴.۴ کیلوبیت بر ثانیه تعیین می‌کند. برای اطلاعات بیشتر در مورد گزینه‌های خط فرمان برای شبیه‌ساز، به بخش «شروع شبیه‌ساز از خط فرمان» مراجعه کنید.

خواندن با logcat

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

خروجی logcat به شما نشان می‌دهد که چه پیام‌های لاگ دیگری را چاپ کرده‌اید، به همراه پیام‌های لاگ دیگر از سیستم. فراموش نکنید که هرگونه عبارت Log اضافی که اضافه کرده‌اید را غیرفعال کنید، زیرا چاپ آنها در حین اجرای برنامه، CPU و باتری را هدر می‌دهد.

جلوگیری از خرابی‌های ناشی از استثنائات اشاره‌گر تهی

استثنائات اشاره‌گر تهی (که با نوع خطای زمان اجرا NullPointerException شناسایی می‌شوند) زمانی رخ می‌دهند که شما سعی دارید به یک شیء تهی دسترسی پیدا کنید، معمولاً با فراخوانی متدهای آن یا دسترسی به اعضای آن. استثنائات اشاره‌گر تهی بزرگترین علت خرابی برنامه‌ها در Google Play هستند. هدف از null این است که نشان دهد شیء وجود ندارد - برای مثال، هنوز ایجاد یا اختصاص داده نشده است.

برای جلوگیری از خطاهای اشاره‌گر تهی، باید قبل از فراخوانی متدها روی ارجاعات شیء یا تلاش برای دسترسی به اعضای آنها، مطمئن شوید که ارجاعات شیء که با آنها کار می‌کنید تهی نیستند. اگر ارجاع شیء تهی است، این مورد را به خوبی مدیریت کنید (برای مثال، قبل از انجام هرگونه عملیاتی روی ارجاع شیء از یک متد خارج شوید و اطلاعات را در یک گزارش اشکال‌زدایی بنویسید).

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

کاتلین

در کاتلین، nullability بخشی از سیستم نوع داده است. برای مثال، یک متغیر باید از ابتدا به صورت nullable یا non-nullable تعریف شود. انواع Nullable با علامت ? مشخص می‌شوند:

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

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

به متغیرهای غیر تهی‌پذیر نمی‌توان مقدار تهی اختصاص داد و متغیرهای تهی‌پذیر قبل از استفاده به عنوان غیر تهی، باید از نظر تهی بودن بررسی شوند.

اگر نمی‌خواهید به طور صریح null بودن را بررسی کنید، می‌توانید از عملگر فراخوانی امن ?. استفاده کنید:

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

به عنوان یک راهکار بهتر، مطمئن شوید که برای یک شیء nullable، حالت null را در نظر گرفته‌اید، در غیر این صورت برنامه شما ممکن است دچار حالت‌های غیرمنتظره شود. اگر برنامه شما با NullPointerException دیگر از کار نیفتد، متوجه وجود این خطاها نخواهید شد.

در زیر چند روش برای بررسی null بودن آورده شده است:

  • if چک کند

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

    به دلیل وجود smart-cast و بررسی null، کامپایلر کاتلین می‌داند که مقدار رشته غیر null است، بنابراین به شما امکان می‌دهد بدون نیاز به عملگر فراخوانی امن، مستقیماً از مرجع استفاده کنید.

  • ?: اپراتور الویس

    این عملگر به شما امکان می‌دهد بیان کنید «اگر شیء غیر تهی (non-null) است، شیء را برگردانید؛ در غیر این صورت، چیز دیگری را برگردانید».

    val length = string?.length ?: 0
    

شما هنوز هم می‌توانید در کاتلین NullPointerException مواجه شوید. موارد زیر رایج‌ترین موقعیت‌ها هستند:

  • وقتی صریحاً یک NullPointerException صادر می‌کنید.
  • وقتی از عملگر null assertion استفاده می‌کنید، از دستور !! استفاده می‌کنید. این عملگر هر مقداری را به نوع غیر null تبدیل می‌کند و در صورت null بودن مقدار، NullPointerException صادر می‌کند.
  • هنگام دسترسی به یک مرجع تهی از نوع پلتفرم.

انواع پلتفرم

انواع پلتفرم، اعلان‌های شیء هستند که از جاوا می‌آیند. با این نوع‌ها به طور خاص رفتار می‌شود ؛ بررسی‌های تهی به اندازه جاوا اعمال نمی‌شوند، بنابراین ضمانت عدم تهی بودن مانند جاوا است. وقتی به یک مرجع نوع پلتفرم دسترسی پیدا می‌کنید، کاتلین خطاهای زمان کامپایل ایجاد نمی‌کند، اما این ارجاعات می‌توانند منجر به خطاهای زمان اجرا شوند. به مثال زیر از مستندات کاتلین مراجعه کنید:

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

کاتلین زمانی که یک مقدار پلتفرم به یک متغیر کاتلین اختصاص داده می‌شود، به استنتاج نوع متکی است، یا می‌توانید نوع مورد انتظار را تعریف کنید. بهترین راه برای اطمینان از وضعیت صحیح تهی‌پذیری یک مرجع از جاوا، استفاده از حاشیه‌نویسی‌های تهی‌پذیری (برای مثال، @Nullable ) در کد جاوای شماست. کامپایلر کاتلین این ارجاعات را به عنوان انواع تهی‌پذیر یا غیرتهی‌پذیر واقعی نمایش می‌دهد، نه به عنوان انواع پلتفرم.

APIهای Java Jetpack در صورت نیاز با @Nullable یا @NonNull حاشیه‌نویسی شده‌اند و رویکرد مشابهی در SDK اندروید ۱۱ نیز در نظر گرفته شده است. انواعی که از این SDK می‌آیند و در کاتلین استفاده می‌شوند، به صورت انواع صحیح nullable یا nonnullable نمایش داده می‌شوند.

سیستم تایپ کاتلین به طور قابل توجهی خطاهای NullPointerException را کاهش می‌دهد. به عنوان مثال، برنامه Google Home در سالی که توسعه ویژگی‌های جدید را به کاتلین منتقل کرد، شاهد کاهش 30 درصدی خطاهای ناشی از خطاهای اشاره‌گر تهی بود.