Để 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:
Nhưng tại sao nhiều bạn vẫn sử dụng AsyncTask?
Nội dung chính của bài viết
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?
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.
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()
hayonCancelled()
đượ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.
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.
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.
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. Cùng nhau thảo luận nhé!