الأعطال

يتعطّل تطبيق Android عند حدوث خروج غير متوقّع بسبب استثناء أو إشارة لم تتم معالجتها. يتعطّل التطبيق المكتوب باستخدام Java أو Kotlin إذا طرح استثناءً لم تتم معالجته، ويتم تمثيله بواسطة الفئة Throwable. يتعطّل التطبيق المكتوب باستخدام لغة الآلة أو C++‎ في حال حدوث إشارة لم تتم معالجتها، مثل SIGSEGV، أثناء تنفيذه.

عندما يتعطّل تطبيق، ينهي نظام التشغيل Android عملية التطبيق ويعرض مربّع حوار لإعلام المستخدم بأنّ التطبيق قد توقّف، كما هو موضّح في الشكل 1.

تعطُّل تطبيق على جهاز يعمل بنظام التشغيل Android
الشكل 1. تعطُّل تطبيق على جهاز يعمل بنظام التشغيل Android

ليس من الضروري أن يكون التطبيق قيد التشغيل في المقدّمة ليتعطّل. يمكن أن يؤدي أي مكوّن من مكونات التطبيق إلى تعطُّله، حتى المكونات التي تعمل في الخلفية، مثل أدوات معالجة عمليات البث أو موفّري المحتوى. وغالبًا ما تكون هذه الأعطال مربكة للمستخدمين لأنّهم لم يكونوا يتفاعلون مع تطبيقك بشكل نشط.

إذا كان تطبيقك يتعطّل، يمكنك اتّباع الإرشادات الواردة في هذه الصفحة لتشخيص المشكلة وحلّها.

الكشف عن المشكلة

قد لا تعرف دائمًا أنّ المستخدمين يواجهون أعطالاً عند استخدام تطبيقك. وإذا كنت قد نشرت تطبيقك، يمكنك استخدام "مؤشرات Android الحيوية" للاطّلاع على نسب الأعطال في تطبيقك.

مؤشرات Android الحيوية

يمكن أن تساعدك "مؤشرات Android الحيوية" في مراقبة نسبة الأعطال في تطبيقك وتحسينها. تقيس "مؤشرات Android الحيوية" عدة نِسب للأعطال، وهي:

  • نسبة الأعطال: هي النسبة المئوية للمستخدمين النشطين يوميًا الذين واجهوا أي نوع من الأعطال.
  • نسبة الأعطال التي لاحظها المستخدمون: هي النسبة المئوية للمستخدمين النشطين يوميًا الذين واجهوا عُطلاً واحدًا على الأقل أثناء استخدامهم تطبيقك بشكل نشط (عُطل لاحظه المستخدم). يُعتبَر التطبيق قيد الاستخدام النشط إذا كان يعرض أي نشاط أو ينفّذ أي خدمة تعمل في المقدّمة.

  • نسبة الأعطال المتعددة: هي النسبة المئوية للمستخدمين النشطين يوميًا الذين واجهوا عُطلَين على الأقل.

المستخدم النشط يوميًا هو مستخدم فريد يستخدم تطبيقك في يوم واحد على جهاز واحد، وقد يكون ذلك خلال جلسات متعدّدة. فإذا كان أحد المستخدمين يستخدم تطبيقك على أكثر من جهاز في يوم واحد، سيساهم كل جهاز في عدد المستخدمين النشطين لذلك اليوم. أمّا إذا استخدم عدة مستخدمين الجهاز نفسه في يوم واحد، فسيتم احتساب ذلك كمستخدم نشط واحد.

إنّ نسبة الأعطال التي لاحظها المستخدمون هي أحد مؤشرات الأداء الأساسية، أي أنّها تؤثر في قابلية اكتشاف تطبيقك على Google Play. وهذا مهم لأنّ الأعطال التي يتم احتسابها تحدث دائمًا عندما يتفاعل المستخدم مع التطبيق، ما يؤدي إلى حدوث أكبر خلل في الأداء.

حدّد Play معيارَين لتحديد الأداء السيئ لهذا المقياس:

  • معيار تحديد الأداء السيئ العام: يواجه ما لا يقل عن% 1.09 من المستخدمين النشطين يوميًا عُطلاً لاحظه المستخدم في جميع طُرز الأجهزة.
  • معيار تحديد الأداء السيئ حسب الجهاز: يواجه% 8 على الأقل من المستخدمين النشطين يوميًا عُطلاً لاحظه المستخدم في طراز جهاز معيّن.

إذا تجاوز تطبيقك معيار تحديد الأداء السيئ الإجمالي، من المرجَّح أن تنخفض فرص العثور عليه على جميع الأجهزة. إذا تجاوز تطبيقك معيار تحديد الأداء السيئ لكل جهاز على بعض الأجهزة، من المرجَّح أن تنخفض فرص العثور عليه على هذه الأجهزة، وقد يظهر تحذير في بطاقة بيانات المتجر.

يمكن أن تنبّهك "مؤشرات Android الحيوية" في Play Console عندما يسجّل تطبيقك عددًا كبيرًا من الأعطال.

للحصول على معلومات حول كيفية جمع Google Play لبيانات "مؤشرات Android الحيوية"، يُرجى الاطّلاع على مستندات Play Console.

تشخيص الأعطال

بعد تحديد أنّ تطبيقك يسجّل أعطالاً، تتمثّل الخطوة التالية في تشخيص هذه الأعطال. قد يكون حلّ الأعطال أمرًا صعبًا. ومع ذلك، إذا تمكّنت من تحديد السبب الجذري للتعطُّل، من المرجّح أن تتمكّن من العثور على حلّ له.

هناك العديد من الحالات التي يمكن أن تؤدي إلى حدوث عطل في تطبيقك، بعضها واضح، مثل التحقّق من قيمة فارغة أو سلسلة فارغة، والبعض الآخر أكثر دقة، مثل تمرير وسيطات غير صالحة إلى واجهة برمجة التطبيقات أو حتى التفاعلات المعقّدة المتعددة مؤشرات الترابط.

تؤدي الأعطال في نظام التشغيل Android إلى إنشاء عملية تتبُّع تسلسُل استدعاء دوال برمجية، وهي عبارة عن لقطة لتسلسُل الدوال المتداخلة التي تم استدعاؤها في برنامجك حتى لحظة تعطُّلها. يمكنك الاطّلاع على تتبُّع تسلسل استدعاء الدوال البرمجية للأعطال في مؤشرات Android الحيوية.

كيفية قراءة تتبُّع تسلسل استدعاء الدوال البرمجية

تتمثّل الخطوة الأولى لإصلاح عُطل في تحديد المكان الذي يحدث فيه. يمكنك استخدام تتبُّع تسلسل استدعاء الدوال البرمجية المتاح في تفاصيل التقرير إذا كنت تستخدم Play Console أو ناتج أداة 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 أو غير ذلك، وابحث عن المستندات المتعلقة بفئة الاستثناء.

يظهر في السطر الثاني من تتبُّع تسلسل استدعاء الدوال البرمجية اسم الفئة والطريقة والملف ورقم السطر الخاص بملف المصدر الذي تم فيه طرح الاستثناء. لكل دالة تم استدعاؤها، يعرض سطر آخر موقع الاستدعاء السابق (يُسمى إطار حزمة التكديس).

من خلال تتبُّع تسلسل استدعاء الدوال البرمجية وفحص الرمز، قد تجد موضعًا يمرِّر قيمة غير صحيحة. إذا لم يظهر الرمز في تتبُّع تسلسل استدعاء الدوال البرمجية، من المحتمل أنّك مرّرت مَعلمة غير صالحة إلى عملية غير متزامنة في مكان ما. يمكنك غالبًا معرفة ما حدث من خلال فحص كل سطر من تتبُّع تسلسل استدعاء الدوال البرمجية، والعثور على أي فئات من واجهة برمجة التطبيقات استخدمتها، والتأكّد من أنّ المَعلمات التي مرّرتها كانت صحيحة، وأنّك استدعيتها من مكان مسموح به.

تعمل عمليات تتبُّع تسلسل استدعاء الدوال البرمجية للتطبيقات التي تتضمّن تعليمات برمجية بلغتَي 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 إعدادات &quot;مدير محاكي Android&quot; حيث يمكنك التحكّم في مقدار الذاكرة على الجهاز.

إعدادات الذاكرة في &quot;مدير الأجهزة الافتراضية لنظام Android&quot;
الشكل 2. إعدادات الذاكرة في "مدير أجهزة Android الافتراضية"

استثناءات الشبكات

بما أنّ المستخدمين يتنقلون بشكل متكرّر بين نطاق تغطية شبكة الجوّال أو شبكة Wi-Fi، يجب عادةً عدم التعامل مع استثناءات الشبكة في التطبيق على أنّها أخطاء، بل على أنّها ظروف تشغيل عادية تحدث بشكل غير متوقّع.

إذا كنت بحاجة إلى إعادة إنتاج استثناء شبكة، مثل UnknownHostException، جرِّب تفعيل وضع الطائرة أثناء محاولة تطبيقك استخدام الشبكة.

يمكنك أيضًا تقليل جودة الشبكة في المحاكي من خلال اختيار محاكاة لسرعة الشبكة أو تأخير الشبكة أو كليهما. يمكنك استخدام إعدادَي السرعة ووقت الاستجابة في "مدير الأجهزة الافتراضية لنظام التشغيل Android"، أو يمكنك بدء المحاكي باستخدام العلامتَين -netdelay و-netspeed، كما هو موضّح في مثال سطر الأوامر التالي:

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

يضبط هذا المثال تأخيرًا لمدة 20 ثانية على جميع طلبات الشبكة وسرعة تحميل وتنزيل تبلغ 14.4 كيلوبت في الثانية. لمزيد من المعلومات عن خيارات سطر الأوامر للمحاكي، يُرجى الاطّلاع على بدء المحاكي من سطر الأوامر.

القراءة باستخدام Logcat

بعد أن تتمكّن من إعادة إنتاج التعطُّل، يمكنك استخدام أداة مثل logcat للحصول على مزيد من المعلومات.

سيعرض لك ناتج logcat رسائل السجلّ الأخرى التي طبعتها، بالإضافة إلى رسائل أخرى من النظام. لا تنسَ إيقاف أي عبارات Log إضافية أضفتها لأنّ طباعتها تؤدي إلى استهلاك وحدة المعالجة المركزية والبطارية أثناء تشغيل تطبيقك.

منع الأعطال الناتجة عن استثناءات مؤشر فارغ

تحدث استثناءات المؤشر الفارغ (التي يتم تحديدها حسب نوع خطأ وقت التشغيل NullPointerException) عند محاولة الوصول إلى عنصر فارغ، عادةً عن طريق استدعاء طُرق العنصر أو الوصول إلى أعضائه. تُعدّ استثناءات المؤشر الفارغ السبب الأكبر لتعطُّل التطبيقات على Google Play. الغرض من القيمة الفارغة هو الإشارة إلى أنّ العنصر مفقود، مثلاً، لم يتم إنشاؤه أو تعيينه بعد.

لتجنُّب حدوث استثناءات المؤشر الفارغ، عليك التأكّد من أنّ مراجع العناصر التي تستخدمها ليست قيمة فارغة قبل استدعاء الطرق عليها أو محاولة الوصول إلى أعضائها. إذا كان مرجع العنصر قيمة فارغة، تعامَل مع هذه الحالة بشكل جيد (على سبيل المثال، اخرج من إحدى الطرق قبل تنفيذ أي عمليات على مرجع العنصر واكتب معلومات في سجلّ تصحيح الأخطاء).

بما أنّك لا تريد إجراء عمليات تحقّق من القيمة الفارغة لكل مَعلمة في كل طلب إجراء يتم استدعاؤه، يمكنك الاعتماد على IDE أو على نوع العنصر للإشارة إلى إمكانية قبول القيم الفارغة.

Kotlin

في Kotlin، تشكّل إمكانية القيم الفارغة جزءًا من نظام الأنواع. على سبيل المثال، يجب تعريف المتغير منذ البداية على أنّه يقبل القيمة الخالية أو لا يقبلها. يتم وضع علامة ? على الأنواع التي تقبل القيم الخالية:

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

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

لا يمكن تعيين قيمة فارغة للمتغيرات غير القابلة للقيم الفارغة، ويجب التحقّق من إمكانية قبول القيم الفارغة للمتغيرات القابلة للقيم الفارغة قبل استخدامها كمتغيرات غير قابلة للقيم الفارغة.

إذا كنت لا تريد التحقّق من القيمة الخالية بشكل صريح، يمكنك استخدام عامل التشغيل ?. للاتصال الآمن:

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

من أفضل الممارسات معالجة حالة القيمة الخالية لكائن قابل للتصغير، وإلا قد يدخل تطبيقك في حالات غير متوقّعة. إذا لم يعُد تطبيقك يتعطّل عند استخدام NullPointerException، لن تعرف بوجود هذه الأخطاء.

في ما يلي بعض الطرق للتحقّق من القيمة الخالية:

  • if عملية تحقّق

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

    بسبب التحويل الذكي وفحص القيمة الخالية، يعرف مترجم Kotlin أنّ قيمة السلسلة ليست خالية، لذا يسمح لك باستخدام المرجع مباشرةً، بدون الحاجة إلى عامل التشغيل الآمن.

  • ?: عامل تشغيل Elvis

    يتيح لك هذا المعامل تحديد ما يلي: "إذا كان العنصر غير فارغ، يتم عرض العنصر، وإلا يتم عرض شيء آخر".

    val length = string?.length ?: 0
    

سيظل بإمكانك الحصول على NullPointerException في Kotlin. في ما يلي الحالات الأكثر شيوعًا:

  • عندما تطرح NullPointerException بشكل صريح.
  • عند استخدام عامل تأكيد القيمة الفارغة !! يحوّل هذا المعامل أي قيمة إلى نوع غير فارغ، ويعرض الخطأ NullPointerException إذا كانت القيمة فارغة.
  • عند الوصول إلى مرجع فارغ من نوع النظام الأساسي

أنواع المنصات

أنواع المنصات هي تعريفات عناصر مصدرها Java. تتم معاملة هذه الأنواع بشكل خاص؛ ولا يتم فرض عمليات التحقّق من القيم الخالية بشكل كبير، لذا فإنّ ضمان عدم القيمة الخالية هو نفسه كما هو الحال في Java. عند الوصول إلى مرجع لنوع النظام الأساسي، لا تنشئ لغة Kotlin أخطاء في وقت الترجمة، ولكن يمكن أن تؤدي هذه المراجع إلى حدوث أخطاء في وقت التشغيل. اطّلِع على المثال التالي من مستندات 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، أو يمكنك تحديد النوع المتوقّع. إنّ أفضل طريقة لضمان حالة إمكانية قبول القيم الفارغة الصحيحة للمرجع الوارد من Java هي استخدام تعليقات توضيحية خاصة بإمكانية قبول القيم الفارغة (على سبيل المثال، @Nullable) في رمز Java. سيمثّل برنامج ترجمة Kotlin هذه المراجع كأنواع فعلية تقبل القيم الفارغة أو لا تقبلها، وليس كأنواع نظام أساسي.

تمت إضافة التعليقات التوضيحية @Nullable أو @NonNull إلى واجهات برمجة تطبيقات Java Jetpack حسب الحاجة، وتم اتّخاذ نهج مماثل في حزمة تطوير البرامج (SDK) لنظام التشغيل Android 11. سيتم تمثيل الأنواع الواردة من حزمة SDK هذه والمستخدَمة في Kotlin على أنّها أنواع صحيحة تقبل القيم الفارغة أو لا تقبلها.

يقلّل نظام أنواع Kotlin من NullPointerException الأعطال بشكل كبير. على سبيل المثال، سجّل تطبيق Google Home انخفاضًا بنسبة% 30 في عدد الأعطال الناتجة عن استثناءات المؤشر الفارغ خلال العام الذي تم فيه نقل عملية تطوير الميزات الجديدة إلى لغة Kotlin.