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

รูปที่ 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 หรือใช้ Repository
ใน เลเยอร์ข้อมูลจะดีกว่า เราขอแนะนำให้ย้ายการดำเนินการ I/O ทั้งหมดไปยังเธรดผู้ปฏิบัติงาน ดังที่แสดงในส่วนก่อนหน้า
ตัวอย่างการดำเนินการ I/O ได้แก่ การดำเนินการเครือข่ายและการดำเนินการพื้นที่เก็บข้อมูล ดูข้อมูลเพิ่มเติมได้ที่ การดำเนินการเครือข่ายและการบันทึกข้อมูล
การช่วงชิงล็อก
ในบางสถานการณ์ งานที่ทำให้เกิด ANR จะไม่ได้ดำเนินการในเธรดหลักของแอปโดยตรง หากเธรดผู้ปฏิบัติงานล็อกทรัพยากรที่เธรดหลักต้องใช้เพื่อทำงานให้เสร็จสมบูรณ์ ANR อาจเกิดขึ้นได้
ตัวอย่างเช่น รูปที่ 3 แสดงไทม์ไลน์ Traceview ที่งานส่วนใหญ่ดำเนินการในเธรดผู้ปฏิบัติงาน

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

รูปที่ 4 เธรดหลักอยู่ในสถานะ Monitor
การติดตามต่อไปนี้แสดงเธรดหลักของแอปที่ถูกบล็อกเนื่องจากรอทรัพยากร
...
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 ของ
a BroadcastReceiver อย่างไรก็ตาม หากแอปต้องมีการประมวลผลที่ซับซ้อนมากขึ้นอันเป็นผลมาจากข้อความประกาศ คุณควรเลื่อนงานไปยัง
ViewModel (ใช้ประโยชน์จากโครูทีน ขอบเขต และ Dispatcher ของ Kotlin)
หากคาดว่างานจะใช้เวลาไม่กี่วินาทีเท่านั้น, ตัวเก็บสถานะประเภทใดก็ได้
หรือไปยัง WorkManager สำหรับงานที่คาดว่าจะใช้เวลานานกว่าไม่กี่วินาที
คุณสามารถใช้เครื่องมือต่างๆ เช่น Traceview เพื่อระบุว่า Broadcast Receiver ดำเนินการที่ใช้เวลานานในเธรดหลักของแอปหรือไม่ ตัวอย่างเช่น รูปที่ 6 แสดงไทม์ไลน์ของ Broadcast Receiver ที่ประมวลผลข้อความในเธรดหลักประมาณ 100 วินาที

รูปที่ 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 แสดงข้อความประกาศที่ประมวลผลในเทรดผู้ปฏิบัติงาน
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 จะยังคงมีผล