큰 비트맵을 효율적으로 로드

참고: 이미지 로드의 권장사항을 따르는 라이브러리는 여러 가지가 있습니다. 앱에서 이러한 라이브러리를 사용하여 가장 최적화된 방식으로 이미지를 로드할 수 있습니다. Android는 Glide 라이브러리를 추천하며 이 라이브러리는 최대한 빨리 그리고 부드럽게 이미지를 로드하고 표시합니다. 인기 있는 다른 이미지 로딩 라이브러리로는 Square의 Picasso, Instacart의 Coil, Facebook의 Fresco 등이 있습니다. 이러한 라이브러리는 Android에서 비트맵 및 기타 이미지 유형과 관련된 복잡한 작업의 대부분을 간소화합니다.

이미지의 모양과 크기는 다양합니다. 많은 경우 이미지는 일반적인 애플리케이션 사용자 인터페이스(UI)에 비해 크기가 큽니다. 예를 들어, 시스템 갤러리 애플리케이션은 Android 기기의 카메라를 사용하여 촬영한 사진을 표시하는데 일반적으로 이러한 사진은 기기의 화면 밀도보다 해상도가 훨씬 높습니다.

제한된 메모리로 작업하는 경우 메모리에 저해상도 버전만 로드하는 것이 이상적입니다. 저해상도 버전은 이미지를 표시하는 UI 구성요소의 크기와 일치해야 합니다. 더 높은 해상도의 이미지는 특별한 이점 없이 메모리를 더 많이 차지하며 즉시 추가로 확장해야 하는 부가적인 성능 오버헤드가 발생합니다.

이 과정에서는 작게 서브 샘플링한 버전을 메모리에 로드하여 애플리케이션당 메모리 제한을 초과하지 않고 큰 비트맵을 디코딩하는 방법을 보여줍니다.

비트맵 크기 및 유형 읽기

BitmapFactory 클래스는 다양한 소스에서 Bitmap을 만들 수 있는 여러 가지 디코딩 메서드(decodeByteArray(), decodeFile(), decodeResource() 등)를 제공합니다. 이미지 데이터 소스에 따라 가장 적합한 디코딩 방법을 선택합니다. 이러한 메서드는 생성된 비트맵에 메모리를 할당하려고 하므로 쉽게 OutOfMemory 예외가 발생할 수 있습니다. 각 유형의 디코딩 메서드에는 BitmapFactory.Options 클래스를 통해 디코딩 옵션을 지정할 수 있는 추가 서명이 있습니다. 디코딩 시 inJustDecodeBounds 속성을 true로 설정하면 메모리 할당이 방지됩니다. 그리고 비트맵 객체에 null이 반환되지만 outWidth, outHeight, outMimeType은 설정됩니다. 이 기법을 사용하면 비트맵을 생성(메모리 할당 포함)하기 전에 이미지 데이터의 크기와 유형을 읽을 수 있습니다.

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;

가용 메모리에 적합한 예측 가능 크기의 이미지 데이터를 제공하는 소스를 절대적으로 신뢰하지 못한다면, java.lang.OutOfMemory 예외를 방지하기 위해 비트맵 디코딩 전에 비트맵 크기를 확인합니다.

축소 버전을 메모리로 로드

이미지 크기를 알면 전체 이미지를 메모리에 로드할지 아니면 서브 샘플링된 버전을 대신 로드할지 결정할 수 있습니다. 이때 고려해야 하는 몇 가지 요소는 다음과 같습니다.

  • 전체 이미지를 메모리에 로드할 때 예상되는 메모리 사용량
  • 애플리케이션의 다른 메모리 요구사항을 고려하여 이 이미지를 로드하는 데 사용할 수 있는 메모리 용량
  • 이미지가 로드되는 타겟 ImageView 또는 UI 구성요소의 크기
  • 현재 기기의 화면 크기 및 밀도

예를 들어, 1024x768픽셀 이미지가 결국에는 ImageView에 128x96픽셀의 미리보기 이미지로 표시된다면 1024x768픽셀 이미지를 메모리에 로드하는 것이 의미가 없습니다.

이미지를 서브 샘플링하여 더 작은 버전을 메모리에 로드하도록 디코더에 지시하려면 BitmapFactory.Options 객체에서 inSampleSizetrue로 설정하면 됩니다. 예를 들어, 해상도가 2048x1536이고 inSampleSize가 4로 디코딩된 이미지는 약 512x384의 비트맵을 생성합니다. 이 비트맵을 메모리에 로드하면 전체 이미지에 12MB 대신 0.75MB가 사용됩니다(비트맵 구성은 ARGB_8888이라고 가정함). 다음은 타겟 너비와 높이를 기준으로 2의 거듭제곱인 샘플 크기 값을 계산하는 메서드입니다.

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

참고: inSampleSize 문서에 따라 디코더가 2의 거듭제곱에 가장 가까운 값으로 내림하여 최종 값을 사용하기 때문에 2의 거듭제곱 값이 계산됩니다.

이 메서드를 사용하려면 먼저 inJustDecodeBoundstrue로 설정한 상태에서 디코딩한 다음 옵션을 전달하고 새 inSampleSize 값과 false로 설정한 inJustDecodeBounds를 사용하여 다시 디코딩합니다.

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

이 메서드를 사용하면 다음 코드 예에서와 같이 100x100픽셀의 썸네일을 표시하는 ImageView에 임의의 큰 비트맵을 쉽게 로드할 수 있습니다.

Kotlin

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

Java

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

필요에 따라 적절한 BitmapFactory.decode* 메서드를 대체하여 비슷한 절차를 따라 다른 소스의 비트맵을 디코딩할 수 있습니다.