비트맵 메모리 관리하기

비트맵 캐싱하기에 더해서, 가비지 콜렉터 사용과 비트멥 재사용을 위해서 해야할 구체적 사항들이 있습니다. 타겟으로 삼는 안드로이드 플랫폼 버전에 따라서 추천될만한 사용법 (전략)이 바뀝니다. BitmapFun이라는 예제 애플리케이션은 다른 안드로이드 버전에 걸처 어떻게 효율적으로 비트맵 메모리를 관리하는 애플리케이션을 디자인 할지 다룹니다.

이 강좌에 앞서, 안드로이드가 어떻게 비트맵 메모리 관리에 있어 진화해 왔는지 살펴봅시다.

  • 안드로이드 2.2.(API 8)과 그 이하 버전에서, 가비지 콜렉터가 작동할 때 애플리케이션의 쓰레드들이 멈췄습니다. 이것것은 성능을 낮추는 지연현상을 야기합니다. 안드로이드 2.3에서는 동시성 가비지 콜렉터가 추가되었습니다. 이것은 비트맵이 더이상 참조되지 않을 때 메모리가 바로 반환되는 것을 의미합니다.

  • 안드로이드 2.3.3(API 10)과 그 이하에서, 역행하는 픽셀 데이터(backing pixel)가 순수 메모리에 직접 저장되었습니다. 이것은 비트맵과 별개로 Dalvix heap에 저장되는 것입니다. 순수 메모리에 저장된 픽셀 데이터는 예견된 방식으로 해제되지 않으며 메모리 부족현상으로 애플리케이션이 충돌하는 현상이 쉽게 발생되도록 합니다.안드로이드 3.0(API 11)부터는, 연관된 비트맵과 함께 픽셀 데이터가 Dalvix heap에 저장됩니다.

이번 파트는 어떻게 다른 안드로이드 버전에서 비트맵 메모리를 최적화하는지를 다룹니다.

안드로이드 2.3.3(API 10)과 그 이하에서 메모리 관리하기

현재 대다수의 애플리케이션은 최소 버전으로 API15 수준을 차용함으로 이부분에 대한 번역은 추후로 미루겠습니다.

안드로이드 3.0(API11)과 그 이상에서 메모리 관리하기

안드로이드 3.0 (API 11)은 BitmapFactory.Options.inBitmap 필드를 가지고 있습니다. 이 옵션이 설정된다면, 해당 Options객체를 갖는 디코딩 메소드는 콘텐츠를 불러올 떄 기존에 존재하던 비트맵을 재사용할것 입니다. 이것은 비트맵 메모리가 재사용됨을 의미하며, 불필요한 메모리 할당과 반환을 없애 성능 향상을 가져옵니다.그러나 inBitmap사용에는 몇가지 제한 사항이 있습니다. 특히 안드로이드 4.4 (API 19)이전의 경우, 오로지 같은 크기의 비트맵만이 지원됩니다. 더 자세한 사항은 inBitmap 문서를 확인하십시오.

inBitmap은 메모리에 있는 비트맵 재사용을 위한 필드로 만약 재사용할 비트맵이 적절하지 않을 경우 디코더는 null값을 반환하며 IlligalArgumentException이 발생할 수 있습니다. 사용시 이해대한 대비가 필요합니다. 이 부분에 주의하십시오.

또한 사요되는 비트맵은 변환가능한 (mutable, 픽셀값 변환이 가능한) 비트맵이며 계속해서 변환가능한 비트맵으로 유지됩니다.

안드로이드4.4.(API 19, KIKAT)부터 디코드될 비트맵과 크기가 재사용될 비트맵의 크기와 같거나 작을경우 BitmapFactory에서 inBitmap을 사용할 수 있습니다. 그 전 버전의 경우 오로지 같은 사이즈의 PNG, JPEG파일만 사용가능합니다.

안드로이드4.1(API16, JELLYBEAN)부터는 BitmapRegionDecoder가 사용가능합니다. BitmapRegionDecode는 출력될 비트맵에 기존의 비트맵을 그려내는 객체로서 만약 출력될 비트맵의 크기가 원본보다 작을 경우 원본을 축소하여 제공합니다.

이후 사용을 위해 비트맵 저장하기

아래의 소스코드는 샘플 애플리케이션에서 어떻게 추후에 사용될 비트맵을 저장하는 지를 보여줍니다. 안드로이드3.0 또는 그 이상 버전에서 비트멥은 LruCache에서 추출되며, HashSet에 추후 inBitMap에서 사용되기 위해 약한 참조(soft reference)됩니다.

Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// 안드로이드 3.0(API 11 HoneyComb)이상일 경우 동기화된 참조 hashset을 재사용될 비트맵을 위해 만들어 놓습니다. 
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // 더 이상 메모리 캐시에 남겨져 있지 않음을 알립니다. 
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
            // 기존 버전의 비트맵을 지우며, 지워졌음을 표시합니다.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // 안드로이드 3.0(API 11)이상의 버전의 경우 약한 참조로 참조를 남겨둡니다. 
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

이미 존재하는 비트맵 사용하기

작동하고 있는 앱에서, 이미 비트맵이 존재하고 있는지를 살피고 있다면 사용하는 예제입니다.

public static Bitmap decodeSampledBitmapFromFile(String filename,
        int reqWidth, int reqHeight, ImageCache cache) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    ...
    BitmapFactory.decodeFile(filename, options);
    ...

    // 안드로이드 3.0(API 11 HoneyComb)이상의 경우 inBitmap을 사용합니다.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

addInmapOptions() 소스코드는 아래와 같습니다. 이 메소드는 이미 존재하는 비트맵을 찾아 inBitmap에 넣는 작업을 합니다. 적절한 비트맵을 찾았을 경우에만 적당한 값을 inBitmap에 넣습니다.

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    //inBitmap은 오로지 변환 가능한 Bitmap만을 사용합니다. 그러므로 디코더가 변환가능한 비트맵만 반환하도록 합니다
    options.inMutable = true;

    if (cache != null) {
        // 사용가능한 비트맵을 찾습니다.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // 적절한 비트맵을 찾았을 경우 inBitmap에 설정합니다.
            options.inBitmap = inBitmap;
        }
    }
}

// 아래 메소드는 재사용가능한 비트맵을 순환하며 찾습니다.(iterator 패턴)
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;
//mReusableBitmaps는 약한 참조로 이루어진 해시세트입니다
    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        synchronized (mReusableBitmaps) {
            final Iterator<SoftReference<Bitmap>> iterator
                    = mReusableBitmaps.iterator();
            Bitmap item;

            while (iterator.hasNext()) {
                item = iterator.next().get();
                //비트맵의 존재여부와, 변환 가능성을 확인합니다.
                if (null != item && item.isMutable()) {
                    // 비트맵이 사용가능한지 확인합니다.
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;

                        // 순환자(iterator)에서 제거하여 다시 사용되는 것을 막습니다.
                        iterator.remove();
                        break;
                    }
                } else {
                    // 적합하지 않을 경우 세트에서 제거하여 참조를 정리합니다.
                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

마지막으로 아래 메소드는 재사용될 비트맵이 inBItmap에 사용가능한 크기인지 결정합니다.

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // 안드로이드 4.4(API19, KITKAT)부터는 재사용될 비트맵이 새롭게 생길 비트맵보다 크다면 재사용할 수 있습니다.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // 그 전 버전의 경우에는 재사용 이미지와 새롭게 생길 비트맵이 정확히 같은 크기여야 합니다. 즉 inSampleSize가 1이어야 합니다.
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == 1;
}

/**
 * 아래 헬퍼는 각각의 설정에 따라 Pixel당 필요로하는 Byte수를 반환합니다.
 */
static int getBytesPerPixel(Config config) {
    if (config == Config.ARGB_8888) {
        return 4;
    } else if (config == Config.RGB_565) {
        return 2;
    } else if (config == Config.ARGB_4444) {
        return 2;
    } else if (config == Config.ALPHA_8) {
        return 1;
    }
    return 1;
}

results matching ""

    No results matching ""