แนวคิดและการติดตั้งใช้งาน Jetpack Compose
เมื่อเธรด UI ของแอป Android ถูกบล็อกนานเกินไป ระบบจะทริกเกอร์ข้อผิดพลาด "แอปพลิเคชันไม่ตอบสนอง" (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 แสดงให้เห็นว่าโค้ดที่ทำให้เกิดปัญหาส่วนใหญ่เกิดขึ้นในแฮนเดิล 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 ที่แสดงงานที่กำลังดำเนินการในเทรดผู้ปฏิบัติงาน
แต่หากผู้ใช้ยังคงพบ ANR คุณควรดูสถานะของเทรดหลักใน Android Device Monitor โดยปกติแล้ว เทรดหลักจะอยู่ในสถานะ RUNNABLE หากพร้อมที่จะอัปเดต UI และโดยทั่วไปจะตอบสนองได้ดี
แต่หากเทรดหลักไม่สามารถกลับมาทำงานต่อได้ เทรดหลักจะอยู่ในสถานะ BLOCKED และไม่สามารถตอบสนองต่อเหตุการณ์ได้ สถานะจะแสดงใน Android Device Monitor เป็นตรวจสอบหรือรอ ดังที่แสดงในรูปที่ 5

รูปที่ 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 ในเทรดหลัก
ลักษณะการทำงานนี้อาจเกิดจากการดำเนินการที่ใช้เวลานานในเมธอด 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 จะยังคงมีผลอยู่