Tải Bitmaps lớn một cách hiệu quả

Lưu ý: Có một số thư viện tuân theo các phương pháp hay nhất để tải hình ảnh. Bạn có thể sử dụng các thư viện này trong ứng dụng của mình để tải hình ảnh theo cách tối ưu nhất. Bạn nên sử dụng thư viện Glide. Thư viện này tải và hiển thị hình ảnh nhanh lẫn mượt mà nhất có thể. Các thư viện tải hình ảnh phổ biến khác bao gồm Picasso từ Square, Coil từ Instacart và Fresco từ Facebook. Các thư viện này đơn giản hóa hầu hết các thao tác phức tạp liên quan đến bitmap và các loại hình ảnh khác trên Android.

Hình ảnh có đủ hình dạng và kích thước. Trong nhiều trường hợp, chúng lớn hơn yêu cầu đối với giao diện người dùng ứng dụng (UI) thông thường. Ví dụ như ứng dụng Thư viện hệ thống hiển thị ảnh được chụp bằng máy ảnh của thiết bị Android thường có độ phân giải cao hơn nhiều so với mật độ màn hình của thiết bị.

Vì bạn đang làm việc với bộ nhớ hạn chế, nên bạn chỉ muốn tải phiên bản có độ phân giải thấp hơn trong bộ nhớ. Phiên bản có độ phân giải thấp hơn phải khớp với kích thước của thành phần giao diện người dùng hiển thị thành phần đó. Hình ảnh có độ phân giải cao hơn không đem lại lợi ích nào rõ ràng nhưng vẫn chiếm bộ nhớ quan trọng, và phải chịu thêm chi phí hiệu suất do bổ sung khi mở rộng quy mô.

Bài này sẽ hướng dẫn bạn cách giải mã các bitmap lớn mà không vượt quá giới hạn bộ nhớ cho mỗi ứng dụng, bằng cách tải một phiên bản mẫu nhỏ hơn vào bộ nhớ.

Đọc kích thước và loại Bitmap

Lớp BitmapFactory cung cấp một số phương pháp giải mã (decodeByteArray(), decodeFile(), decodeResource(), v.v.) để tạo Bitmap từ nhiều nguồn. Chọn phương thức giải mã phù hợp nhất dựa trên nguồn dữ liệu hình ảnh của bạn. Các phương thức này cố gắng phân bổ bộ nhớ cho bitmap đã được tạo, việc này có thể dễ dàng dẫn đến trường hợp ngoại lệ OutOfMemory. Mỗi loại phương thức giải mã có các chữ ký bổ sung cho phép bạn chỉ định các tùy chọn giải mã thông qua lớp BitmapFactory.Options. Việc đặt thuộc tính inJustDecodeBounds thành true trong khi giải mã sẽ tránh tình trạng phân bổ bộ nhớ, trả về null cho đối tượng bitmap, nhưng đặt outWidth, outHeightoutMimeType. Kỹ thuật này cho phép bạn đọc các thứ nguyên và loại dữ liệu hình ảnh trước khi tạo (và phân bổ bộ nhớ) của bitmap.

Kotlin

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType

Java

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 java.lang.OutOfMemory trường hợp ngoại lệ, hãy kiểm tra kích thước của một bitmap trước khi giải mã xong, trừ khi bạn hoàn toàn tin tưởng nguồn đó để cung cấp cho bạn dữ liệu hình ảnh có kích thước dự đoán phù hợp trong bộ nhớ hiện có.

Tải phiên bản giảm tỷ lệ vào Bộ nhớ

Giờ đây, khi đã biết kích thước hình ảnh, bạn có thể dùng các kích thước đó để quyết định xem hình ảnh đầy đủ có được tải vào bộ nhớ hay không, hoặc liệu có nên tải phiên bản lấy mẫu con thay thế. Dưới đây là một số yếu tố cần xem xét:

  • Mức sử dụng bộ nhớ ước tính khi tải toàn bộ hình ảnh trong bộ nhớ.
  • Dung lượng bộ nhớ bạn sẵn sàng cam kết tải hình ảnh này theo các yêu cầu khác về bộ nhớ trong ứng dụng của bạn.
  • Kích thước của thành phần giao diện người dùng ImageView hoặc giao diện người dùng mà hình ảnh sẽ được tải vào.
  • Kích thước màn hình và mật độ của thiết bị hiện tại.

Ví dụ như bạn không nên tải hình ảnh có kích thước 1024 x 768 pixel vào bộ nhớ nếu hình ảnh đó hiển thị dưới dạng hình thu nhỏ 128x96 pixel trong ImageView.

Để yêu cầu bộ giải mã lấy mẫu con của hình ảnh, hãy tải một phiên bản nhỏ hơn vào bộ nhớ, đặt inSampleSize thành true trong đối tượng BitmapFactory.Options của bạn. Ví dụ như một hình ảnh có độ phân giải 2048x1536 được giải mã bằng inSampleSize là 4 sẽ tạo ra một bitmap có kích thước khoảng 512x384. Thao tác tải này vào bộ nhớ sử dụng 0,75 MB thay vì 12 MB cho hình ảnh đầy đủ (giả sử cấu hình bitmap là ARGB_8888). Dưới đây là phương thức để tính toán giá trị kích thước mẫu, là lũy thừa của 2 dựa trên chiều rộng và chiều cao mục tiêu:

Kotlin

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

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

        val halfHeight: Int = height / 2
        val halfWidth: Int = 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
}

Java

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;
}

Lưu ý: Lũy thừa của hai giá trị được tính toán vì bộ giải mã sử dụng giá trị cuối cùng bằng cách làm tròn đến lũy thừa gần nhất của hai giá trị, theo tài liệu inSampleSize.

Để sử dụng phương thức này, trước tiên hãy giải mã bằng inJustDecodeBounds được đặt thành true, truyền các tùy chọn, sau đó giải mã lại bằng cách sử dụng giá trị inSampleSize mới và đặt inJustDecodeBounds thành false:

Kotlin

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    // First decode with inJustDecodeBounds=true to check dimensions
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)

        // Calculate inSampleSize
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

        // Decode bitmap with inSampleSize set
        inJustDecodeBounds = false

        BitmapFactory.decodeResource(res, resId, this)
    }
}

Java

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);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

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

Phương thức này giúp bạn dễ dàng tải một bitmap có kích thước lớn tùy ý vào một ImageView hiển thị hình thu nhỏ có kích thước 100x100 pixel, như được minh họa trong mã ví dụ sau:

Kotlin

imageView.setImageBitmap(
        decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)

Java

imageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

Bạn có thể thực hiện theo một quy trình tương tự để giải mã bitmap từ các nguồn khác, bằng cách thay thế phương thức BitmapFactory.decode* thích hợp khi cần.