Caching Bitmaps
Caching Bitmaps
원문: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
소스코드: http://developer.android.com/shareables/training/BitmapFun.zip
여러분의 어플리케이션에 한 장의 비트맵을 표시하기는 쉽습니다. 하지만 한 번에 여러장의 이미지를 로드해야하는 경우에는 문제가 복잡해 집니다. 특히나 ListView, GridView, ViewPager 등의 컴포넌트를 사용하는 경우라면, 본질적으로 처리해야할 이미지의 수에는 제한이 없는 셈 입니다.
위에 열거한 컴포넌트들은 화면 상에서 사라지는 자식 뷰를 재활용하여 메모리 사용량을 적절히 유지합니다. GC(Garbage Collector)는 여러분이 로드한 이미지가 어디서도 참조되지 않는 경우 해당 이미지 리소스를 해제합니다. 좋은 일입니다. 하지만 필요할 때 마다 매번 이미지를 로딩해서는 UI 를 그리는데 시간이 걸리고, 어플리케이션이 부드럽게 동작할 수 없습니다. 이런 경우, 이미 로드된 이미지를 재활용할 수 있도록 메모리와 디스크 캐쉬를 적용해야 합니다.
이 트레이닝 레슨에서는 다 수의 비트맵을 로드할 때 메모리 / 디스크 캐쉬를 사용하여 UI 의 반응성과 부드러움을 향상 시킬 수 있는 방법을 알아보도록 하겠습니다.
메모리 캐쉬 사용하기
메모리 캐쉬는 당연히 어플리케이션 메모리를 차지합니다. 귀중한 메모리를 차지하는 대신 빠르게 비트맵을 불러 올 수 있습니다. 허니콤 이 후 추가된 LruCache (Support Library 에서도 지원합니다.) 클래스는 메모리 캐쉬를 구현하는데 유용합니다. 최근에 사용된 비트맵은 LinkedHashMap 에 의해 강한 참조가 유지되며(GC의 대상이 되지 않으며), 가장 예전에 사용된 비트맵은 캐쉬에 지정된 메모리 사이즈에 맞추어 적절히 캐쉬에서 삭제됩니다. (주> 한 가지, 비트맵이 삭제 된 후, 실제 GC 에 의해 리소스가 해제될 때 까지 시차가 있습니다.)
노트: 과거에는 많은 개발자가 SoftReference 나 WeakReference 를 사용하여 메모리 캐쉬를 구현했습니다. 하지만 안드로이드 2.3(API 9) 부터 GC 가 보다 적극적으로 이 약한 리퍼런스들을 콜렉팅 하며, 따라서 SoftReference 나 WeakReference 를 사용하는 것은 그다지 효율적이지 못합니다. (주> 강한 참조가 사라지자 마자 콜렉팅 됨으로, 사실 캐쉬로서 전혀 역할을 못합니다. ) 더군다나, 안드로이드 3.0 이전에는 비트맵 데이터는 네이티브 메모리 영역에 저장되었고, 언제 메모리가 해제되는지 예측하기 쉽지 않았습니다. 결과적으로 어플리케이션이 힙 메모리 한계를 넘겨 죽어버리는 경우가 발생하기도 했습니다. (주> 명시적으로 recycle 을 호출하지 않고 시스템에서 알아서 free 해주기만을 기다리다가는 이렇게 되기 쉽상입니다. 그렇다고 적절한 recycle 타이밍을 잡는 것도 쉽지 않더군요ㅠㅠ)
LruCache를 적절한 사이즈로 설정하기 위해서는 고려해야할 사항이 많이있습니다. 예를 들자면,
- 캐쉬를 제외하고 여러분의 어플리케이션은 얼마나 많은 메모리가 필요한가요?
- 한 번에 얼마나 많은 이미지가 표시되나요? 그리고 얼마나 많은 이미지가 준비되어 있어야 하나요?
- 디바이스의 스크린 사이즈와 해상도는 어떻게 되는가? 갤럭시 넥서스와 같은 xhdpi 디바이스는 이 보다 해상도가 낮은 넥서스 S 에 비해 동일 한 수의 비트맵을 캐쉬하는데 더 많은 공간이 필요합니다.
- 비트맵의 크기와 해상도는 얼마인가요? 따라서 개별 비트맵을 처리하는데 필요한 메모리 공간은 얼마나 됩니까?
- 이미지가 얼마나 자주 억세스 되나요? 특정 이미지는 다른 이미지에 비해 더 빈번하게 억세스되나요? 그렇다면, 특정 이미지가 계속 캐쉬에 머무르게 하거나, 아니면 따로 구분된 별도의 캐쉬를 유지해야할 필요도 있습니다.
- 질과 양 사이에서 균형을 맞출수 있나요? 때로는 고해상도의 이미지는 별도의 백그라운드 잡에서 따로 로딩하고 품질이 낮은 비트맵을 왕창 캐쉬하고 있는 것이 더 유리할 수가 있습니다.
모든 경우에 적합한 숫자와 공식은 없습니다. 여러분의 어플리케이션을 잘 진단하고 적절한 해결책을 찾는 것은 여러분의 손에 달렸습니다. 너무 작은 캐쉬는 아무런 이득없이 추가적인 비용만 발생 시킬 것 입니다. 너무 큰 캐쉬는 java.lang.OutOfMemory 예외를 발생 시키거나 어플리케이션의 다른 부분이 필요로 하는 메모리를 너무 차지하게 될 수도 있습니다.
다음은 LruCache 클래스를 이용한 Bitmap 캐쉬 예제 코드입니다.
private LruCache<string bitmap=""> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // Get memory class, exceeding this amount will throw an // OutOfMemory exception. final int memClass = ((ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE)).getMemoryClass(); // Use 1/8th of the available memory for this memory cache. final int cacheSize = 1024 * 1024 * memClass / 8; mMemoryCache = new LruCache<string bitmap="">(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in bytes return bitmap.getByteCount(); } }; ... } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }</string></string>
노트: 이 예제에서는 어플리케이션 메모리의 1/8 을 캐쉬로 사용합니다. 갤럭시S2 와 같은 일반적인 폰에서 이 값은 대게 4메가 정도 이며, 비트맵 이미지로 화면이 가득차는 경우에 사용되는 비트맵의 크기는 약 1.5메가 정도가 됩니다. (800x480x4byte) 따라서, 약 2.5 페이지 분량의 이미지가 캐쉬되는 셈 입니다.
비트맵을 ImageView 로 로드할 때, LruCache를 우선 확인합니다. 만일 값이 있으면 해당 비트맵을 이용하여 ImageView를 바로 업데이트 합니다. 그렇지 않은 경우, 백그라운드 스레드를 하나 생성해서 이미지를 처리합니다.
Use a Disk Cache
메모리 캐쉬는 최근에 사용된 비트맵을 빠르게 로딩할 때 유용합니다. 하지만, 큰 데이타 셋을 활용하는 GridView와 같은 컴포넌트들은 금새 메모리 캐쉬를 가득 채울 것 입니다. 또한, 여러분의 어플리케이션은 전화가 걸려온다던가의 이유로 백그라운드 상태로 전환될 수 있으며, 언제든 종료될 수 있습니다. 이런경우, 메모리 캐쉬는 모두 사라지며, 유저가 앱으로 돌아오면 모든 이미지를 다시 처리해야 합니다. 메모리 캐쉬만으로는 결코 충분하지 않습니다.
디스크 캐쉬는 처리된 비트맵을 보다 오래 보존하고, 해당 비트맵이 메모리 캐쉬 상에서 제거 된 경우에도 로딩 시간을 개선하는데 활용될 수 있습니다. 물론 디스크에서 이미지를 불러오는 것은 메모리에서 불러오는 것보다는 느리고 작업 시간을 예측할 수 없기 때문에, 백그라운드 스레드 상에서 이미지 로딩 작업이 이루어져야합니다.
노트:이미지 갤러리와 같이 이미지가 빈번하게 억세스 되는 앱의 경우에는 ContentProvider 를 사용하는 편이 더 좋을 수도 있습니다.
샘플 코드 상에 간단하게 구현된 DiskLruCache 클래스가 포함되어 있습니다. 하지만 보다 튼튼하고 추천할 만한 DiskLruCache 솔루션이 안드로이드 4.0 소스 코드에 포함되어 있습니다. (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) 이전 버전의 안드로이드에서 해당 클래스를 사용하도록 포팅을 할 수도 있을테고, 조금만 찾아보시면 이미 그런 작업을 한 사람들이 있습니다. (주> 저의 우상 JakeWhartom 씨 입니다!)
다음은 첨부된 소스 파일 중 DiskLruCache를 사용하는 부분의 코드 입니다.
private DiskLruCache mDiskCache; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected void onCreate(Bundle savedInstanceState) { ... // Initialize memory cache ... File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); ... } class BitmapWorkerTask extends AsyncTask<integer void="void" bitmap=""> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // Check disk cache in background thread Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // Not found in disk cache // Process as normal final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // Add final bitmap to caches addBitmapToCache(String.valueOf(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } // Also add to disk cache if (!mDiskCache.containsKey(key)) { mDiskCache.put(key, bitmap); } } public Bitmap getBitmapFromDiskCache(String key) { return mDiskCache.get(key); } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. public static File getCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED || !Environment.isExternalStorageRemovable() ? context.getExternalCacheDir().getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }</integer>
메모리 캐쉬는 UI 스레드 상에서 체크될 수 있지만, 디스크 캐쉬를 체크하는 작업은 백그라운드 스레드 상에서 이루어져야합니다. 디스크 작업은 절대 UI 스레드 상에서 이루어져서는 않됩니다. 이미지 프로세싱 작업이 모두 완료되면, 비트맵은 앞으로를 위해 메모리 / 디스크 캐쉬 양쪽에 저장됩니다.
Handle Configuration Changes
화면이 회전되는 등의 이유로 Runtime Configuration 이 변경될 수 있습니다. 이 경우, 안드로이드는 현재 실행 중인 엑티비티를 종료하고 새로운 엑티비티를 생성합니다. (이 주제에 관한 보다 자세한 내용은 Handling Runtime Changes 페이지를 참고하시기 바랍니다.) 엑티비티가 새로 생성 될 떄 마다, 모든 이미지를 다시 처리해서 사용자들이 화면을 돌리면 어플리케이션이 버벅거리도록 만들고 싶지는 않으실 겁니다.
다행히도 여러분은 멋진 메모리 캐쉬를 갖고 있습니다. 이 캐쉬 인스턴스는 UI 가 없는 Fragment 로 감싼 후, getRetainInstance(true) 메서드를 호출하여 새로운 엑티비티로 인스턴스로 전달 될 수 있습니다. 엑티비티가 새롭게 생성된 후, 보존된 Fragment는 엑티비티에 다시 부착되고 여러분은 메모리 캐쉬 오브젝트에 이전처럼 접근할 수 있습니다. 이미지는 재빨리 로딩되어 ImageView 상 에 표시될 것 입니다.
다음은 Fragment 객체를 이용하여 LruCache 오브젝트를 보존하는 예제 코드입니다.
private LruCache<string bitmap=""> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); mMemoryCache = RetainFragment.mRetainedCache; if (mMemoryCache == null) { mMemoryCache = new LruCache<string bitmap="">(cacheSize) { ... // Initialize cache here as usual } mRetainFragment.mRetainedCache = mMemoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache<string bitmap=""> mRetainedCache; public RetainFragment() {} public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); if (fragment == null) { fragment = new RetainFragment(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }</string></string></string>
Fragment 의 setRetainInstance(true) 메서드를 콜하고 디바이스를 돌려보시면, 이미지는 버벅임 없이 거의 즉시 화면 상에 표시될 것입니다. 메모리 캐쉬에 없는 이미지는 디스크 캐쉬에서 로드될 것이고, 만일 이 마저도 여의치 않다면 일반적인 방식으로 처리될 것 입니다.
[출처] [번역] 안드로이드 Caching Bitmaps|작성자 휴우