저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'bitmap'에 해당되는 글 1건

  1. 2013.01.31 안드로이드 이미지 캐시 구현 (17)

특정 URL 이미지를 목록에서 보여주어야 할 때, 동일 URL 이미지를 매번 다운로드 받아 출력하면 비효율적일 뿐만 아니라 사용자에게 보여지는 응답도 느려지게 된다. 사용자에게 응답을 빠르게 보여주기 위해서는 URL 이미지를 메모리나 로컬 파일에 캐싱하도록 구현해야 한다. 이미지를 캐싱함으로써, 동일 URL 이미지를 보여주어야 할 때 다운로드 없이 빠르게 이미지를 사용자에게 보여줄 수 있게 된다.


본 글에서는 이미지를 위한 캐시를 만들어보도록 하겠다. 실제 URL로부터 이미지를 읽어와 캐시에 담고 ImageView에 다운로드 받은 이미지를 보여주는 코드는 '안드로이드에서 URL 이미지를 ImageView에 보여주기' 글을 참고하기 바란다.


이미지 캐시 기능


제공할 기능은 다음과 같다.

  • 이미지 메모리 캐시를 제공한다.
    • 메모리에 지정 개수 만큼의 이미지를 보관한다.
  • 이미지 메모리/파일의 2레벨 캐시를 제공한다. 
    • 캐시에 이미지를 보관하면 메모리와 파일에 동시에 보관된다.
    • 메모리 캐시는 보관할 수 있는 개수에 제한이 있다.
    • 파일 캐시는 보관할 수 있는 전체 크기에 제한이 있다. ('안드로이드에서 파일 캐시 구현하기' 글에서 만든 파일 캐시를 사용해서 구현한다.)
    • 메모리 캐시에 없으면, 파일 캐시로부터 이미지를 읽어온다.

2레벨 캐시를 사용할 경우 자주 사용되는 이미지는 메모리에 담고 일정 크기만큼의 이미지는 파일로도 보관한다. 이를 통해 메모리 용량의 사용을 일정 수준으로 유지하면서 동시에 네트워크 사용을 최소화해서 사용자에게 이미지를 빠르게 보여줄 수 있다.


이미지 캐시  클래스 구성


구현할 이미지 캐시의 클래스 구성은 아래와 같다.



구성요소

설명 

ImageCacheFactory

ImageCache의 생성 및 검색 기능을 제공한다. 

ImageCache

이미지 캐시를 위한 인터페이스를 제공한다. 

MemoryImageCache

메모리 기반의 이미지 캐시를 구현한다. 

FileImageCache 

파일 기반의 이미지 캐시를 구현한다.

ChainedImageCache 

캐시 체인 기능을 제공한다.


ImageCache 인터페이스


public interface ImageCache {


public void addBitmap(String key, Bitmap bitmap);


public void addBitmap(String key, File bitmapFile);


public Bitmap getBitmap(String key);


public void clear();


}


MemoryImageCache 클래스


MemoryImageCache는 내부적으로 LruCache를 사용해서 구현하였다.


package com.toonburi.app.infra.imagecache;


import java.io.File;


import android.graphics.Bitmap;

import android.graphics.BitmapFactory;

import android.support.v4.util.LruCache;


public class MemoryImageCache implements ImageCache {


private LruCache<String, Bitmap> lruCache;


public MemoryImageCache(int maxCount) {

lruCache = new LruCache<String, Bitmap>(maxCount);

}


@Override

public void addBitmap(String key, Bitmap bitmap) {

if (bitmap == null)

return;

lruCache.put(key, bitmap);

}


@Override

public void addBitmap(String key, File bitmapFile) {

if (bitmapFile == null)

return;

if (!bitmapFile.exists())

return;


Bitmap bitmap = BitmapFactory.decodeFile(bitmapFile.getAbsolutePath());

lruCache.put(key, bitmap);

}


@Override

public Bitmap getBitmap(String key) {

return lruCache.get(key);

}


@Override

public void clear() {

lruCache.evictAll();

}


}


FileImageCache 클래스


FileImageCache는 앞서 '안드로이드에서 파일 캐시 구현하기'에서 만든 파일 캐시를 이용해서 구현하였다. 코드는 다음과 같다.


public class FileImageCache implements ImageCache {

private static final String TAG = "FileImageCache";


private FileCache fileCache;


public FileImageCache(String cacheName) {

fileCache = FileCacheFactory.getInstance().get(cacheName);

}


@Override

public void addBitmap(String key, final Bitmap bitmap) {

try {

fileCache.put(key, new ByteProvider() {

@Override

public void writeTo(OutputStream os) {

bitmap.compress(CompressFormat.PNG, 100, os);

}

});

} catch (IOException e) {

Log.e(TAG, "fail to bitmap to fileCache", e);

}

}


@Override

public void addBitmap(String key, File bitmapFile) {

try {

fileCache.put(key, bitmapFile, true);

} catch (IOException e) {

Log.e(TAG, String.format("fail to bitmap file[%s] to fileCache",

bitmapFile.getAbsolutePath()), e);

}

}


@Override

public Bitmap getBitmap(String key) {

FileEntry cachedFile = fileCache.get(key);

if (cachedFile == null) {

return null;

}

return BitmapFactory.decodeFile(cachedFile.getFile().getAbsolutePath());

}


@Override

public void clear() {

fileCache.clear();

}


}


위 코드에서 유의할 점은 FileImageCache 객체를 생성할 때, 파라미터로 전달받은 cacheName을 이용해서 FileCache를 구한다는 점이다. 즉, FileImageCache의 이름과 동일한 이름을 갖는 FileCache가 존재해야 정상적으로 동작한다. 따라서, FileImageCache를 사용하기 전에 다음과 같이 이미지 캐시와 동일한 이름을 갖는 FileCache를 생성해 주어야 한다.


// onCreate 등에서 파일을 이용하는 이미지 캐시 생성 전에 초기화

FileCacheFactory.getInstance().create(cacheName, cacheSize);


ChainedImageCache 클래스


이미지 캐시와 파일 캐시를 1차/2차 캐시로 사용하기 위해 ChainedImageCache 클래스를 만들었다.


public class ChainedImageCache implements ImageCache {


private List<ImageCache> chain;


public ChainedImageCache(List<ImageCache> chain) {

this.chain = chain;

}


@Override

public void addBitmap(String key, Bitmap bitmap) {

for (ImageCache cache : chain) {

cache.addBitmap(key, bitmap);

}

}


@Override

public void addBitmap(String key, File bitmapFile) {

for (ImageCache cache : chain) {

cache.addBitmap(key, bitmapFile);

}

}


@Override

public final Bitmap getBitmap(String key) {

Bitmap bitmap = null;

List<ImageCache> previousCaches = new ArrayList<ImageCache>();

for (ImageCache cache : chain) {

bitmap = cache.getBitmap(key);

if (bitmap != null) {

break;

}

previousCaches.add(cache);

}

if (bitmap == null)

return null;


if (!previousCaches.isEmpty()) {

for (ImageCache cache : previousCaches) {

cache.addBitmap(key, bitmap);

}

}

return bitmap;

}


@Override

public final void clear() {

for (ImageCache cache : chain) {

cache.clear();

}

}


}


ChainedImageCache는 chain에 등록되어 있는 모든 ImageCache를 차례대로 실행한다. getBitmap()은 약간 복잡하다. getBitmap()은 체인을 따라 Bitmap이 존재할 때까지 탐색한다. Bitmap이 발견되면 해당 캐시 이전에 위치한 캐시들(previousCaches에 보관됨)에 Bitmap 정보를 추가해서, 이후 동일 키로 요청이 오면 체인의 앞에서 발견되도록 한다.


ImageCacheFactory 클래스


ImageCacheFactory는 캐시 생성 기능을 제공한다.


public class ImageCacheFactory {


private static ImageCacheFactory instance = new ImageCacheFactory();


public static ImageCacheFactory getInstance() {

return instance;

}


private HashMap<String, ImageCache> cacheMap = new HashMap<String, ImageCache>();


private ImageCacheFactory() {

}


public ImageCache createMemoryCache(String cacheName, int imageMaxCounts) {

synchronized (cacheMap) {

checkAleadyExists(cacheName);

ImageCache cache = new MemoryImageCache(imageMaxCounts);

cacheMap.put(cacheName, cache);

return cache;

}

}


private void checkAleadyExists(String cacheName) {

ImageCache cache = cacheMap.get(cacheName);

if (cache != null) {

throw new ImageCacheAleadyExistException(String.format(

"ImageCache[%s] aleady exists", cacheName));

}

}


public ImageCache createTwoLevelCache(String cacheName, int imageMaxCounts) {

synchronized (cacheMap) {

checkAleadyExists(cacheName);

List<ImageCache> chain = new ArrayList<ImageCache>();

chain.add(new MemoryImageCache(imageMaxCounts));

chain.add(new FileImageCache(cacheName));

ChainedImageCache cache = new ChainedImageCache(chain);

cacheMap.put(cacheName, cache);

return cache;

}

}


public ImageCache get(String cacheName) {

ImageCache cache = cacheMap.get(cacheName);

if (cache == null) {

throw new ImageCacheNotFoundException(

String.format("ImageCache[%s] not founds"));

}

return cache;

}

}


createMemoryCache() 메서드는 메모리만 사용하는 ImageCache를 생성한다. createTwoLevelCache() 메서드는 1차 메모리/2차 파일 기반의 2레벨 캐시를 생성한다.


이미지 캐시 사용하기


다음은 이미지 캐시의 사용 예시이다.


-- onCreate 등 초기화 부분


// 2레벨 캐시(이미지 파일 캐시)를 사용하려면 동일 이름의 파일 캐시를 생성해 주어야 한다.

FileCacheFactory.getInstance().create(cacheName, cacheSize);


// 이미지 캐시 초기화

ImageCacheFactory.getInstance().createTwoLevelCache(cacheName, memoryImageMaxCounts);



-- 이미지 캐시 사용 부분

ImageCache imageCache = ImageCacheFactory.getInstance().getCache(cacheName);

Bitmap bitmap = imageCache.getBitmap(key);

if (bitmap != null) {

imageView.set.....

}


-- 이미지 캐시 추가 부분

imageCache.putBitmap(key, someBitmap);


실제 ImageCache를 사용하는 예제 코드는 '안드로이드에서 URL 이미지를 ImageView에 보여주기'에 있으니 이 글을 참고하면 된다.


관련자료



Posted by 최범균 madvirus

댓글을 달아 주세요

  1. bluepoet 2013.01.31 17:35 신고  댓글주소  수정/삭제  댓글쓰기

    저도 이번 스타앱 리뉴얼때, 프로게이머 프로필 이미지쪽을 구현할 예정이었는데

    이번 포스팅이 많이 참고가 되겠네요.

    특히나, 체인을 이용한 캐시 구현방법은 참 신선하네요.

    근데, previousCaches에 정보를 넣고 활용하는 쪽은 좀 어렵네요.

    동일 키로 요청이 오면 어떻게 체인의 앞에서 발견되도록 하는 건지 궁금합니다.

  2. andu 2013.05.16 13:28 신고  댓글주소  수정/삭제  댓글쓰기

    FileImageCache 클래스에서 fileCache.clear()가 사용되는데,

    FileCache 클래스에는 clear()가 없네요. 새로 추가된 메소드인가요?

  3. truelifer 2013.10.10 17:13 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 포스팅 감사합니다.
    저도 한가지 의문이 있는건, bluepoet 님이 문의하신 것 처럼 동일 키로 요청이 오면 어떻게 체인의 앞에서 발견되도록 하는건지 궁금합니다.
    코드를 보니 previousCaches 를 따로 저장하지 않고 getBitmap() 함수가 끝나면 previousCaches 가 소멸될 것 같은데..

    • 최범균 madvirus 2013.10.11 09:09 신고  댓글주소  수정/삭제

      previousCaches 라는 로컬 변수는 사라지지만, previousCache에 담은 객체는 chain 필드에 보관되어 있는 ImageCache 입니다.
      getBitmap() 메서드를 보시면, chain에 있는 ImageCache를 차례대로 탐색하면서 해당 ImageCache가 key에 해당하는 비트맵을 갖고 있는 지 확인을 합니다. 갖고 있으면, for 루프를 나오고, 아니면 (로컬 변수인) previousCaches에 ImageCache를 추가합니다.

      발견된 Bitmap이 없으면 그냥 null을 리턴하고, 발결된 Bitmap이 있으면, previousCaches에 담아 두었던 ImageCache들의 addBitmap을 호출해서 비트맵을 추가해줍니다. 여기서 previousCaches에는 chain에서 비트맵이 발견되기 전까지의 ImageCache 목록을 갖고 있게 되죠. 따라서, chain의 특정 ImageCache에서 비트맵이 발견되면, 그 ImageCache 이전에 있던 ImageCache들에 비트맵을 추가하게 됩니다.

  4. winterCha 2013.10.17 16:38 신고  댓글주소  수정/삭제  댓글쓰기

    정말 감사합니다 아주 투통이 사라지는 캐쉬 로직이네요
    태클아닌 태클 하나.....

    checkAlreadyExists 를 표현 하고 싶으신것 같은데 r이 빠졌네요
    쏘리욤

    madvirus fan 입니다 최범균님 책으로 참 많은 것을 공부 했었던 기억이

    존경합니다.

  5. cs만두 2014.05.19 22:22 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 안드로이드에 관심이 많은 대학생 개발자입니다!
    몇달전 프로젝트를 진행하면서 bitmap OOM관련 이슈때문에 고민을 하다가 AUIL 라이브러리를 사용해서 해결했습니다. 그러면서 꼭 시간나면 이미지 로더 관련해서 공부해봐야겠다 생각했는데 이렇게 좋은 글을 만나서 너무 좋습니다.
    좋은 글 감사합니다! 열심히 공부하겠습니다!!!!!!

  6. 이승화 2014.05.23 16:59 신고  댓글주소  수정/삭제  댓글쓰기

    감사합니다. 덕분에 좀더 쉽게 코딩할수 있게 되었습니다.
    책도 많이 쓰셨던데 짱~ 이십니다ㅋ

  7. 최형식 2015.03.25 14:07 신고  댓글주소  수정/삭제  댓글쓰기

    지도 앱을 만들고 있습니다. svg파일로 하든 png로 하든 비트맵으로 변경해서 보여주려고 합니다. 사이즈가 크다 보니 타일처럼 짤라서 화면밖의 타일들은 캐시로, 이미지가 움직이면 캐시에서 불러오고 이런식으로 하고싶은데 어떤 방법이 있나요?

  8. 최수혁 2015.05.22 11:03 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요?
    이 글 보고 안드로이드 이미지 캐시를 완벽하게 구현하였습니다.
    그런데 문제가 하나 있습니다.

    A라는 이름으로 캐시를 하나 창조해서 다운로드 해서 이미지를 모두 넣었습니다.
    그 다음 액티비티에서 B라는 이름으로 캐시를 하나 또 창조했는데 다운로드되지 않고 있습니다.
    캐시창조할때에는 모두 오류가 없는데 이상하게 onResult에는 안들어오네요..

    B와 A의 순서를 바꾸어도 역시 2번째는 작동을 안합니다.

    왜그럴가요?

    도저히 원인을 못찾겠네요.

    • 최범균 madvirus 2015.05.23 00:47 신고  댓글주소  수정/삭제

      코드를 바꾸지 않으셨다면, 코드에 버그가 있는 거겠죠. 지금은 졸려서 코드가 눈에 잘 안 들어오네요. 좀 맨 정신에 코드 보고 답글 달아보렵니다.