Load Bitmap Efficiently in Android [Part 2]
Chào mọi người, ở phần trước mình đã giới thiệu về một số kỹ thuật cơ bản trong lập trình Android để có thể làm việc hiệu quả hơn với Bitmap. Nếu đã bỏ qua bài trước, các bạn hãy bỏ chút thời gian để tham khảo lại ở đây nhé. Và ở bài viết hôm nay, mình sẽ tiếp tục với chủ đề Làm việc hiệu quả với ...
Chào mọi người, ở phần trước mình đã giới thiệu về một số kỹ thuật cơ bản trong lập trình Android để có thể làm việc hiệu quả hơn với Bitmap. Nếu đã bỏ qua bài trước, các bạn hãy bỏ chút thời gian để tham khảo lại ở đây nhé. Và ở bài viết hôm nay, mình sẽ tiếp tục với chủ đề Làm việc hiệu quả với Bitmap trong Android nhưng ở một khía cạnh khác, đó là Process Bitmap In Separate Thread với kỹ thuật Multi Threading.
Dẫn nhập
Như các bạn đã biết, Android có một thread chính để xử lý giao diện (UI) được gọi là Main Thread (UI Thread), mặc định một ứng dụng khi chạy, nếu chúng ta không chủ động tạo thread mới thì mọi xử lý đều được thực hiện trên Main Thread. Trong một số trường hợp cần xử lý các task cần performance cao hoặc có độ trễ nhất định, ví dụ như connect internet, đọc/ghi từ SD card hoặc decode Bitmap, nếu thực hiện chúng trên Main Thread rất có thể sẽ làm block UI của bạn, nhưng thật may là Android không cho phép bạn thực hiện Network Request trên Main Thread, cái này chỉ là lưu ý thêm thôi nhé. Cái mình đang muốn nhấn mạnh trong chủ đề của bài chia sẻ hôm nay, như tiêu đề, liên quan đến Bitmap. Vậy nếu decode Bitmap ở Main Thread, như đã nói ở trên là hoàn toàn không nên, nó có rất nhiều khả năng sẽ làm block UI của bạn mà nếu show Log, bạn sẽ thấy dòng warning "Skipped xxx frames! The application may be doing too much work on its main thread" kèm theo ứng dụng của bạn sẽ bị hanging trong giây lát. Chưa kể source data của chúng ta thường đc đọc từ SD card hoặc từ Network Request là những thứ có độ trễ nhất định, điều này ảnh hưởng lớn đến UI của bạn. Để khắc phục tất cả những điểm trên, best practise là xử lý decode bitmap bất đồng bộ ở một thread khác và load nó lên View bất cứ khi nào thực hiện xong. Hãy cũng tìm hiểu cách làm nhé.
Sử dụng AsynTask
Android cung cấp cho chúng ta một công cụ rất hữu ích để thực hiện các công việc ở Background Thread và handle kết quả ở Main Thread sau khi thực hiện xong, đó là AsyncTask. Đến với đoạn code mẫu bên dưới nhé.
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { imageViewReference = new WeakReference<ImageView>(imageView); } protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
Method decodeSampledBitmapFromResource thực hiện decode bitmap từ resource sử dụng kỹ thuật Load Scale Down Bitmap đã đề cập ở bài trước, và dĩ nhiên nó được thực hiện ở Background thread. Chúng ta sử dụng WeakReference ở đây để chắc chắn rằng ImageView có thể được GC thu hồi bất kỳ lúc nào và sẽ không gây ra leak memory.
Có một lưu ý khi sử dụng AsynTask ở đây, bởi lẽ AsynTask thực hiện một task bất đồng bộ ở background thread và nó không phụ thuộc vào Activity lifecycle. Vì vậy, thật cẩn thận và chắc chắn kiểm soát được lifecycle của AsynTask mà bạn đã define để tránh tình trạng leak memory. Cái này có thể mình sẽ chia sẻ ở các bài tiếp theo ở một chủ đề khác phù hợp hơn.
Quay lại với ví dụ ở trên, chúng ta sẽ thực hiện gọi nó như sau:
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
Handle Concurrency
Chúng ta xét đến một số trường hợp đặc biệt trong khi load Bitmap, ví dụ các component như ListView hay GridView cần load một số lượng Bitmap rất lớn. Mặt khác, để tối ưu hóa RAM Usage, Android sẽ tái sử dụng các child view này mỗi lần người dùng scroll view ( method getView trong Adapter sẽ được gọi mỗi lần scroll ). Giả sử ta cần load hình ảnh vào một ImageView và sử dùng 1 AsyncTask để thực hiện việc này - như ví dụ ở trên, rõ ràng chúng ta không thể biết được khi nào AsyncTask này được hoàn thành dẫn đến ImageView này sẽ không thể được tái sử dụng cho những child view khác. Mặt khác, nhiều AsyncTask mới sẽ được tạo ra và giữ reference tới cùng một ImageView ảnh hưởng đến performance cũng tính đúng đắn trong hiển thị của ứng dụng. Vì vậy, solution đưa ra ở đây là chúng ta sẽ kiểm tra trạng thái của một ImageView trước khi gắn vào cho nó một AsyncTask để load data. Cụ thể sẽ implement theo ví dụ bên dưới :
static class AsyncDrawable extends BitmapDrawable { private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } }
Chúng ta sẽ tạo ra một class kế thừa từ BitmapDrawable và giữ một instance của BitmapWorkerTask. Mục đích của class này là tạo mối liên hệ giữa AsyncTask và ImageView, dựa vào đó chúng ta sẽ xác định được task nào đang đc gắn với ImageView nào.
Method sau dùng để load data vào ImageView, và được sử dụng ở method getView ở Adapter.
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); } }
Method cancelPotentialWork sử dụng để check xem việc cancel một task có cần thực hiện hay không. Ta sử dụng method này trước khi gắn và thực thi một AsyncTask mới.
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; // If bitmapData is not yet set or it differs from the new data if (bitmapData == 0 || bitmapData != data) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true; }
Method getBitmapWorkerTask sử dụng để lấy AsyncTask instance ứng với ImageView cụ thể.
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; }
Và ở BitmapWorkerTask, ta sẽ hiển thị data lên view theo cách sau.
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } } }
Kết luận
Ở phần chia sẻ trên, chúng ta đã tìm hiểu thêm về cách làm việc với Bitmap ở Background thread như thế nào và kỹ thuật để tối ưu hóa chúng ở một số component phức tạp.