개요

보통 인터넷이나 리소스 파일의 이미지 크기는 우리가 보여줄 ImageView의 크기보다 큰 경우가 많다. 만약 원본 이미지를 그대로 ImageView에 로딩한다면, 불필요하게 높은 해상도의 이미지로 메모리를 낭비하게 된다. 게다가, Bitmap은 메모리를 많이 차지하므로 Out Of Memory가 발생할 수도 있다.

 

예를 들어, 129x86 pixel의 썸네일 ImageView에 1024x768 pixel 이미지를 로딩한다고 생각해보자. Glide에서 사용하는 RGB_565 포맷으로 129x86 pixel을 Bitmap 변환하면 129862byte = 22KB가 된다. 그러나 1024x768 pixel은 1.5MB가 된다. 이런 썸네일이 리스트로 사용된다면 꽤 문제가 될 것 같다.

 

따라서, 이미지를 메모리 효율적으로 로딩하려면 크기를 줄여서 Bitmap으로 변환하는 과정이 필요하다.

1. 원본 이미지 크기와 타입 읽기

BitmapFactory는 다양한 이미지 리소스로부터 Bitmap을 생성하는 메서드를 제공한다. decodeByteArray(), decodeFile(), decodeResource()등이 있다. 이러한 메서드로 디코딩을 하면 메모리에 Bitmap이 바로 할당된다.

 

그러나 BitmapFactory 클래스는 이미지의 메모리 할당을 피하면서 크기와 타입 정보를 알 수 있는 기능을 제공한다. BitmapFactory.Options를 사용하면 메타 데이터를 읽고, 수정을 할 수 있다.

inJustDecodeBounds 속성을 true로 설정하고 이미지 데이터를 Bitmap으로 변환하면, Options에 Bitmap 메모리 할당 없이 메타 데이터만 생성된다.

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

2. 이미지 크기 줄이기

원본 이미지의 크기를 알았으니, ImageView의 면적을 바탕으로 크기를 줄일 것인지 결정할 수 있다.

크기를 줄일 땐 inSampleSize 속성을 사용하는데, 이것은 압축할 정도를 뜻한다. 예를 들어, inSampleSize = 4라면, 4x4개의 pixel을 한 개의 pixel로 합친다는 의미로 메모리는 1/16만큼 줄어든다.

아래는 inSampleSize를 결정하는 코드와 설명이다. 자세한 내용은 Android Developers에서 확인할 수 있다.

코드)

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
}
  1. reqWidth, reqHeight: 요구(ImageView) 크기
  2. inSampleSize = 1로 설정 (최소 값이 1임)
  3. 일단 원본 width, height를 2로 나눈다 ⇒ halfWidth, halfHeight
  4. halfWidth와 halfHeight를 inSampleSize로 나누고, 요구 크기와 비교한다.
    1. 요구 크기보다 크거나 같다면, inSampleSize를 증가시켜도 된다는 뜻이므로 2배 증가시킨 후 4번 과정을 다시 진행한다.
    2. 나눈 너비와 높이 중 요구 크기보다 작은 값이 하나라도 있다면 inSampleSize의 감소를 중단한다.
💡 inSampleSize를 2의 제곱으로 증가시키는 이유는 decoder가 최종적으로 inSampleSize를 2의 제곱으로 반올림해서 사용하기 때문이다.

3. 이미지 resize 및 로딩 전체 코드

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

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeResource(res, resId, options)

    // Calculate inSampleSize
    inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
    options.inJustDecodeBounds = false

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

참조

+ Recent posts