HIỂN THỊ BITMAP LỚN HIỆU QUẢ

Tìm hiểu về các cách phổ biến khi xử lý và nạp đối tượng Bitmap để đáp ứng được với các thành phần giao diện và tránh vượt quá giới hạn bộ nhớ dành cho ứng dụng của bạn.

Một đối tương Bitmap nếu sử dụng không đúng cách có thế làm tiêu hao bộ nhớ sẵn có dành cho ứng dụng của bạn và gây ra 1 exception: java.lang.OutofMemoryError: bitmap size exceeds VM budget.

Một vài lý do khiến việc nạp (loading) đối tượng Bitmap trở nên khó khăn:

  • Thông thường điện thoại sẽ giới hạn tài nguyên hệ thống, thiết bị Android có thể có ít nhất 16Mb bộ nhớ cho mỗi ứng dụng

  • Bitmap là tốn rất nhiều dữ liệu. Một bức ảnh chụp có độ phân giải 2592×1936 pixels nếu Bitmap được cài đặt sử dụng ARGB_8888 -> Nó sẽ tốn tới 2592* 1936 * 4 bytes = ~ 19MB. tức là vượt giới hạn cho phép trên mỗi ứng dụng

  • Giao diện Ứng dụng android thường xuyên đòi hỏi một số Bitmap được nạp cũng 1 lúc. Các thành phần như ListView, GridView, ViewPager thường xuyên được sử dụng để hiển thị nhìu bitmap cùng 1 lúc, và cả các vùng chưa được hiển thị chỉ = 1 cái phẩy tay.

I, Loading Large Bitmaps Efficiently: Nạp đối tượng Bitmap lớn hiệu quả

Hình ảnh có nhiều hình dạng và kích cỡ và đôi khi chúng lớn hơn cần thiết cho ứng dụng.

Giả sử bạn đang làm việc với thiết bị có bộ giới hạn, cách tốt nhât bạn sẽ chỉ muốn tải 1 phiên bản với độ phân giải thấp hơn trong ứng dụng của bạn.

Phiên bản này nên khớp với kích cỡ của các thành phần dùng để hiển thị chúng.

Một hình ảnh với độ phân giải cao không cung cấp bất kì lợi ích nào nhìn thấy được nhưng chúng vẫn chiếm bộ nhớ quý giá và phải chịu thêm chi phí hoạt động do bổ sung mở rộng cho việc co dãn.

Bài hướng dẫn này giúp chúng ta giải mã (decoding) đối tượng Bitmap mà không gây vượt quá giới hạn bộ nhớ dành cho mỗi ứng dụng bằng cách tải (loading) 1 phiên bản mẫu nhỏ hơn (subsample) so với ảnh gốc (origin).

Đọc kích thước Bitmap và kiểu của chúng – (Read Bitmap Dimensions and Type)

Lớp BitmapFactory cung cấp một vài phương thức (decodeByteArray(), decodeFile(), decoderResource() …) để giải mã dùng cho việc tạo Bitmap từ nhiều nguồn (resources) khác nhau. Các phương thức này cố gắng để cấp phát (allocate) bộ nhớ cho việc tạo ra Bitmap do do dễ dàng dẫn đến 1 ngoại lệ OutOfMemory.

Một loại phương thức giải mã này thêm vào 1 đối số cho phép bạn chỉ định các tuỳ chọn (options) dành cho việc giải mã thông qua lớp BitmapFactory.Options. Trong lớp này cài đặt thuộc tính inJustDecodeBounds tới true khi thực hiện việc giải mã để tránh cấp phát bộ nhớ, thuộc tính này không trả về đối tượng Bitmap (null) nhưng trả về các thông số: outWidht, outHeight, outMimeType. Kĩ thuật này cho phép bạn đọc kích thước và kiểu dữ liệu của hình ảnh trước khi xây dựng (và cấp pháp bộ nhớ) đối tượng Bitmap.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

Để tránh ngoại lệ java.lang.OutofMemoryError, đọc kích thước của Bitmap trước khi giải mã chúng, trừ khi bạn tuyệt đối tin tưởng vào nguồn cung cấp cho bạn các dữ liệu hình ảnh có kích thước phù hợp với bộ nhớ có sẵn của bạn.

Tải một phiên bản thu nhỏ (Scaled) vào bộ nhớ

Bây giờ sau khi biết được kích cỡ của hình ảnh, chúng ta có thể sử dụng để quyết định xem hình ảnh đầy đủ sẽ được nạp vào bộ nhớ hay một phiên bản thu nhỏ (subsampled) nên được thay thế. Dưới đây là một vài yếu tố để xem xét:

  • Ước tính bộ nhớ được sử dụng cho việc tải 1 hình ảnh đầy đủ (not scaled).
  • Tổng số lượng bộ nhớ sẵn sàng cho việc bản tải hình ảnh này và bất kì yêu cầu bộ nhớ khác trong ứng dụng của bạn.
  • Kích thước mục tiêu của ImageView hay thành phần UI mà hình ảnh sẽ được nạp vào.
  • Kích thước của màn hình và mật độ điểm ảnh (dpi) của thiết bị hiện tại

Ví dụ, sẽ không có giá trị khi tải một hình ảnh 1024×768 pixel vào bộ nhớ khi cuối cùng nó được hiển thị trên một hình ảnh thu nhỏ (thumbnail) với kích thước 128×96 pixel của một ImageView.

Để nói cho các bộ giải mã nạp một phiên bản nhỏ hơn vào bộ nhớ. Gán thuộc tính inSampleSize là True trong đối tượng BitmapFactory.Options của bạn. Cho ví dụ, một hình ảnh với độ phân giải 2048×1536 được giải mã với inSampleSize là 4 sẽ tạo ra một Bitmap với kích cỡ khoảng 512×384 (1/4 so với mẫu). Bitmap được tạo ra chỉ nạp vào bộ nhớ mất 0.75Mb chứ không phải là 12Mb khi tải một hình ảnh đầy đủ (giả sử Bitmap được cấu hình với ARGB_8888). Dưới đây là phương thức để tính toán một giá trị kích thước mẫu (sample size) dựa trên chiều rộng và chiều cao của mục tiêu:

public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight)
{
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize =1;
    if(height > reqHeight || width > reqWidth){
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;
        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while((halfHeight / inSampleSize)> reqHeight && (halfWidth / inSampleSize)> reqWidth){
            inSampleSize *=2;
        }
    }
    return inSampleSize;
}

Để sử dụng phương thức trên đầu tiên cài đặt inJustDecodeBounds to True, thêm vào các tuỳ chọn (Options Object) với giá trị inSampleSiez mới và 1 gán lại inJustDecodeBounds tới False:

public static Bitmap decodeSampledBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight) {
    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds =true;
    BitmapFactory.decodeResource(res, resId, options);

    // Decode bitmap with inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

Với phương thức này dễ dàng để tải một bitmap kích thước lớn tuỳ ý vào một ImageView hiển thị nhỏ (thumbnail).

Bạn cũng thực hiện tương tự quá trình này để giải mã Bitmap từ các nguồn khác nhau bằng cách thay thế các phương thức BitmapFactory.decode* thích hợp nếu cần thiết.

II, Xử lý Bitmap không ảnh hưởng tới UI Thread

Các phương thức để giải mã Bitmap BitmapFactory.decode* được nhắc tới trong phần trước Không nên được thực thi trong main UI thread (luồng chính chạy tất thành phần trong Android) nếu nguồn cung cấp dữ liệu được đọc từ ổ đĩa (disk) hoặc một địa chỉ mạng (network localtion) – hoặc bất cứ nguồn nào khác nếu không phải bộ nhớ.

Các dữ liệu từ các nguồn này thường cần thời gian để tải và không thể đoán trước vì còn phụ thuộc vào nhiều yếu tố (tốc độ đọc trên ổ cứng hoặc mạng, kích cỡ của hình ảnh, tốc độc CPU …). Nếu 1 trong các luồng ngăn chặn việc tương tác UI thread và không hồi đáp, sau đó hệ thống sẽ cho phép người dùng đóng nó.

Trong phần này bạn sẽ được học cách xử lý các Bitmap trong một background thread (Luồng chạy ngầm) sử dụng AsyncTask.

Sử dụng AsyncTask

Lớp AsyncTask cung cấp một cách dễ dàng để thực hiện một số nhiệm vụ (task) trong background thread và công bố kết quả trở lại UI thread.

Sau đây là 1 ví dụ cho việc tải và hiển thị một hình ảnh lớn (large image) tới ImageView sử dụng AsyncTask và phương thức decodeSampledBitmapFromResource() được nói tới ở trên:

class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{

private final WeakReference<ImageView> imageViewReference;
private int data =0;
public BitmapWorkerTask(ImageView imageView){
    // Use a WeakReference to ensure the ImageView can be garbage collected
    imageViewReference = new WeakReference<ImageView>(imageView);
}

// Decode image in background.
@Override
protected Bitmap doInBackground(Integer...params){
   data = params[0];
   return decodeSampledBitmapFromResource(getResources(), data,100,100));
}

// Once complete, see if ImageView is still around and set bitmap.
@Override
protectedvoid onPostExecute(Bitmap bitmap){
if(imageViewReference != null && bitmap != null){
    final ImageView imageView = imageViewReference.get();
    if(imageView != null){
       imageView.setImageBitmap(bitmap);
    }
  }
}
}

Các WeakReference cho ImageView đảm bảo rằng các AsyncTask không ngăn cản ImageView và bất cứ điều gì khi tham chiếu đến nó không còn sử dụng sau đó nó sẽ bị thu hồi và loại bỏ (garbage collected). Không có gì bảo đảm các ImageView vẫn còn sống khi luồng xử lý hoàn thành, vì vậy bạn cũng phải kiểm tra các ánh xạ trong onPostExecute (). Khi đủ điều kiện được thu gom ví dụ khi ImageView không còn tồn tại (null) hoặc người dùng điều hướng sang một activity khác hoặc thay đổi các cài đặt trước khi nhiệm vụ được hoàn thành chúng sẽ tự động được trình thu gom rác thải loại bỏ.

Cuối cùng để thực hiện việc tải Bitmap bất đồng bộ (asynchronously), chúng ta tạo ra một luồng mới và thực thi chúng:

public void loadBitmap(int resId,ImageView imageView){
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

Demo project: DownloadSourceCode

Screenshot:

Bitmap

Tham khảo:


All Rights Reserved