Load Bitmap Efficiently in Android [Part 2]
This post hasn't been updated for 6 years
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.
Tài liệu tham khảo
https://developer.android.com/training/displaying-bitmaps/process-bitmap.html
All Rights Reserved