Lời khuyên dùng Android Thread cho tác vụ Network!!

0

Để thực hiện các tác vụ tới kết nối server như download/upload file… thì giải pháp nhiều bạn nghĩ tới là sử dụng Android Thread. Trong đó dễ nhất là dùng AsyncTask.

Nhưng lời khuyên của mình là không nên dùng nó cho các tác vụ kiểu như vậy?

Tại sao mình lại khuyên các bạn không nên sử dụng AsyncTasks cho các tác vụ cần thời gian, đặc biệt là các request network? Tất nhiên, bạn hoàn toàn có thể dụng nó.

Tuy nhiên, đây được thiết kế lý tưởng dành cho các tác vụ ngắn (cùng lắm là vài giây). Nhưng với các tác vụ liên quan đến network, bạn có chắc là chỉ cần vài giây? Có lẽ không ai khẳng định được vì còn phụ thuộc vào tốc độ mạng của người dùng.

Theo tài liệu chính thức của Google:

If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor,ThreadPoolExecutor and FutureTask.Official Android Developer documentation.

Nhưng tại sao nhiều bạn vẫn sử dụng AsyncTask?

Không nên sử dụng AsyncTask khi nào

Yeah, bởi vì AsyncTask rất dễ sử dụng

Cái này thì mình công nhận. Nó rất dễ sử dụng, nó giúp ứng dụng thực hiện các tác vụ tách biệt với Main UI thread, và cập nhật kết quả lên UI. Có thể hiểu AsyncTask là một phiên bản được Google viết lại từ Thread trong Java để các developer dễ sử dụng hơn.

Chỉ cần vài dòng code đơn giản là bạn đã có thể tạo một AsyncTask.

public class MinimalAsyncTask extends AsyncTask<Params, Void, Result> {
     
    @Override
    protected Result doInBackground(Params... param) {
        //do some task on background thread
    }
 
}

doInBackground() là một abstract method được định nghĩa trong lớp AsyncTask. Vậy bạn sẽ cần phải override nó. Ngoài ra, nó là method duy nhất trong đó là chạy dưới background Thread.

Mình sẽ không nói lại cách sử dụng như thế nào ở bài viết này. Bạn có thể tìm lại bài viết cũ mà mình đã hướng dẫn chi tiết: Các sử dụng AsyncTask từng bước.

Đây là một ví dụ :

public class DataFetcherAsyncTask extends AsyncTask<Params, Progress, Result> {
     
    @Override
    protected void onPreExecute() { ... }
     
    @Override
    protected Result doInBackground(Params... param) {
        try {
            if (!isCancelled()) {
                doSomeLongOperation(param[0]);
            }
        } catch (Exception e) {
            // Do nothing. Or just print error.
        }
     
        return Result;
    }
 
    @Override
    protected void onProgressUpdate(Progress... progress) { ... }
 
    @Override
    protected void onPostExecute(Result result) { ... }
 
    @Override
    protected void onCancelled(Result result) { ... }
 
}

Nếu UI Thread có lúc nào đó mà không cần sử dụng kết quả trả về từ AsyncTask (ví dụ lúc destroy app). UI Thread có thể gửi yêu cầu dừng AsyncTask bằng hàm cancel(true)

public class MainActivity extends AppCompatActivity {
     
    private DataFetcherAsyncTask mDataFetcherAsyncTask;
     
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mDataFetcherAsyncTask = new DataFetcherAsyncTask().execute(Params... param);
        ...
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
 
        // Cancel the task. Interrupt background thread.
        mDataFetcherAsyncTask.cancel(true);
    }
 
    ...
 
}

Quay trở lại vấn đề chính: Tại sao bạn không sử dụng nó?

Và đây là nguyên do, mời bạn đọc tiếp.

Lý do không nên sử dụng Android Thread kiểu AsyncTask

Chà, hãy tưởng tượng bạn mở một Activity và start một AsyncTask trong onCreate().

Sau đó, bạn xoay màn hình 1 lần, 2 lần… hàng chục lần xem thế nào?

Giải thích thêm: Mỗi khi bạn xoay màn hình, Activity sẽ bị destroy và tạo lại Activity từ đầu. Mục đích của việc này là để Android có thể load lại layout cho phù hợp Portrait và Landscape modes.

Vấn đề ở đây là: khi xoay màn hình, Activity bị kill nhưng không. Và khi xoay 20 lần thì sẽ có 20 cái AsyncTask được tạo và chạy cùng với một data đầu vào.

Còn nữa, tất cả 20 AsyncTask này khi thực hiện xong sẽ cố gắng update kết quả lên UI thông qua onPostExecute(). Tuy nhiên, Activity đã bị hủy rồi còn đâu. Vấn đề này có thể gây ra lỗi crash nếu bạn xử lý không khéo. Chưa kể có thể gây lỗi Memory Leak nếu bạn địng nghĩa UI elements là static.

The AsyncTask should therefore be declared as a standalone or static inner class so that the worker thread avoids implicit references to outer classes. Using non-static inner classes for long running operations is always a bad practice, not just in Android.Official Android Developer documentation.

Bạn đã thấy sự bất cập của nó chưa? Nếu bạn vẫn nhất quyết sử dụng thì cần phải xử lý việc cancel AsyncTask thật khéo. Không thì hỏng hết cả.

Và đây là cách của mình.

Một cách cancel AsyncTask đúng

Khi nhận yêu cầu cancel, AsyncTask sẽ bỏ qua gọi hàm onPostExecute() – và gọi một trong các cancel callback: onCancelled() hoặc onCancelled(Result).

Bạn cũng có thể sử dụng các cancel callback này để cập nhập giao diện hoặc hiển thị thông báo cho người biết về việc tác vụ bị hủy.

Một AsyncTask có 3 trạng thái:

  • PENDING – một AsyncTask được khởi tạo nhưng chưa thực thi (hàm execute() chưa được gọi).
  • RUNNING – execute() được gọi, AsyncTask đang thực thi tác vụ.
  • FINISHED – AsyncTask ở trạng thái này khi hàm onPostExecute() hay onCancelled() được gọi và hoàn thành.
public class StandaloneActivity extends AppCompatActivity {
     
    private static final String IMAGE_TO_DOWNLOAD_URL = "http://...";
 
    private DataFetcherAsyncTask mDataFetcherAsyncTask;
    public ProgressBar mProgressBar;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
         
        setContentView(R.layout.activity_file_download);
         
        mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
        ...
        fetchData();
        ...
    }
 
    private void fetchData() {
        mDataFetcherAsyncTask = new DataFetcherAsyncTask(this);
        mDataFetcherAsyncTask.execute(IMAGE_TO_DOWNLOAD_URL);
    }
 
    public void onDataFetched(Bitmap bitmap) {
         
    }
 
    public void onDataCancelled() {
         
    }
 
    private void reloadData() {
        if (mDataFetcherAsyncTask != null && mDataFetcherAsyncTask.getStatus() != AsyncTask.Status.RUNNING) {
            fetchData();
        }
    }
 
    @Override
    protected void onDestroy() {
        // Cancel the task. Interrupt background thread
 
        mDataFetcherAsyncTask.cancel(true);
    }
 
    ...
 
}

Static inner class và WeakReference

Cá nhân mình thích viết AsycTask thành một  class riêng. Mục đích là để code dễ đọc và có thể tái sử dụng ở nhiều nơi trong dự án.

Tuy nhiên,  bạn cũng có thể địng nghĩa một class AsyncTask trong một Activity như một inner class và sử dụng WeakReference. Với cách làm này sẽ tránh được  memory leak.

Most memory leaks involving threads are caused by objects staying in memory longer than required. Threads and handlers can keep objects unreachable from a thread GC root even when they are not used anymore.Official Android Developer documentation.

Các inner class sẽ chứa các tham chiếu không tường mình (implicit references) tới class mà chúng định nghĩa ở đó. Điều này sẽ gây ra vấn đề memory leak vì Activity chứa AsyncTask không thể bị dọn dẹp bởi cơ chế GC.

Vì vậy, chúng ta cần phải sử dụng static inner class, vì chúng sẽ chỉ tham chiếu tới global class, chứ không phải instance object.

Tất cả các tham chiếu tường minh (explicit references) từ static inner class tới các instance objects sẽ tồn tại khi Thread thực thi.

Static inner classes do not have access to instance fields of the outer class. This can be a limitation if an application would like to execute a task on a worker thread and access or update an instance field of the outer instance. For this need, java.lang.ref.WeakReference can help.Official Android Developer documentation.

Giải pháp: định nghĩa một AsyncTask là một private static inner class.

public class StaticInnerClassActivity extends AppCompatActivity {
     
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        new DataFetcherAsyncTask(new WeakReference<>(this));
        ...
    }
 
    ...
 
    /**
     * Static inner classes don't hold implicit references to their
     * enclosing class, so the Activity instance won't be leaked across
     * configuration changes.
     */
    private static class DataFetcherAsyncTask extends AsyncTask<String, Void, Bitmap> {
     
        private WeakReference<StaticInnerClassActivity> mWeakActivity;
 
        public DataFetcherAsyncTask(WeakReference<StaticInnerClassActivity> activity) {
            this.mWeakActivity = activity;
        }
 
        @Override
        protected Bitmap doInBackground(String... param) {
           Bitmap bitmap = null;
            try {
                if (!isCancelled()) {
                    bitmap = downloadImageFile(param[0]);
                }
            } catch (Exception e) {
                // Do nothing. Or just print error.
            }
            return bitmap;
        }
 
        ...
 
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
             
            ImageDownloadActivity localReferenceActivity = mWeakActivity.get();
            if (localReferenceActivity != null) {
                localReferenceActivity.mProgressBar.setVisibility(View.GONE);
                localReferenceActivity.onDataFetched(bitmap);
            }
        }
    }
 
}

Cách tái sử dụng code AsyncTask cho Android Thread

Như mình đã nhắc đến ở phần trên bài viết, mình thích viết AsyncTask ra riêng một class. Mục đích thì không gì khác là để tái sử dụng code, tránh bị trùng lặp code.

Hãy thử tưởng tượng, bạn phát triển một ứng dụng tải hình ảnh từ server và hiển thị lên giao diện. Và một màn hình khác (ví dụ như màn hình profile, cũng cần tải avatar và hiển thị).

Thế tại sao bạn không tạo một AsyncTask chuyên trị việc tải ảnh về và cập nhật lên UI.

//IDownloadImageAsyncTaskHolder.java
public interface IDownloadImageAsyncTaskHolder {
 
    void onDataFetched(Bitmap bitmap);
    void onDataCancelled();
    void showProgressBar();
    void hideProgressBar(); 
 
}

//DownloadImageFileAsyncTask.java
public class DownloadImageFileAsyncTask extends AsyncTask<String, Void, Bitmap> {
 
    private IDownloadImageAsyncTaskHolder mActivity;
 
    public DownloadImageFileAsyncTask(IDownloadImageAsyncTaskHolder activity) {
        mActivity = activity;
    }
 
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        if (mActivity != null) {
            mActivity.showProgressBar();
        }
    }
 
    @Override
    protected Bitmap doInBackground(String... param) {
        Bitmap bitmap = null;
        try {
            if (!isCancelled()) {
                bitmap = downloadImageFile(param[0]);
            }
        } catch (Exception e) {
            // Do nothing. Or just print error.
        }
        return bitmap;
    }
 
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        if (mActivity != null) {
            mActivity.hideProgressBar();
            mActivity.onDataFetched(bitmap);
        }
    }
 
    @Override
    protected void onCancelled() {
        super.onCancelled();
        if (mActivity != null) {
            mActivity.hideProgressBar();
            mActivity.onDataCancelled();
        }
    }
}

Kết quả theo dõi tài nguyên giữa sử dụng static và không static AsyncTask

Để những lời nói có chút thuyết phục, mình đã tiến hành thống kê bộ nhớ Android trong hai trường hợp khai báo static AsyncTask và không khai báo static, xem Android thread thế nào nhé.

Như hình bên dưới, Static AsyncTask bên trái, còn không static AsyncTask bên phải với test case là xoay màn hình liên tục.

so sánh các Android Thread

Bạn nhìn hình có thể thấy, nếu không sử dụng static inner class thì bộ nhớ bị sử dụng rất nhiều, việc giải phóng bộ nhớ cũng khó khăn hơn.

#Tạm kết

Qua bài viết này, trong các loại Android Thread thì AsyncTask là lại dễ dùng nhất. Nhưng không nên sử dụng nó một cách tùy tiện.

Bản thân nó không được thiết kế để thực hiện các tác vụ rất dài. Do vậy, với các request tới server thì không nên sử dụng AsyncTask.

Nếu bạn không ngại sử dụng thư viện thì mình khuyên chân thành là nên sử dựng các thư viện như Volley cho các request tới server. Hoặc Glide, Picasso cho việc tải ảnh từ server. Những thư viện này đã thực hiện rất tốt, bạn sẽ không phải lo lắng về các vấn đề như memory leak các kiểu.

Bình luận. Đặt câu hỏi cũng là một cách học

avatar
  Theo dõi bình luận  
Thông báo