ANR

Stay organized with collections Save and categorize content based on your preferences.

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 họa 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.

Hình 1. Hộp thoại ANR hiển thị cho người dùng

Hình 1. Hộp thoại ANR hiển thị cho người dùng

ANR là một sự cố của luồng chính trong ứng dụng, 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 sẽ đượ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:

  • Khi hoạt động ở nền trước, ứng dụng của bạn không phản hồi một sự kiện đầu vào hoặc BroadcastReceiver (chẳng hạn như sự kiện nhấn phím hoặc chạm vào màn hình) trong vòng 5 giây.
  • Mặc dù bạn không có hoạt động nào trên nền trước, nhưng BroadcastReceiver của bạn chưa hoàn thành việc thực thi trong một khoảng thời gian đáng kể.

Nếu ứng dụng của bạn đang gặp các sự cố 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à chẩn đoán vấn đề

Android sẽ cung cấp một số phương tiện để thông báo cho bạn biết ứng dụng của mình có vấn đề, đồng thời giúp bạn chẩn đoán vấn đề đó. Nếu bạn đã phát hành ứng dụng của mình, Android vitals có thể gửi cảnh báo cho bạn về sự cố đang xảy ra, cùng các công cụ chẩn đoán để giúp bạn tìm ra sự cố.

Android vitals

Android vitals có thể giúp cải thiện hiệu suất hoạt động của ứng dụng bằng cách thông báo cho bạn qua Play Console khi ứng dụng gặp quá nhiều sự cố ANR. Android vitals định nghĩa về việc quá nhiều sự cố ANR là khi một ứng dụng:

  • Hiển thị ít nhất một ANR trong ít nhất 0,47% số phiên hàng ngày.
  • Hiển thị 2 ANR trở lên trong ít nhất 0,24% số phiên hằng ngày.

Phiên hằng ngày tức là một ngày mà ứng dụng của bạn được sử dụng.

Để biết thông tin về cách Google Play thu thập dữ liệu Android vitals, hãy xem tài liệu Play Console.

Chẩn đoán các sự cố ANR

Có một vài mẫu phổ biến cần xem xét khi chẩn đoán ANR:

  1. Ứ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.
  2. Ứ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.
  3. 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.
  4. Luồng chính bị chặn đang chờ một khối đồng bộ hóa cho một hoạt động mất nhiều thời gian đang diễn ra trên luồng khác.
  5. 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 trong tình huống tắc nghẽn. Để biết thêm thông tin, vui lòng xem phần Deadlock trên Wikipedia.

Các kỹ thuật sau có thể giúp bạn tìm nguyên nhân gây ra các sự cố ANR.

Chế độ nghiêm ngặt

Sử dụng StrictMode sẽ giúp bạn tìm thấy các hoạt động I/O ngẫu nhiên trên luồng chính khi đang phát triển ứng dụng của mình. Bạn có thể dùng StrictMode ở cấp ứng dụng hoặc hoạt động.

Bật hộp thoại ANR trong nền

Android sẽ hiển thị hộp thoạ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 sự cố ANR được bật trong tùy chọn dành cho nhà phát triển thiết bị. Vì lý do này, hộp thoạ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 sự cố 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ị nghẽn. Để biết thêm 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 dấu vết

Android lưu trữ thông tin theo dõi khi gặp sự cố 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 sự cố 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 tùy chọn cho phép nhà phát triển Báo cáo lỗi 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 vấn đề, 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ị nghẽn

Hình 2. Dòng thời gian Traceview hiển thị một luồng chính bị nghẽn

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 họa 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. 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 sự cố 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ạngLư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ụ như hình 4 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 luồng thực thi.

Hình 4. Dòng thời gian Traceview cho thấy tác vụ đang được thực thi trên một luồng
thực thi

Hình 4. Dòng thời gian Traceview cho thấy tác vụ đang được thực thi trên một luồng thực thi

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 đá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 Theo dõi hoặc Chờ, như minh họa trong hình 5.

Hình 5. Luồng chính trong trạng thái Theo dõi

Hình 5. Luồng chính trong trạng thái Theo dõi

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 thực thi, như minh họa trong mã sau. Lưu ý là bạn không nên dùng mẫu wait()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 khóa mà ứng dụng của đ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 khóa đượ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 khóa đượ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 khóa để 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 thực thi, hãy sử dụng các cơ chế như onProgressUpdate()onPostExecute() để giao tiếp giữa luồng thực thi 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ẽnThuậ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 tin nhắn thông báo, chẳng hạn như bật hoặc tắt chế độ trên máy bay hoặc thay đổi trạng thái kết nối bằng thiết bị thu phát sóng. 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.

ANR xảy ra trong các trường hợp sau:

  • Một bộ thu phát sóng 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ể.
  • Bộ thu phát sóng gọi goAsync() và không thể gọi finish() trên đối tượng PendingResult.

Ứ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 IntentService.

Bạn có thể sử dụng các công cụ như Traceview để xác định xem bộ thu phát sóng 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 bộ thu phát sóng xử lý tin nhắn trên luồng chính trong khoảng 100 giây.

Hình 6. Dòng thời gian của Traceview cho thấy BroadcastReceiver hoạt động trên luồng chính

Hình 6. 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 họa 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 các hoạt động diễn ra trong thời gian dài sang IntentService vì nó sử dụng luồng thực thi để thực hiện các hoạt động đó. Đ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 thực thi thay vì luồng chính. Hình 7 cho thấy tác vụ được trì hoãn tới luồng thực thi trong dòng thời gian Traceview.

Hình 7. Dòng thời gian Traceview hiển thị thông báo phát đi được xử lý trên luồng thực thi

Hình 7. Dòng thời gian Traceview hiển thị thông báo phát đi được xử lý trên luồng thực thi

Bộ thu phát sóng 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 bộ thu phát sóng và tránh sự cố 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ộ thu phát sóng 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 phát đi diễn ra ở chế độ nền. Thời gian chờ ANR vẫn được áp dụng.

Để biết thêm thông tin chi tiết về lỗi ANR, vui lòng 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.