6월 3일의 ⁠#Android11: 베타 버전 출시 행사에 참여하세요.

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

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

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

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

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

비트맵 크기 및 유형 읽기

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

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
    

자바

    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 픽셀 이미지를 로드하는 것은 그럴만한 가치가 없는 일입니다.

디코더가 이미지를 서브 샘플링하여 더 작은 버전을 메모리에 로딩하도록 지시하려면 inSampleSizetrue로 설정하면 됩니다(BitmapFactory.Options 개체에서). 예를 들어, 해상도가 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
    }
    

자바

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

자바

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

자바

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

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