الأعطال

يتعطل تطبيق 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.

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

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

تعمل عمليات تتبّع تسلسل استدعاء الدوال البرمجية للتطبيقات التي تتضمّن الرمز 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

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

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

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

ويتوفّر خيار آخر وهو خفض جودة الشبكة في المحاكي من خلال اختيار محاكاة سرعة الشبكة و/أو تأخير الشبكة. يمكنك استخدام إعدادات السرعة ووقت الاستجابة في مدير AVD أو يمكنك بدء المحاكي بعلامتَي -netdelay و-netspeed، كما هو موضّح في مثال سطر الأوامر التالي:

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

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

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

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

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

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

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

يمكنك الاعتماد على بيئة التطوير المتكاملة (IDE) أو نوع الكائن للدلالة على قابلية استخدام القيم الفارغة.

لغة البرمجة Java

تنطبق الأقسام التالية على لغة البرمجة Java.

تجميع تحذيرات الوقت

يمكنك إضافة تعليقات توضيحية إلى مَعلمات الطرق والقيم المعروضة باستخدام @Nullable و@NonNull لتلقّي تحذيرات متعلّقة بوقت التجميع من بيئة التطوير المتكاملة. تطلب منك هذه التحذيرات توقع ظهور عنصر قابل للقيم:

تحذير استثناء مؤشر فارغ

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

أخطاء وقت التجميع

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

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 هذه المراجع على أنّها أنواع فعلية قابلة للقيم أو غير قابلة للقيم الفارغة، وليس كأنواع لمنصات.

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

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