+2

[Android Performance]Khắc phục triệt để khi ứng dụng của bạn bị "Treo"

Thông thường chúng ta rất hay gặp phải tình trạng crash apps, đó là khi ứng dụng hiện tại không thể hoạt động được nữa mà chỉ có thể bắt buộc tạm dừng (force close). Nhưng có một trạng thái khác ở một mức độ nghiêm trọng thấp hơn đó là Application Not Responding "Ứng dụng bị treo", tại sao tôi nói nó ít nghiêm trọng hơn là bởi vì ứng dụng của bạn vẫn có thể tiếp tục sử dụng bình thường sau khi chờ đợi (Wait). Ứng dụng của chúng ta sau khi public cho mọi người sử dụng mà muốn khẳng định được mức độ chuyên nghiệp của Team & Cty. Ít nhất mức độ xảy ra lỗi trên cần phải giảm dần về 0, vậy câu hỏi LÀM THẾ NÀO ? Câu trả lời sẽ giúp bạn ngay ở phía dưới đây 😃

1. Ứng dụng để tái lặp

Chúng ta tạo ra một ứng dụng Android để tái lặp ANRS Dialog xuất hiện như nào, để từ đó tìm hiểu về nguyên nhân và đưa cách khắc phục. Mô tả ứng dụng : (Hình ảnh) Rất đơn giản chỉ tạo ra 1 button (GO) sau khi click vào -> TextView update số từ 1 - 20 sau mỗi lần nghỉ (sleep) giữa các vòng lặp là 1s (Tổng thời gian trễ : 20s)

MainActivity.java

public class MainActivity extends AppCompatActivity {
    TextView tv; //for class wide reference to update status

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //get the references to on screen items
        tv = (TextView) findViewById(R.id.textView);
        //handle button presses
        findViewById(R.id.button).setOnClickListener(new doButtonClick());
    }

    class doButtonClick implements View.OnClickListener {
        public void onClick(View v) {
            tv.setText("Processing, please wait.");
            ThisTakesAWhile();
            tv.setText("Finished.");
        }
    }

    private void ThisTakesAWhile() {
        //mimic long running code
        int count = 0;
        do {
            SystemClock.sleep(1000);
            count++;
            tv.setText("Processed " + count + " of 20.");
        } while (count < 20);
    }
}

Vì UI cực kì đơn giản nên các bạn tự xây dựng nhé. 😃

Kết quả: sau khi chạy , tôi click vào "GO" thì nhận được thông báo Application Not Responding Phần TextView hiển thị "Finished" chứ không chạy lần lượt từ 1 - 20

2. Nguyên nhân do đâu

Nhìn vào Logcat lúc này thấy được dòng thông báo :

I/Choreographer: Skipped 1200 frames! The application may be doing too much work on its main thread.

=> Do Main thread đang phải làm quá nhiều việc cùng 1 lúc. Nhìn lại một chút ở function này xem có sai sót ở đâu không ?

private void ThisTakesAWhile() {
        //mimic long running code
        int count = 0;
        do {
            SystemClock.sleep(1000);
            count++;
            tv.setText("Processed " + count + " of 10.");
        } while (count < 20);
    }

À vấn đề là UI thread đang bị chiếm giữ trong 20s vì sau khi main thread ở trong trạng thái nghỉ 1s thì ta tương tác với UI tv.setText("Processed " + count + " of 10."); mà không hề có sự giải phóng UI thread.

ANRS dialog (Application Not Responding) được quản lý bởi Activity Manager & Window Manager , nó sẽ đưa ra thông báo nếu xảy ra 1 trong những điều kiện sau :

  • UI Thread bị chiếm để thực hiện tác vụ quá lâu sau 5s vẫn chưa kết thúc. Dẫn đến trình trạng những UI event bị block và không thể phản hồi.
  • Xử lý BroadcastReceiver quá 10s mà vẫn chưa hoàn tất.
  • Xử lý những tác vụ như Network connection, Database connection, Dowload File, Read File… trong Service và thời gian phải kết thúc là 20s

No response to an input event (such as key press or screen touch events) within 5 seconds.

A BroadcastReceiver hasn't finished executing within 10 seconds.

A service runs in the main thread of its hosting process—the service does not create its own thread and does not run in a separate process (unless you specify otherwise).

3. Làm thế nào để check được đoạn code của bạn có thể bị lỗi ANRS không ?

Để phát hiện ra nguy cơ từ những dòng code liên quan đến vòng lặp, handle file, hay services nào đó có thể gặp vấn đề ANRS hay không? Google đã đưa ra những cách dưới đây giúp bạn áp dụng để detect.

a. Strict mode

Using StrictMode helps you find accidental I/O operations on the main thread while you’re developing your app. You can use StrictMode at the application or activity level.

Hãy đặt đoạn code dưới đây vào hàm onCreate() trong class extend Application hoặc trong Activity của bạn:

 public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
     }
     super.onCreate();
 }

Kiểm tra logcat để xem những trường hợp có thể được detect từ StrictMode.

b. Enable background ANR dialogs

Android shows ANR dialogs for apps that take too long to process the broadcast message only if Show all ANRs is enabled in the device’s Developer options. For this reason, background ANR dialogs are not always displayed to the user, but the app could still be experiencing performance issues.

c. Traceview

You can use Traceview to get a trace of your running app while going through the use cases and identify the places where the main thread is busy.

Cách sử dụng bạn có thể tham khảo ở đây

4. Cách khắc phục

Đến đây bạn cũng đã biết rõ ràng nguyên nhân của ví dụ ban đầu chúng ta đang gặp phải là gì rồi, giờ chúng ta sẽ tiến hành fix nó. Rõ ràng việc TextView được set từ 1 - 20 là 1 công việc mất 20s nên tôi dùng một AsyncTask để handle nó. Có 1 cách làm đơn giản hơn nữa là bạn dùng RxJava tôi sẽ đề cập đến ở 1 chủ đề sau nhé. Tôi chuyển method ThisTakesAWhile thành 1 AsyncTask , nhưng nó chỉ là 1 inner class trong MainActivity cho đơn giản:

class ThisTakesAWhile extends AsyncTask<Integer, Integer, Integer>{
        int numcycles;  //total number of times to execute process
        protected void onPreExecute(){
            //Executes in UI thread before task begins
            //Can be used to set things up in UI such as showing progress bar
            count=0;    //count number of cycles
            processing=true;
            tv.setText("Processing, please wait.");
            bt.setText("STOP");
        }
        protected Integer doInBackground(Integer... arg0) {
            //Runs in a background thread
            //Used to run code that could block the UI
            numcycles=arg0[0];  //Run arg0 times
            //Need to check isCancelled to see if cancel was called
            while(count < numcycles && !isCancelled()) {
                //wait one second (simulate a long process)
                SystemClock.sleep(1000);
                //count cycles
                count++;
                //signal to the UI (via onProgressUpdate)
                //class arg1 determines type of data sent
                publishProgress(count);
            }
            //return value sent to UI via onPostExecute
            //class arg2 determines result type sent
            return count;
        }
        protected void onProgressUpdate(Integer... arg1){
            //called when background task calls publishProgress
            //in doInBackground
            if(isCancelled()) {
                tv.setText("Cancelled! Completed " + arg1[0] + " processes.");
            } else {
                tv.setText("Processed " + arg1[0] + " of " + numcycles + ".");
            }
        }
        protected void onPostExecute(Integer result){
            //result comes from return value of doInBackground
            //runs on UI thread, not called if task cancelled
            tv.setText("Processed " + result + ", finished!");
            processing=false;
            bt.setText("GO");
        }
        protected void onCancelled() {
            //run on UI thread if task is cancelled
            processing=false;
            bt.setText("GO");
        }
    }

Tôi giải thích thêm một chút nếu bạn nào chưa quen dùng AsyncTask, ở method doInBackground chúng ta đã làm công việc của main thread là dừng trong 1s và gia tăng giá trí count lên 1. Nhưng có sự khác biệt ở đây là chúng ta làm việc này ở background (chạy ngầm) chứ không cùng thread với main thread, nhưng sau mỗi lần chạy xong nó sẽ truyền giá trị count về method onProgressUpdate, lặp lại đến khi count = 20 thì mới dừng toàn bộ quá trình update TextView các bạn nhé. Như vậy thì UI thread không bị blocked mà liên tục được giải phóng.

Kết quả sau khi áp dụng AsyncTask:

5. Tổng kết

Như vậy chúng ta đã hiểu thêm về nguyên nhân xảy ra tình trạng ANRS và cách khắc phục. Trên thực tế nếu như bạn biết mình sẽ phải thực hiện một công việc mất bao nhiêu lâu thì cần làm nó ở tầng background để tránh lỗi trên mà công việc trở lên mượt hơn rất nhiều. Một ứng dụng không mắc phải lỗi này thì chắc chắn trải nghiệm người dùng sẽ tốt hơn rất nhiều, team thực hiện project đó cũng đã có thêm điểm 1+ trong tiêu chí ứng dụng chuyên nghiệp. Bài viết của tôi kết thúc ở đây, mong rằng các bạn sẽ tìm thấy sự hữu ích cho mình !


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí