12/08/2018, 16:42

Loading Large Bitmaps Efficiently

Bạn có bao giờ tự hỏi, một ảnh với dung lượng 100mb thì nó được lưu vào memory dung lượng bao nhiêu? Làm thế nào mà nó bị OOM vậy? Ở đây chắc ai code Android cũng từng gặp vấn đề về dung lượng, độ phân giải ảnh khi load vào memory. Điều đầu tiên mình muốn nhấn mạnh là mình khuyên các bạn nên sử ...

Bạn có bao giờ tự hỏi, một ảnh với dung lượng 100mb thì nó được lưu vào memory dung lượng bao nhiêu? Làm thế nào mà nó bị OOM vậy? Ở đây chắc ai code Android cũng từng gặp vấn đề về dung lượng, độ phân giải ảnh khi load vào memory. Điều đầu tiên mình muốn nhấn mạnh là mình khuyên các bạn nên sử dụng Glide, Picasso hoặc Fresco để load ảnh, đó là những thư viện đã được Google hay Facebook khuyên nên dùng. Nếu muốn tìm hiểu thì dành chút thời gian đọc bài này nhé, mình muốn chia sẻ một ít thông tin mình biết về việc loading một ảnh lớn và thực sự công việc bên trong của nó là như thế nào?

Load Bitmap Into Memory

Rất đơn giản, bạn chỉ cần decode ảnh của mình, sử dụng BitmapFactory

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage);
imageView.setImageBitmap(bitmap);

Bây giờ bạn hãy kiểm tra bitmap size trong memory. bitmap.getByteCount() trả về size là 12262248 Bytes = 12.3 Mb Nhìn vào con số kia bạn có thể bị nhầm lẫn, vì bạn nghĩ ảnh chỉ có size khoảng 3.5 Mb thôi, vậy tại sao lại có số kia, nguyên nhân là :

Hình ảnh được nén khi ở trên ổ cứng, dưới định dạng JPG, PNG hay một số các định dạng tương tự. Khi bạn lưu vào trong bộ nhớ máy, nó không bị nén nữa và cần rất nhiều bộ nhớ để có thể lưu được hết các điểm ảnh.

Steps:

- Get size của hình ảnh không load vào memory

- Tính tỷ lệ với kích thước của ảnh

- Load bitmap vào memory với các kích thước đã tính

BitmapFactory.Options

Dùng class này để thực hiệp step 1

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage, options);

Bạn có thể nhìn thấy khi set options.inJustDecodeBounds = true; , điều này có nghĩa là mình không muốn load bitmap vào memory. Chúng ta chỉ cần get thông tin awidth, height, ... của ảnh. Chúng ta có thể tính toán được với các thông tin nhận được

options.outHeight : 1126
options.outWidth : 2000
options.bitmap : null

Reducing Image Size (In Memory)

Trong BitmapFactory.Options có giá trị inSampleSize, nếu bạn có ảnh 1000x1000, và bạn set inSampleSize = 2 trước khi decode, bạn sẽ có ảnh 500x500 sau khi decode. Tương tự vậy nếu ảnh trước khi decode là 200x400 và set inSampleSize = 5 thì sau khi decode sẽ có ảnh 40x80

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inSampleSize = 3; 
BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage, options);

Đoạn code trên set cứng giá trị là 3, nhưng giá trị của inSampleSize sẽ cần tính động theo kích thước của hình ảnh hiển thị, nó phụ thuộc vào bạn, nghĩa là bạn có thể viết thuật toán để tính nếu cần thiết. Trong document của android, giá trị này được tính dựa trên 2 giá trị reqWidth và reqHeight :

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and awidth of image
    final int height = options.outHeight;
    final int awidth = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || awidth > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = awidth / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and awidth larger than the requested height and awidth.
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Và chúng ta sẽ sử dụng hàm tính toán này :

options.inSampleSize = calculateInSampleSize(options, 500,500);
options.inJustDecodeBounds = false;
Bitmap smallBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage, options);

Ta có thể thấy, inJustDecodeBounds đã được chuyển thành false và bitmap được decode với biến options Bây giờ bitmap.getByteCount() sẽ trả về 3.1Mb, đó chính là memory size. Như mình nói ở trên, anh được nén khi lưu trong ổ cứng, nó lơn hơn nhiều khi ta load vào trong memory. Vậy ta đã giảm từ 12.3 MB xuống 3.1MB, tương đương với 75% trong memory, wow

Reducing Image Size (In Disk)

Bước tiếp theo, ta cần giảm size ảnh trong ổ cứng, chúng ta có thể dùng method compress của Bitmap, về chất lượng của ảnh, giá trị 100 nghĩa là chất lượng tương đương nhau.

ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
byte[] bitmapdata = bos.toByteArray();

Khi ta tính kích thước thật của ảnh là 1.6MB, bây giờ thử thay đổi chất lượng ảnh và kiểm tra lại kích thước xem sao

bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos);

Tôi đã thay chất lượng ảnh từ 100 về 50 và kết quả là 24.4 KB. Cần đổi định dạng ảnh về .JPEG nếu muốn thay đổi chất lượng của bitmap, ở định PNG ta không thể thay đổi được chất lượng của ảnh. Chúng ta đã giảm size của file từ 1.6MB về 24.4KB Đây là kết quả : Nguồn: https://developer.android.com/topic/performance/graphics/load-bitmap.html https://android.jlelse.eu/loading-large-bitmaps-efficiently-in-android-66826cd4ad53

0