ANR (มุมมอง)

แนวคิดและการติดตั้งใช้งาน Jetpack Compose

เมื่อเธรด UI ของแอป Android ถูกบล็อกนานเกินไป ระบบจะทริกเกอร์ข้อผิดพลาด "แอปพลิเคชันไม่ตอบสนอง" (ANR) หากแอปอยู่เบื้องหน้า ระบบจะแสดงกล่องโต้ตอบแก่ผู้ใช้ ดังที่แสดงในรูปที่ 1 กล่องโต้ตอบ ANR จะให้โอกาสผู้ใช้ในการบังคับปิดแอป

กล่องโต้ตอบ ANR ที่แสดงต่อผู้ใช้
รูปที่ 1 กล่องโต้ตอบ ANR ที่แสดงต่อผู้ใช้

ANR เป็นปัญหาเนื่องจากชุดข้อความหลักของแอปซึ่งมีหน้าที่อัปเดต UI ไม่สามารถประมวลผลเหตุการณ์การป้อนข้อมูลของผู้ใช้หรือวาดภาพได้ ซึ่งทำให้ผู้ใช้หงุดหงิด ดูข้อมูลเพิ่มเติมเกี่ยวกับเทรดหลักของแอปได้ที่ภาพรวมกระบวนการและเทรด

ระบบจะทริกเกอร์ ANR สำหรับแอปของคุณเมื่อเกิดเงื่อนไขอย่างใดอย่างหนึ่งต่อไปนี้

  • การส่งอินพุตหมดเวลา: หากแอปไม่ตอบสนองต่อเหตุการณ์อินพุต (เช่น การกดแป้นหรือการแตะหน้าจอ) ภายใน 5 วินาที
  • การเรียกใช้บริการ: หากบริการที่แอปประกาศไม่สามารถเรียกใช้ Service.onCreate() และ Service.onStartCommand()/Service.onBind() ให้เสร็จภายในไม่กี่วินาที
  • **Service.startForeground() ไม่ได้ถูกเรียก**: หากแอปใช้ Context.startForegroundService() เพื่อเริ่มบริการใหม่ในเบื้องหน้า แต่บริการไม่เรียก startForeground() ภายใน 5 วินาที
  • การประกาศ Intent: หาก BroadcastReceiver ทำงานไม่เสร็จภายในระยะเวลาที่กำหนด หากแอปมีกิจกรรมที่ทำงานอยู่เบื้องหน้า ระยะหมดเวลานี้จะเท่ากับ 10 วินาที
  • การโต้ตอบ **JobScheduler**: หาก JobService ไม่แสดงผลจาก JobService.onStartJob() หรือ JobService.onStopJob() ภายในไม่กี่วินาที หรือหากงานที่ผู้ใช้เริ่มเริ่มต้นขึ้นและแอปของคุณไม่เรียกใช้ JobService.setNotification() ภายในไม่กี่วินาทีหลังจากเรียกใช้ JobService.onStartJob() สำหรับแอปที่กำหนดเป้าหมายเป็น Android 13 และต่ำกว่า ANR จะเกิดขึ้นโดยไม่มีการแจ้งเตือนและไม่มีการรายงานไปยังแอป สำหรับแอปที่กำหนดเป้าหมายเป็น Android 14 ขึ้นไป ANR จะเกิดขึ้นอย่างชัดเจนและมีการรายงานไปยังแอป

หากแอปของคุณพบ ANR คุณสามารถใช้คำแนะนำในบทความนี้เพื่อวิเคราะห์และแก้ไขปัญหาได้

แก้ไขปัญหา

หลังจากระบุปัญหาแล้ว คุณสามารถใช้เคล็ดลับในส่วนนี้เพื่อแก้ไขปัญหาที่พบได้ทั่วไป

โค้ดที่ทำงานช้าในเทรดหลัก

ระบุตำแหน่งในโค้ดที่เทรดหลักของแอปไม่ว่างนานกว่า 5 วินาที มองหา Use Case ที่น่าสงสัยในแอปและพยายามจำลอง ANR ซ้ำ

ตัวอย่างเช่น รูปที่ 2 แสดงไทม์ไลน์ Traceview ที่เทรดหลักไม่ว่างนานกว่า 5 วินาที

รูปที่ 2 ไทม์ไลน์ Traceview ที่แสดงเทรดหลักที่ทำงานอยู่

รูปที่ 2 ไทม์ไลน์ Traceview ที่แสดงเทรดหลักที่ทำงานอยู่

รูปที่ 2 แสดงให้เห็นว่าโค้ดที่ทำให้เกิดปัญหาส่วนใหญ่เกิดขึ้นในแฮนเดิล onClick(View) ดังที่แสดงในตัวอย่างโค้ดต่อไปนี้

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

ในกรณีนี้ คุณควรย้ายงานที่ทำงานในเทรดหลักไปยังเทรดผู้ปฏิบัติงาน เฟรมเวิร์ก Android มีคลาสที่ช่วยย้ายงานไปยังเทรดผู้ปฏิบัติงานได้ ดูข้อมูลเพิ่มเติมได้ที่เทรดผู้ปฏิบัติงาน

I/O ในเทรดหลัก

การดําเนินการ I/O ในเทรดหลักเป็นสาเหตุที่พบบ่อยของการทํางานที่ช้าในเทรดหลัก ซึ่งอาจทําให้เกิด ANR ใน Compose นักพัฒนามักจะเรียกให้ระบบอ่านดิสก์โดยไม่ตั้งใจ (เช่น SharedPreferences หรือการเรียกใช้ฐานข้อมูล) ขณะพยายามรับสถานะเริ่มต้น

ดำเนินการ I/O ที่ใช้เวลานานนอกเลเยอร์ UI ใช้ withContext(Dispatchers.IO) ใน ViewModel หรือจะให้ดีกว่านั้น ให้ใช้ที่เก็บในชั้นข้อมูล เราขอแนะนำให้ย้ายการดำเนินการ I/O ทั้งหมดไปยังเทรดผู้ปฏิบัติงานตามที่แสดงในส่วนก่อนหน้า

ตัวอย่างการดำเนินการ IO ได้แก่ การดำเนินการกับเครือข่ายและพื้นที่เก็บข้อมูล ดูข้อมูลเพิ่มเติมได้ที่การดำเนินการด้านเครือข่ายและการบันทึกข้อมูล

การช่วงชิงล็อก

ในบางสถานการณ์ งานที่ทำให้เกิด ANR ไม่ได้ดำเนินการโดยตรงในเทรดหลักของแอป หากเทรดผู้ปฏิบัติงานล็อกทรัพยากรที่เทรดหลักต้องใช้เพื่อทำงานให้เสร็จ ANR อาจเกิดขึ้นได้

ตัวอย่างเช่น รูปที่ 3 แสดงไทม์ไลน์ Traceview ที่งานส่วนใหญ่ดำเนินการในเทรดผู้ปฏิบัติงาน

รูปที่ 3 ไทม์ไลน์ Traceview ที่แสดงงานที่กำลังดำเนินการในเทรดผู้ปฏิบัติงาน

รูปที่ 3 ไทม์ไลน์ Traceview ที่แสดงงานที่กำลังดำเนินการในเทรดผู้ปฏิบัติงาน

แต่หากผู้ใช้ยังคงพบ ANR คุณควรดูสถานะของเทรดหลักใน Android Device Monitor โดยปกติแล้ว เทรดหลักจะอยู่ในสถานะ RUNNABLE หากพร้อมที่จะอัปเดต UI และโดยทั่วไปจะตอบสนองได้ดี

แต่หากเทรดหลักไม่สามารถกลับมาทำงานต่อได้ เทรดหลักจะอยู่ในสถานะ BLOCKED และไม่สามารถตอบสนองต่อเหตุการณ์ได้ สถานะจะแสดงใน Android Device Monitor เป็นตรวจสอบหรือรอ ดังที่แสดงในรูปที่ 5

รูปที่ 4 เทรดหลักในสถานะ Monitor

รูปที่ 4 เทรดหลักอยู่ในสถานะตรวจสอบ

การติดตามต่อไปนี้แสดงเทรดหลักของแอปที่ถูกบล็อกเพื่อรอทรัพยากร

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

การตรวจสอบการติดตามจะช่วยให้คุณค้นหาโค้ดที่บล็อกเทรดหลักได้ โค้ดต่อไปนี้มีหน้าที่รับผิดชอบในการคงการล็อกที่บล็อกเทรดหลักในการติดตามก่อนหน้า

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

    synchronized(lockedResource) {
        // The main thread requires lockedResource here
        // but it has to wait until LockTask finishes using it.
    }
}

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
  new LockTask().execute(data);

  synchronized (lockedResource) {
      // The main thread requires lockedResource here
      // but it has to wait until LockTask finishes using it.
  }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
  @Override
  protected Long doInBackground(Integer[]... params) {
      synchronized (lockedResource) {
          // This is a long-running operation, which makes
          // the lock last for a long time
          BubbleSort.sort(params[0]);
      }
  }
}

อีกตัวอย่างหนึ่งคือเทรดหลักของแอปที่กำลังรอผลลัพธ์จากเทรดผู้ปฏิบัติงาน ดังที่แสดงในโค้ดต่อไปนี้ โปรดทราบว่าการใช้ wait() และ notify() ไม่ใช่รูปแบบที่แนะนำใน Kotlin ซึ่งมีกลไกของตัวเองในการจัดการการทำงานพร้อมกัน เมื่อใช้ Kotlin คุณควรใช้กลไกเฉพาะของ Kotlin หากเป็นไปได้

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread's notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

public void onClick(View v) {
  WaitTask waitTask = new WaitTask();
  synchronized (waitTask) {
      try {
          waitTask.execute(data);
          // Wait for this worker thread’s notification
          waitTask.wait();
      } catch (InterruptedException e) {}
  }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
  @Override
  protected Long doInBackground(Integer[]... params) {
      synchronized (this) {
          BubbleSort.sort(params[0]);
          // Finished, notify the main thread
          notify();
      }
  }
}

ยังมีสถานการณ์อื่นๆ ที่อาจบล็อกเทรดหลักได้ ซึ่งรวมถึงเทรดที่ใช้ Lock, Semaphore ตลอดจนพูลทรัพยากร (เช่น พูลการเชื่อมต่อฐานข้อมูล) หรือกลไกการยกเว้นร่วมกัน (Mutex) อื่นๆ

คุณควรประเมินการล็อกที่แอปมีต่อทรัพยากรโดยทั่วไป แต่หากต้องการหลีกเลี่ยง ANR คุณควรดูการล็อกที่มีต่อทรัพยากรที่เทรดหลักต้องใช้

ตรวจสอบว่ามีการล็อกไว้เป็นระยะเวลาสั้นที่สุด หรือจะดีกว่านั้นหากประเมินว่าแอปจำเป็นต้องล็อกไว้ตั้งแต่แรกหรือไม่ หากคุณใช้การล็อกเพื่อกำหนดเวลาอัปเดต UI โดยอิงตามการประมวลผลของเทรดผู้ปฏิบัติงาน ให้ใช้กลไกต่างๆ เช่น onProgressUpdate() และ onPostExecute() เพื่อสื่อสารระหว่างเทรดผู้ปฏิบัติงานกับเทรดหลัก

Broadcast Receiver ที่ทำงานช้า

แอปสามารถตอบสนองต่อข้อความประกาศ เช่น การเปิดหรือปิดใช้โหมดบนเครื่องบินหรือการเปลี่ยนแปลงสถานะการเชื่อมต่อ โดยใช้ Broadcast Receiver ANR จะเกิดขึ้นเมื่อแอปใช้เวลานานเกินไปในการประมวลผลข้อความประกาศ

ANR จะเกิดขึ้นในกรณีต่อไปนี้

  • Broadcast Receiver ยังดำเนินการตามเมธอด onReceive ไม่เสร็จภายในระยะเวลาที่เหมาะสม
  • Broadcast Receiver เรียกใช้ goAsync และไม่สามารถเรียกใช้ finish ในออบเจ็กต์ PendingResult

แอปควรดำเนินการสั้นๆ ในเมธอด onReceive ของ BroadcastReceiver เท่านั้น อย่างไรก็ตาม หากแอปของคุณต้องมีการประมวลผลที่ซับซ้อนมากขึ้นอันเป็นผลมาจากข้อความประกาศ คุณควรเลื่อนงานออกไปเป็น ViewModel (ใช้ประโยชน์จากประสิทธิภาพของโครูทีน, ขอบเขต และผู้จัดส่งของ Kotlin) หากคาดว่างานจะใช้เวลาไม่เกิน 2-3 วินาที ตัวเก็บสถานะประเภทใดก็ได้ หรือ WorkManager สำหรับงานที่คาดว่าจะใช้เวลานานกว่า 2-3 วินาที

คุณสามารถใช้เครื่องมืออย่าง Traceview เพื่อระบุว่า Broadcast Receiver ดำเนินการที่ใช้เวลานานในเทรดหลักของแอปหรือไม่ ตัวอย่างเช่น รูปที่ 6 แสดงไทม์ไลน์ของ Broadcast Receiver ที่ประมวลผลข้อความในชุดข้อความหลักเป็นเวลาประมาณ 100 วินาที

รูปที่ 5 ไทม์ไลน์ Traceview แสดงการทำงานของ `BroadcastReceiver` ในเทรดหลัก

รูปที่ 5 ไทม์ไลน์ Traceview แสดงงาน BroadcastReceiver ในเทรดหลัก

ลักษณะการทำงานนี้อาจเกิดจากการดำเนินการที่ใช้เวลานานในเมธอด onReceive() ของ BroadcastReceiver ดังที่แสดงในตัวอย่างต่อไปนี้

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

ในสถานการณ์เช่นนี้ ขอแนะนำให้ย้ายการดำเนินการที่ใช้เวลานานไปยัง IntentService เนื่องจากใช้เทรดผู้ปฏิบัติงานเพื่อดำเนินการ โค้ดต่อไปนี้แสดงวิธีใช้ IntentService เพื่อประมวลผลการดำเนินการที่ใช้เวลานาน

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
  @Override
  protected void onHandleIntent(@Nullable Intent intent) {
      BubbleSort.sort(data);
  }
}

ผลจากการใช้ IntentService คือการดำเนินการที่ใช้เวลานานจะทำงานในเทรดผู้ปฏิบัติงานแทนเทรดหลัก รูปที่ 7 แสดงงานที่เลื่อนไปยังเทรดผู้ปฏิบัติงานในไทม์ไลน์ Traceview

รูปที่ 6 ไทม์ไลน์ Traceview ที่แสดงข้อความประกาศที่ประมวลผลในเทรดผู้ปฏิบัติงาน

รูปที่ 6 ไทม์ไลน์ Traceview ที่แสดงข้อความประกาศที่ประมวลผลในเทรดผู้ปฏิบัติงาน

Broadcast Receiver สามารถใช้ goAsync() เพื่อส่งสัญญาณไปยังระบบว่าต้องการเวลาเพิ่มเติมในการประมวลผลข้อความ อย่างไรก็ตาม คุณควรเรียกใช้ finish() ในออบเจ็กต์ PendingResult ตัวอย่างต่อไปนี้แสดงวิธีเรียกใช้ finish() เพื่อให้ระบบรีไซเคิล Broadcast Receiver และหลีกเลี่ยง ANR

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
  @Override
  protected Long doInBackground(Integer[]... params) {
      // This is a long-running operation
      BubbleSort.sort(params[0]);
      pendingResult.finish();
  }
}.execute(data);

อย่างไรก็ตาม การย้ายโค้ดจาก Broadcast Receiver ที่ช้าไปยังชุดข้อความอื่นและการใช้ goAsync() จะไม่แก้ไข ANR หากการออกอากาศอยู่ในเบื้องหลัง การหมดเวลาของ ANR จะยังคงมีผลอยู่