Các khái niệm và cách triển khai Jetpack Compose
Khi luồng giao diện người dùng của một ứng dụng Android bị chặn quá lâu, lỗi "Ứng dụng không phản hồi" (ANR) sẽ được kích hoạt. Nếu ứng dụng chạy ở nền trước, hệ thống sẽ hiển thị hộp thoại cho người dùng như minh hoạ trong hình 1. Hộp thoại ANR cho phép người dùng buộc thoát khỏi ứng dụng.
ANR là một sự cố của luồng chính trong ứng dụng, vì luồng này chịu trách nhiệm cập nhật giao diện người dùng nhưng không thể xử lý các sự kiện đầu vào của người dùng hoặc vẽ khiến người dùng thất vọng. Để biết thêm thông tin về luồng chính của ứng dụng, hãy xem bài viết Tổng quan về quy trình và luồng.
ANR được kích hoạt cho ứng dụng của bạn khi một trong các điều kiện sau xảy ra:
- Hết thời gian chờ điều phối đầu vào: Nếu ứng dụng của bạn chưa phản hồi một sự kiện đầu vào (chẳng hạn như thao tác nhấn phím hoặc chạm vào màn hình) trong vòng 5 giây.
- Thực thi dịch vụ: Nếu một dịch vụ do ứng dụng của bạn khai báo không thể hoàn tất quá trình thực thi
Service.onCreate()vàService.onStartCommand()/Service.onBind()trong vòng vài giây. **Service.startForeground()không được gọi**: Nếu ứng dụng của bạn sử dụngContext.startForegroundService()để bắt đầu một dịch vụ mới trên nền trước nhưng dịch vụ đó không gọistartForeground()trong vòng 5 giây.- Truyền đi ý định: Nếu
BroadcastReceiverchưa hoàn tất quá trình thực thi trong một khoảng thời gian nhất định. Nếu ứng dụng có bất kỳ hoạt động nào ở nền trước, thời gian chờ này là 5 giây. **JobSchedulertương tác**: NếuJobServicekhông trả về từJobService.onStartJob()hoặcJobService.onStopJob()trong vòng vài giây, hoặc nếu lệnh do người dùng yêu cầu bắt đầu và ứng dụng của bạn không gọiJobService.setNotification()trong vòng vài giây sau khiJobService.onStartJob()được gọi. Đối với các ứng dụng nhắm đến Android 13 trở xuống, lỗi ANR không hoạt động và không được báo cáo cho ứng dụng. Đối với các ứng dụng nhắm đến Android 14 trở lên, lỗi ANR phải rõ ràng và được báo cáo cho ứng dụng.
Nếu ứng dụng của bạn gặp các lỗi ANR, bạn có thể sử dụng hướng dẫn trong bài viết này để chẩn đoán và khắc phục vấn đề.
Khắc phục sự cố
Sau khi đã xác định sự cố, bạn có thể sử dụng các mẹo trong phần này để khắc phục các sự cố thường gặp.
Mã chậm trên luồng chính
Xác định các vị trí trong mã của bạn nơi luồng chính của ứng dụng bận trong hơn 5 giây. Tìm các trường hợp sử dụng đáng ngờ trong ứng dụng của bạn và cố gắng mô phỏng lỗi ANR.
Ví dụ: hình 2 cho thấy dòng thời gian Traceview trong đó luồng chính bận trong hơn 5 giây.

Hình 2. Dòng thời gian Traceview hiển thị một luồng chính bận
Hình 2 cho thấy hầu hết mã vi phạm xảy ra trong trình xử lý onClick(View), như minh hoạ trong ví dụ về mã sau:
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);
}
Trong trường hợp này, bạn nên di chuyển tác vụ chạy trong luồng chính sang một luồng thực thi. Khung Android có các lớp có thể giúp bạn di chuyển tác vụ sang một luồng worker. Hãy xem phần Luồng thực thi để biết thêm thông tin.
I/O trên luồng chính
Việc thực thi các thao tác I/O trên luồng chính là nguyên nhân phổ biến gây ra các thao tác chậm trên luồng chính, điều này dẫn đến các lỗi ANR. Trong Compose, các nhà phát triển thường vô tình kích hoạt các hoạt động đọc dữ liệu trên ổ đĩa (chẳng hạn như SharedPreferences hoặc lệnh gọi cơ sở dữ liệu) trong khi cố gắng lấy trạng thái ban đầu.
Thực thi các thao tác I/O chạy trong thời gian dài bên ngoài lớp giao diện người dùng. Sử dụng withContext(Dispatchers.IO) trong ViewModel hoặc tốt hơn là sử dụng một Kho lưu trữ trên lớp dữ liệu. Bạn nên chuyển tất cả các thao tác IO vào một luồng thực thi, như minh hoạ trong phần trước.
Một số ví dụ về hoạt động IO là hoạt động mạng và bộ nhớ. Để biết thêm thông tin, hãy xem bài viết Thực hiện các thao tác mạng và Lưu dữ liệu.
Tranh chấp khoá
Trong một số trường hợp, tác vụ gây ra lỗi ANR không được thực thi trực tiếp trên luồng chính của ứng dụng. Nếu một luồng thực thi lưu giữ khóa tài nguyên mà luồng chính yêu cầu để hoàn thành công việc, thì lỗi ANR có thể xảy ra.
Ví dụ: hình 3 cho thấy dòng thời gian Traceview nơi hầu hết tác vụ được thực hiện trên một luồng worker.

Hình 3. Dòng thời gian Traceview cho thấy tác vụ đang được thực thi trên một luồng worker
Tuy nhiên, nếu người dùng của bạn vẫn gặp lỗi ANR, bạn nên xem trạng thái của luồng chính trong Trình theo dõi thiết bị Android. Thông thường, luồng chính sẽ ở trạng thái RUNNABLE nếu đã sẵn sàng cập nhật giao diện người dùng và thường có tính đáp ứng.
Tuy nhiên, nếu luồng chính không thể tiếp tục thực thi thì luồng đó đang ở trạng thái BLOCKED và không thể phản hồi các sự kiện. Trạng thái hiển thị trên Trình theo dõi thiết bị Android dưới dạng Monitor (Theo dõi) hoặc Wait (Chờ), như minh hoạ trong hình 5.

Hình 4. Luồng chính trong trạng thái Theo dõi
Dấu vết sau đây cho thấy luồng chính của một ứng dụng bị chặn khi đang chờ tài nguyên:
...
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)
...
Việc xem lại dấu vết có thể giúp bạn tìm được mã đang chặn luồng chính. Đoạn mã sau chịu trách nhiệm giữ khóa chặn luồng chính trong dấu vết trước đó:
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]);
}
}
}
Một ví dụ khác là luồng chính của ứng dụng đang chờ kết quả từ một luồng worker, như minh hoạ trong mã sau. Lưu ý là bạn không nên dùng mẫu wait() và notify() trong Kotlin, mẫu này có các cơ chế riêng để xử lý tính năng đồng thời. Khi sử dụng Kotlin, bạn nên dùng các cơ chế dành riêng cho Kotlin nếu có thể.
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();
}
}
}
Một số tình huống khác có thể chặn luồng chính, bao gồm các luồng sử dụng Lock, Semaphore và nhóm tài nguyên (chẳng hạn như nhóm các kết nối cơ sở dữ liệu) hoặc các cơ chế loại trừ lẫn nhau (mutex) khác.
Bạn nên đánh giá các khoá mà ứng dụng của bạn đang nắm giữ đối với các tài nguyên nói chung, nhưng nếu muốn tránh các lỗi ANR, bạn nên xem xét các khoá được giữ lại đối với các tài nguyên cần thiết cho luồng chính.
Hãy đảm bảo các khoá được giữ trong khoảng thời gian ngắn nhất hoặc thậm chí tốt hơn là nên đánh giá xem ứng dụng có cần giữ lại ngay từ đầu hay không. Nếu bạn đang sử dụng khoá để xác định thời điểm cập nhật giao diện người dùng dựa trên quá trình xử lý luồng worker, hãy sử dụng các cơ chế như onProgressUpdate() và onPostExecute() để giao tiếp giữa luồng worker và các luồng chính.
Broadcast receiver (bộ thu phát sóng) chậm
Các ứng dụng có thể phản hồi thông báo phát đi, chẳng hạn như bật hoặc tắt chế độ trên máy bay hoặc sự thay đổi trạng thái kết nối bằng broadcast receiver. ANR xảy ra khi ứng dụng mất quá nhiều thời gian để xử lý thông báo phát đi.
Lỗi ANR xảy ra trong các trường hợp sau:
- Một bộ nhận tín hiệu truyền tin vẫn chưa hoàn tất việc thực thi phương thức
onReceivetrong khoảng thời gian đáng kể. - Bộ nhận tín hiệu truyền tin gọi
goAsyncnhưng không gọi đượcfinishtrên đối tượngPendingResult.
Ứng dụng của bạn chỉ được thực hiện các thao tác ngắn trong phương thức onReceive của BroadcastReceiver. Tuy nhiên, nếu ứng dụng của bạn yêu cầu xử lý phức tạp hơn do có thông báo phát đi, bạn nên chuyển nhiệm vụ đó cho ViewModel (tận dụng sức mạnh của các coroutine, phạm vi và trình điều phối Kotlin) nếu nhiệm vụ dự kiến mất tối đa vài giây, bất kỳ loại phần tử giữ trạng thái nào hoặc cho WorkManager đối với những nhiệm vụ dự kiến mất nhiều hơn vài giây.
Bạn có thể sử dụng các công cụ như Traceview để xác định xem broadcast receiver của bạn có thực thi các hoạt động diễn ra trong thời gian dài trên luồng chính của ứng dụng hay không. Ví dụ như hình 6 cho thấy dòng thời gian của broadcast receiver xử lý tin nhắn trên luồng chính trong khoảng 100 giây.

Hình 5. Dòng thời gian của Traceview cho thấy BroadcastReceiver hoạt động trên luồng chính
Hành vi này có thể do việc thực thi các hoạt động diễn ra trong thời gian dài trong phương thức onReceive() của BroadcastReceiver, như minh hoạ trong ví dụ sau:
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);
}
Trong những trường hợp như vậy, bạn nên di chuyển hoạt động diễn ra trong thời gian dài sang IntentService vì nó sử dụng một luồng worker để thực hiện công việc của mình.
Đoạn mã sau đây cho biết cách sử dụng IntentService để xử lý một hoạt động diễn ra trong thời gian dài:
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);
}
}
Nhờ sử dụng IntentService, các hoạt động diễn ra trong thời gian dài được thực hiện trên luồng worker thay vì luồng chính. Hình 7 cho thấy tác vụ được chuyển sang luồng worker trong dòng thời gian Traceview.

Hình 6. Dòng thời gian Traceview hiển thị thông báo phát đi được xử lý trên luồng worker
Bộ nhận tín hiệu truyền tin của bạn có thể sử dụng goAsync() để báo cho hệ thống biết cần thêm thời gian để xử lý thông báo. Tuy nhiên, bạn nên gọi finish() trên đối tượng PendingResult. Ví dụ sau đây cho thấy cách gọi finish() để cho phép hệ thống phục hồi bộ nhận tín hiệu truyền tin và tránh lỗi 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);
Tuy nhiên, việc di chuyển mã từ một bộ nhận tín hiệu truyền tin chậm sang một luồng khác và sử dụng goAsync() sẽ không khắc phục được lỗi ANR nếu việc truyền tin diễn ra ở chế độ nền.
Thời gian chờ lỗi ANR vẫn được áp dụng.