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, vui lòng xem 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 sự kiện nhập (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ụng
Context.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
BroadcastReceiver
chư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 cứ hoạt động nào trong nền trước, thời gian chờ này là 5 giây. - Tương tác với JobScheduler: Nếu
JobService
khô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 đề.
Phát hiện vấn đề
Nếu đã phát hành ứng dụng, bạn có thể sử dụng Android vitals để xem thông tin về lỗi ANR cho ứng dụng của mình. Bạn có thể dùng các công cụ khác để phát hiện lỗi ANR trong trường này, nhưng hãy lưu ý rằng, khác với Android vitals, công cụ bên thứ ba không thể báo cáo lỗi ANR trên các phiên bản Android cũ (Android 10 trở xuống).
Android vitals
Android vitals có thể giúp bạn theo dõi và cải thiện tỷ lệ ANR của ứng dụng. Android vitals đo lường một số tỷ lệ ANR:
- Tỷ lệ ANR: Là tỷ lệ phần trăm số người dùng hoạt động hằng ngày gặp phải bất kỳ loại lỗi ANR nào.
- Tỷ lệ lỗi ANR mà người dùng nhận thấy: Là tỷ lệ phần trăm người dùng hoạt động hằng ngày gặp phải ít nhất 1 lỗi ANR mà người dùng nhận thấy. Hiện tại, chỉ những lỗi ANR thuộc loại
Input dispatching timed out
được coi là do người dùng nhận thấy. - Tỷ lệ ANR nhiều lần: Là tỷ lệ phần trăm người dùng hoạt động hằng ngày gặp phải ít nhất 2 lỗi ANR.
Người dùng hoạt động hằng ngày là người dùng riêng biệt dùng ứng dụng của bạn trong một ngày trên một thiết bị, có thể có nhiều phiên hoạt động. Nếu một người dùng sử dụng ứng dụng của bạn trên nhiều thiết bị trong một ngày, thì từng thiết bị đó cũng sẽ đóng góp vào số lượng người dùng đang hoạt động trong ngày đó. Nếu nhiều người dùng sử dụng cùng một thiết bị trong một ngày, thì trường hợp này chỉ được tính là một người dùng đang hoạt động.
Tỷ lệ lỗi ANR mà người dùng nhận thấy là một chỉ số quan trọng chính, có nghĩa là tỷ lệ này ảnh hưởng đến khả năng người dùng phát hiện được ứng dụng của bạn trên Google Play. Đây là một chỉ số quan trọng vì các lỗi ANR mà chỉ số này tính đến luôn xảy ra khi người dùng tương tác với ứng dụng, do đó gây gián đoạn nhiều nhất.
Play đã xác định 2 ngưỡng hành vi xấu cho chỉ số này:
- Ngưỡng hành vi xấu chung: Ít nhất 0,47% số người dùng hoạt động hằng ngày gặp phải lỗi ANR mà người dùng nhận thấy trên tất cả các kiểu máy.
- Ngưỡng hành vi xấu trên mỗi thiết bị: Ít nhất 8% số người dùng hoạt động hằng ngày gặp phải lỗi do người dùng nhận thấy, đối với một kiểu máy duy nhất.
Nếu ứng dụng của bạn vượt quá ngưỡng hành vi xấu chung, thì ứng dụng đó có thể khó được phát hiện hơn trên tất cả các thiết bị. Nếu ứng dụng của bạn vượt quá ngưỡng hành vi xấu trên mỗi thiết bị ở một số thiết bị, thì ứng dụng đó có thể khó được phát hiện hơn trên các thiết bị đó và cảnh báo có thể sẽ xuất hiện trên trang thông tin của bạn trên Cửa hàng Play.
Android vitals có thể cảnh báo cho bạn qua Play Console khi ứng dụng gặp quá nhiều lỗi ANR.
Để biết thông tin về cách Google Play thu thập dữ liệu Android vitals, vui lòng xem tài liệu về Play Console.
Chẩn đoán lỗi ANR (Ứng dụng không phản hồi)
Có một vài mẫu phổ biến cần xem xét khi chẩn đoán lỗi ANR:
- Ứng dụng đang thực hiện các thao tác chậm liên quan đến I/O trên luồng chính.
- Ứng dụng đang thực hiện một phép tính mất nhiều thời gian trên luồng chính.
- Luồng chính đang thực hiện một lệnh gọi liên kết đồng bộ tới một quy trình khác, và quy trình này sẽ mất nhiều thời gian để trả lệnh về lại.
- Luồng chính bị chặn đang chờ một khối đồng bộ hoá cho hoạt động mất nhiều thời gian đang diễn ra trên luồng khác.
- Luồng chính đang bị tắc nghẽn với một luồng khác, trong quá trình hoặc qua một lệnh gọi liên kết. Luồng chính không những đang đợi một hoạt động mất nhiều thời gian kết thúc mà còn đang gặp tình huống tắc nghẽn. Để biết thêm thông tin, vui lòng xem phần Tắc nghẽn trên Wikipedia.
Các kỹ thuật sau có thể giúp bạn xác định nguyên nhân dẫn đến lỗi ANR.
HealthStats
HealthStats
cung cấp các chỉ số về trạng thái của ứng dụng bằng cách ghi lại tổng thời gian người dùng và hệ thống, thời gian của CPU, số liệu thống kê về đài, mạng, thời gian bật/tắt màn hình và chuông báo thức. Thông tin này có thể giúp đo lường mức sử dụng CPU tổng thể và mức tiêu hao pin.
Gỡ lỗi
Debug
giúp kiểm tra ứng dụng Android trong quá trình phát triển, bao gồm cả việc theo dõi và phân bổ số lượng để xác định hiện tượng giật và độ trễ trong ứng dụng. Bạn cũng có thể sử dụng Debug
để xem bộ đếm thời gian chạy và bộ nhớ gốc, cũng như các chỉ số về bộ nhớ có thể giúp xác định mức sử dụng bộ nhớ của một quy trình cụ thể.
ApplicationExitInfo
ApplicationExitInfo
có trên Android 11 (API cấp 30) trở lên và cung cấp thông tin về lý do thoát khỏi ứng dụng. Lý do bao gồm lỗi ANR, sắp hết bộ nhớ, ứng dụng gặp sự cố, sử dụng CPU quá mức, tình trạng gián đoạn do người dùng, gián đoạn do hệ thống hoặc các thay đổi quyền khi bắt đầu chạy.
Chế độ nghiêm ngặt
Việc sử dụng StrictMode
giúp bạn tìm thấy
thao tác I/O ngẫu nhiên trên luồng chính khi bạn đang phát triển ứng dụng của mình.
Bạn có thể sử dụng StrictMode
ở cấp ứng dụng hoặc hoạt động.
Bật hộp thoại lỗi ANR trong nền
Android sẽ hiển thị hộp thoại lỗi ANR cho các ứng dụng mất quá nhiều thời gian để xử lý thông báo phát đi với điều kiện mục Hiển thị tất cả các lỗi ANR được bật trong phần Tuỳ chọn cho nhà phát triển trên thiết bị. Vì lý do này, hộp thoại lỗi ANR trong nền không phải lúc nào cũng hiển thị cho người dùng, nhưng ứng dụng vẫn có thể gặp vấn đề về hiệu suất.
Traceview
Bạn có thể sử dụng Traceview để theo dõi ứng dụng đang chạy khi đang xem xét các trường hợp sử dụng và xác định những vị trí mà luồng chính bận. Để biết thông tin về cách sử dụng Traceview, vui lòng xem bài viết Phân tích bằng Traceview và dmtracedump.
Kéo tệp theo dõi
Android lưu trữ thông tin theo dõi khi gặp lỗi ANR. Trên các bản phát hành hệ điều hành cũ, chỉ có một tệp /data/anr/traces.txt
trên thiết bị.
Trên các bản phát hành hệ điều hành mới hơn, có nhiều tệp /data/anr/anr_*
.
Bạn có thể truy cập các dấu vết của lỗi ANR từ một thiết bị hoặc trình mô phỏng bằng cách sử dụng Cầu gỡ lỗi Android (adb) làm thư mục gốc:
adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>
Bạn có thể ghi lại báo cáo lỗi từ thiết bị thực bằng cách sử dụng tuỳ chọn Tạo báo cáo lỗi cho nhà phát triển trên thiết bị hoặc lệnh báo cáo lỗi adb trên máy phát triển của mình. Để biết thêm thông tin, vui lòng xem bài viết Ghi lại và đọc báo cáo lỗi.
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 cho chúng ta 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 thực thi (worker thread). Vui lòng xem nội dung Luồng thực thi để biết thêm thông tin chi tiết.
IO trên luồng chính
Việc thực thi các hoạt động IO 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. Bạn nên chuyển tất cả các hoạt động IO vào một luồng thực thi, như minh họa 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 chi tiết, vui lòng xem bài viết Thực hiện các thao tác mạng và Lưu dữ liệu.
Khóa nội dung
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 của Traceview trong đó phần lớn công việc là đượ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.
Dấu vết dưới đâ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.
Tình trạng tắc nghẽn
Tình trạng tắc nghẽn sẽ xảy ra khi một luồng chuyển sang trạng thái chờ vì một tài nguyên cần thiết được giữ lại trong một luồng khác, đồng thời đang chờ tài nguyên do luồng đầu tiên lưu giữ. Nếu luồng chính của ứng dụng rơi vào trường hợp này, thì nhiều khả năng lỗi ANR sẽ xảy ra.
Tắc nghẽn là một hiện tượng được nghiên cứu kỹ lưỡng trong khoa học máy tính và có các thuật toán ngăn chặn cụ thể mà bạn có thể sử dụng để tránh tình trạng này.
Để biết thêm thông tin chi tiết, vui lòng xem bài viết về Thuật toán tắc nghẽn và Thuật toán ngăn ngừa tắc nghẽn trên Wikipedia.
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 broadcast receiver vẫn chưa hoàn tất việc thực thi phương thức
onReceive()
trong khoảng thời gian đáng kể. - Một broadcast receiver gọi
goAsync()
và không gọi đượcfinish()
trênPendingResult
.
Ứng dụng của bạn chỉ nên thực hiện các thao tác ngắn trong
onReceive()
của một
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 IntentService
.
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 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 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.
Broadcast receiver 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 cho thấy cách gọi hàm finish() để cho phép hệ thống phục hồi broadcast receiver 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 broadcast receiver 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 thông báo diễn ra ở chế độ nền.
Thời gian chờ lỗi ANR vẫn được áp dụng.
GameActivity
Thư viện GameActivity
đã giảm lỗi ANR trong các nghiên cứu điển hình về trò chơi và ứng dụng được viết bằng C hoặc C++. Nếu thay thế hoạt động gốc hiện có bằng GameActivity
, bạn có thể giảm tình trạng chặn luồng giao diện người dùng và ngăn chặn một số lỗi ANR xảy ra.
Để biết thêm thông tin về lỗi ANR, hãy xem bài viết Duy trì khả năng thích ứng của ứng dụng. Để biết thêm thông tin chi tiết về luồng, vui lòng xem phần Hiệu suất luồng.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Đánh thức quá nhiều lần