مثال عملي على تصحيح أخطاء الأداء: أخطاء ANR

يوضِّح هذا القسم كيفية تصحيح خطأ التطبيق لا يستجيب (ANR) باستخدام ProfilingManager مع مثال على عملية تتبُّع.

إعداد التطبيق لجمع أخطاء "التطبيق لا يستجيب"

ابدأ بإعداد مشغِّل خطأ "التطبيق لا يستجيب" في تطبيقك:

public void addANRTrigger() {
  ProfilingManager profilingManager = getApplicationContext().getSystemService(
      ProfilingManager.class);
  List<ProfilingTrigger> triggers = new ArrayList<>();
  ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder(
      ProfilingTrigger.TRIGGER_TYPE_ANR);
  triggers.add(triggerBuilder.build());
  Executor mainExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      profilingResult -> {
        // Handle uploading trace to your back-end
      };
  profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);
}

بعد تسجيل عملية تتبُّع خطأ "التطبيق لا يستجيب" وتحميلها، افتحها في واجهة مستخدم Perfetto.

تحليل عملية التتبُّع

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

التنقّل في واجهة مستخدم Perfetto إلى سلسلة التعليمات الرئيسية للتطبيق
الشكل 1. الانتقال إلى سلسلة التعليمات الرئيسية في التطبيق

تتطابق نهاية عملية التتبُّع مع الطابع الزمني لخطأ "التطبيق لا يستجيب"، كما هو موضّح في الشكل 2.

واجهة مستخدم Perfetto تعرض نهاية عملية تتبُّع، مع تمييز الموقع الجغرافي الذي تم فيه تشغيل خطأ ANR.
الشكل 2. موقع مشغِّل خطأ "التطبيق لا يستجيب"

تعرض عملية التتبُّع أيضًا العمليات التي كان التطبيق يُجريها عند حدوث خطأ "التطبيق لا يستجيب". على وجه التحديد، شغَّل التطبيق رمزًا في شريحة التتبُّع handleNetworkResponse. كانت هذه الشريحة داخل شريحة MyApp:SubmitButton. استغرقت 1.48 ثانية من وقت وحدة المعالجة المركزية (الشكل 3).

تعرض واجهة مستخدم Perfetto وقت وحدة المعالجة المركزية (CPU) المستخدَم في تنفيذ handleNetworkResponse
 في وقت حدوث خطأ ANR.
الشكل 3. التنفيذ في وقت حدوث خطأ "التطبيق لا يستجيب"

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

للحصول على نقطة بداية للبحث عن سبب خطأ "التطبيق لا يستجيب"، نبدأ البحث بعد آخر إطار أنشأته سلسلة واجهة المستخدم التي تتطابق مع شريحة Choreographer#doFrame 551275، وليس هناك مصادر كبيرة للتأخير قبل بدء شريحة MyApp:SubmitButton التي انتهت بخطأ "التطبيق لا يستجيب" (الشكل 4).

تعرض واجهة مستخدم Perfetto آخر لقطة عرضتها سلسلة واجهة المستخدم قبل حدوث خطأ ANR.
الشكل 4. آخر إطار للتطبيق تم إنشاؤه قبل خطأ "التطبيق لا يستجيب"

لفهم الحظر، عليك التصغير لفحص شريحة MyApp:SubmitButton الكاملة. ستلاحظ تفصيلاً مهمًا في حالات سلسلة التعليمات، كما هو موضّح في الشكل 4: قضت سلسلة التعليمات 75% من الوقت (6.7 ثانية) في حالة Sleeping و24% فقط من الوقت في حالة Running .

تعرض واجهة مستخدم Perfetto حالات سلاسل المحادثات أثناء عملية ما، مع إبراز أنّ 75% من الوقت كان في وضع السكون و24% من الوقت كان وقت تشغيل.
الشكل 5. حالات سلسلة التعليمات أثناء عملية `MyApp:SubmitButton`

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

تعرض واجهة مستخدم Perfetto الفاصل الزمني الأول لوضع السكون ضمن شريحة تتبُّع MyAppSubmitButton.
الشكل 6. وقت النوم الأول ضمن `MyAppSubmitButton`.
تعرض واجهة مستخدم Perfetto فاصل السكون الثاني ضمن شريحة تتبُّع MyAppSubmitButton.
الشكل 7. وقت النوم الثاني ضمن `MyAppSubmitButton`.
تعرض واجهة مستخدم Perfetto الفاصل الزمني الثالث لوضع السكون ضمن شريحة تتبُّع MyAppSubmitButton.
الشكل 8. وقت النوم الثالث ضمن `MyAppSubmitButton`.
تعرض واجهة مستخدم Perfetto الفاصل الزمني الرابع في وضع السكون ضمن شريحة تتبُّع MyAppSubmitButton.
الشكل 9. وقت النوم الرابع ضمن `MyAppSubmitButton`.

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

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

تعرض واجهة مستخدم Perfetto ملخّصًا للتأخيرات خلال شريحة التتبُّع MyApp:SubmitButton<0x0A>، ما يشير إلى فواصل نوم متعدّدة مدتها ثانيتان.
الشكل 10. ملخّص حالات التأخير أثناء شريحة `MyApp:SubmitButton`.

للتحقّق من ذلك، افحص الرمز المرتبط بقسم التتبُّع MyApp:SubmitButton:

private static final int NETWORK_TIMEOUT_MILLISECS = 2000;
public void setupButtonCallback() {
  findViewById(R.id.submit).setOnClickListener(submitButtonView -> {
    Trace.beginSection("MyApp:SubmitButton");
    onClickSubmit();
    Trace.endSection();
  });
}

public void onClickSubmit() {
  prepareNetworkRequest();

  boolean networkRequestSuccess = false;
  int maxAttempts = 10;
  while (!networkRequestSuccess && maxAttempts > 0) {
    networkRequestSuccess = performNetworkRequest(NETWORK_TIMEOUT_MILLISECS);
    maxAttempts--;
  }

  if (networkRequestSuccess) {
    handleNetworkResponse();
  }
}

boolean performNetworkRequest(int timeoutMiliseconds) {
  // ...
}


void prepareNetworkRequest() {
  // ...
}

public void handleNetworkResponse() {
  Trace.beginSection("handleNetworkResponse");
  // ...
  Trace.endSection();
}

يؤكّد الرمز هذه الفرضية. تنفِّذ طريقة onClickSubmit طلب شبكة على سلسلة واجهة المستخدم مع NETWORK_TIMEOUT_MILLISECS مبرمَجة مسبقًا تبلغ 2000 ملي ثانية. والأهم من ذلك، يتم تشغيلها داخل حلقة while تعيد المحاولة حتى 10 مرات.

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

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