[Android 05] AsyncTask và những vấn đề kĩ thuật

1. AsyncTask là gì? 
Trong Android, Activity và Services, hầu hết tất cả các hàm gọi lại (callbacks) đều được chạy trong main thread. Rất dễ dàng để cập nhật UI, nhưng tiến trình hoặc I/O, những tiến trình nặng có thể làm cho UI đứng lại, và sẽ mắc vào bẫy ANR.
Có thể biện pháp khắc phục ở đây là cho tiến trình chạy trong một thread ở background. Một trong số các cách hữu dụng là sử dụng AsyncTask, được cung cấp trong framework để tạo điều kiện dễ dàng sử dụng trong thread background, và cũng trình diễn UI Thread trước, trong và sau khi background Thread hoàn thành.
Đơn giản AsyncTask là vậy, cụ thể thì nó có 4 phương thức chính được ghi đè: onPreExecute, doInBackground, onProgressUpdate, onPostExecute.
Hầu hết các lập trình viên Android, từ dân tửu lượng 1 cút đến dân tửu lượng vài can đều biết sử dụng AsyncTask như thế nào trong việc xử lý dữ liệu thông qua 4 phương thức đề cập ở trên. Nhưng để phân tầng level technical thì cần đến "1 vài kĩ thuật xử lý quan trọng với AsyncTask" làm trọng tài.
2. Vấn đề với AsyncTask có thể bạn sẽ gặp phải.
  • AsyncTask và Activity lifecycle: AsyncTask không gắn theo Activity instance life cycle. Nếu sử dụng AsyncTask bên trong 1 Activity, và sau đó xoay màn hình, chuyển từ poitrait thành landscape, sau đó Activity sẽ bị destroy, và 1 instance mới sẽ được khởi tạo. AsyncTask sẽ làm gì tiếp theo?
    Câu trả lời là nó sẽ không làm gì cả, ngoại trừ tiếp tục tiến trình nó đã chạy trươc đó. Nó sẽ tiếp tục sống cho tới khi hoàn thành.
    Vậy làm thế nào khi trường hợp: Một Activity đã chết, trong khi AsyncTask muốn update UI thông qua onProgressUpdate, và component bây giờ là null vì Activity chưa được khởi tạo 1 instance mới?
    Câu trả lời là sử dụng AsyncTask Loader. Nghe lạ phải không? Nó là một lớp con của Loaders và trình diễn các hàm tương tự AsyncTask, nhưng tốt hơn rất nhiều. Nó có thể xử lý khi cấu hình của Activity thay đổi một cách dễ dàng, và nó gắn liền với lifecycle của Activity hoặc Fragment.
    AsyncTaskLoader làm được tất cả những gì AsyncTask có thể làm được, và bất kì khi nào dữ liệu cần load vào vùng nhớ cho Activity hoặc Fragment xử lý, AsyncTaskLoaders có thể làm tốt hơn AsyncTask thông thường. 
  • Bạn đã bao giờ gặp trường hợp ANR vì dùng AsyncTask chưa?
    Nếu chưa, hãy thử nghĩ đến trường hợp này: AsyncTask có thể là 1 inner class của một Activity, và bạn có thể reference đến nó, truy cập vào bất cứ biến hoặc phương thức một cách chính xác.
    Tuy nhiên, nếu AsyncTask không phải là 1 inner class của 1 Activity, và dĩ nhiên bạn phải ném một Activity vào trong đó. Sau khi ném, một vấn đề xảy ra là AsyncTask sẽ reference đến Activity cho đến khi nó hoàn thành công việc ở background Thread.
    Nếu Activity bị kill hoặc finish trước khi AsyncTask hoàn thành công việc, AsyncTask vẫn reference tới Activity trước đó. Và Garbage Collector sẽ không hoàn thành công việc, dẫn đến vấn đề Memory Leak.
    Làm thế nào để giải quyết vấn đề này?

    Câu trả lời là WeakReference. Hãy xem ví dụ sau: 
  • Khi sử dụng, chỉ cần gọi trong Activity:
    new MyAsyncTask(this).execute("param1","param2");
    
    Có thể thấy, WeakReference làm cho đối tượng có thể dễ dàng bị trình thu rác của hệ thống dọndẹp, mở đường cho vùng nhớ cũ đã bị chiếm dụng để trình diễn, thực thi. Ở trong đó, sử dụngphương thức get() lấy data bên trong đó. Không quên kiểm tra null trước khi updateUI!
  • Đã bao giờ bạn thử cancel AsyncTask khi nó đang execute hay chưa? Nếu rồi thì bạn có thực sự làm cho nó dừng được không? Hay là thử thế này nhỉ? Vì nó có phương thức cancel() mà:
  • YourAsyncTask task = new YourAsyncTask();
    task.execute();
    task.cancel();
    
    Nó không dừng đâu!
    Chỉ có cách là cắm 1 cái cờ với 1 tên nào đó nhằm kiểm tra giá trị trả về của isCancelled() để trả về giá trị null trong doInBackground mà thôi, như này:

    class YourAsyncTask extends AsyncTask<Void, Void, Void> {
    @Override
    protected Void doInBackground(Void... params) {
    while(!isCancelled()) {
    ... doing long task stuff
    //Do something, you need, upload part of file, for example
    if (isCancelled()) {
    return null; // Task was detected as canceled
    }
    if (yourTaskCompleted) {
    return null;
     }
    }
    
    Lưu ý rằng: Nếu một AsyncTask bị cancel khi doInBackground vẫn đang thực thi thì phương thức onPostExecute sẽ không được gọi sau khi doInBackground trả về. Thay vào đó thì AsyncTask sẽ gọi onCancelled để chỉ ra rằng task đã được cancel trong quá trình thực thi mà thôi.
  • AsyncTask kế thừa class Thread. 
    Bạn nhầm rồi!!! Nhấn giữ phím Ctrl và click vào AsyncTask xem lại nào. 
    AsyncTask là một lớp abstract, và hoàn toàn không kế thừa Thread class. Nó có phương thức abstract doInBackground, cái mà bị ghi đè khi mà sử dụng để thực thi task. Nó được gọi bằng AsyncTask.call(). Nếu bạn đã biết về Concurrency thì bỏ qua, còn không thì xem câu dưới nè: 
    Executor là một phần của java.util.concurrent package.
    Hơn nữa, AsyncTask còn có đến 2 executors, đó là THREAD_POOL_EXECUTOR, sử dụng worker thread để thực thi tác vụ song song, và SERIAL_EXECUTOR, sử dụng để thực thi tác vụ theo hàng.
    Bởi vì những executor là static, do đó chỉ mỗi một object tồn tại. Vậy làm sao để tạo multiple object? Đơn giản, hãy tạo nhiều object AsyncTask 😅😅😅.
    Do đó, nếu bạn muốn thực thi tác vụ với executor mặc định là SerialExecutor, thì các tác vụ sẽ nằm trong 1 queue và thực thi theo hàng, và muốn thực thi tác vụ song song thì hãy dùng THREAD_POOL_EXECUTOR, nó sẽ thực thi tác vụ song song.
    Hãy xem một ít code bên dưới. Đừng hoa mắt 💀
    public class MainActivity extends Activity {
    private Button bt;
    private int CountTask = 0;
    private static final String TAG = "AsyncTaskExample";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    bt = (Button) findViewById(R.id.button);
    bt.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    BackgroundTask backgroundTask = new BackgroundTask ();
    Integer data[] = { ++CountTask, null, null };
    // Task Executed in thread pool ( 1 )
    backgroundTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, data);
    // Task executed Serially ( 2 )
    // Uncomment the below code and comment the above code of Thread
    // pool Executor and check
    // backgroundTask.execute(data);
    Log.d(TAG, "Task = " + (int) CountTask + " Task Queued");
    }
    });
    }
    private class BackgroundTask extends AsyncTask<Integer, Integer, Integer> {
    int taskNumber;
    @Override
    protected Integer doInBackground(Integer... integers) {
    taskNumber = integers[0];
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    Log.d(TAG, "Task = " + taskNumber + " Task Running in Background");
    publishProgress(taskNumber);
    return null;
    }
    @Override
    protected void onPreExecute() {
    super.onPreExecute();
    }
    @Override
    protected void onPostExecute(Integer aLong) {
    super.onPostExecute(aLong);
    }
    @Override
    protected void onProgressUpdate(Integer... values) {
    super.onProgressUpdate(values);
    Log.d(TAG, "Task = " + (int) values[0]
    + " Task Execution Completed");
    }
    }
    }
    
  • Và sau đây là kết quả trả về:
  • Sắp xếp thứ tự của execution là một vấn đề  không được quan tâm từ khi ban đầu AsyncTask được giới thiệu. Bắt đầu với DONUT, được chuyển thành pool của threading, cho phép execute các tác vụ đa luồng song song, với HONEYCOMB, các tác vụ được thực thi đơn luồng, tránh lỗi ứng dụng gây nên bởi đa luồng.
    Nhưng nếu bạn thực sự muốn thực thi song song, bạn có thể sử dụng executeOnExecutor (java.util.concurrent.Executor, Object[]) với THREAD_POOL_EXECUTOR.
    Có thể ví dụ sau đây là một minh chứng cho việc kiểm tra version của thiết bị trước khi thực thi tác vụ:
  • Task task = new Task();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
    task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, data);
    else
    task.execute(data);
HappyCoding!

Nhận xét

Bài đăng phổ biến từ blog này

[Android-02] LayoutParams trong Android