Memory vs Disk
- Memory 는 컴퓨터 내부에서 현재 CPU 가 처리하고 있는 내용을 저장하고 있는 휘발성 장치로, 처리 속도가 빠르다.
- Disk 는 Memory 보다는 느리지만, 많은 양의 데이터를 전원이 꺼져도 사라지지 않고 영구적으로 보관할 수 있는 장치이다.
[CS] CPU, 주기억장치(ROM, RAM(SRAM, DRAM), 보조기억장치(HDD, SDD)
중앙 처리 장치 (Central Processing Unit, CPU)컴퓨터의 중앙에서 모든 데이터의 처리를 담당하는 장치, 컴퓨터의 두뇌컴퓨터의 속도는 CPU 의 성능이 가장 큰 영향을 미친다. 컴퓨터는 사용자의 명령을
dev-baik.tistory.com
Memory Cache
- 애플리케이션 내에 존재하는 메모리에 비트맵을 캐싱하고, 필요할 때 빠르게 접근가능하다.
- 하지만 Memory Cache 도 곧 애플리케이션 용량을 키우는 주범이 될 수 있기 때문에 많은 캐싱을 요구하는 비트맵의 경우에는 Disk Cache 에 넣는 것이 더 좋을 수 있다.
1. BitmapPool
- 사용하지 않는 Bitmap 을 리스트에 넣어 놓고, 추후에 동일한 이미지를 로드할 때 다시 메모리에 적재하지 않고 pool 에 잇는 이미지를 가져와 재사용하는 것이다.
- 보통 BitmapPool 을 이용해 재사용 Pool 을 만들게 될 때, LRU 캐싱 알고리즘으로 구현된 LinkedList(lruBitmapList)와 Byte Size 순으로 정렬된 LinkedList(bitmapList)를 사용하여 구현하게 된다.
- 이 둘은 들어있는 비트맵의 순서만 다를 뿐, 같은 비트맵이 담기게 된다.
private val lruBitmapList = LinkedList<Bitmap>()
private val bitmapList = LinkedList<Bitmap>()
- LRU 알고리즘을 이용해 오랫동안 참조되지 않은 비트맵 객체는 맨 뒤로 밀리게 되고, 맨 뒤에 있는 객체를 회수하면서 BitmapPool 을 유지시키는 것이다.
- LRU 알고리즘을 이용하지 않는다면 처음 BitmapPool 이 가득 찰 때까지는 문제없이 동작하지만, 비트맵을 재사용하는 시점부터는 특정 비트맵만 재사용될 수 있으며, 앱이 끝날 때까지 메모리가 줄어들지 않게 된다.
- Glide 에서 구현한 LruBitmapPool Class 내부
public class LruBitmapPool implements BitmapPool {
private static final String TAG = "LruBitmapPool";
private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888;
private final LruPoolStrategy strategy;
private final Set<Bitmap.Config> allowedConfigs;
private final long initialMaxSize;
private final BitmapTracker tracker;
// Pool에 들어올수 있는 최대 크기
private long maxSize;
// 현재 pool에 들어간 bitmap byte 크기
private long currentSize;
- Glide 는 디코딩 단계에서 *RGB_565를 사용하여 메모리 효율성을 높이고, LruBitmapPool 에서는 *ARGB_8888을 기본값으로 사용해 재사용성과 호환성을 보장한다.
- RGB 565 : 2 byte(16 bit)로 1 px 를 표현하는 방식
- 16 bit 를 R(5 bit) G(6 BIt) B (5 bit)로 표현하게 된다. R 과 B 의 경우 2^5인 32가지의 색상을 나타낼 수 있어 색상 표현 수준은 낮으나, 그만큼 이미지의 용량을 아낄 수 있다.
- ARGB 8888 : 4 btye(32 bit)로 1 px 를 표현하는 방식
- 32 bit 를 각 8 bit 로 동등하게 표현이 가능하며, 투명도를 나타내는 알파 값이 추가되어 보다 다채로운 색상의 표현이 가능하다.
- RGB 565 : 2 byte(16 bit)로 1 px 를 표현하는 방식
- Glide 내 LruBitmapPool Class 에서는 strategy(LruPoolStrategy)가 곧 LRU 기반으로 구현된 리스트이며, tracker(BitmapTracker)가 Bitmap size 순으로 정렬된 리스트이다.
@Override
public synchronized void put(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("Bitmap must not be null");
}
// isRecycled : *재활용된 bitmap 인지 여부
// 재활용 : 해당 비트맵이 더 이상 필요하지 않거나 사용되지 않도록 메모리에서 해제되었다는 것
if (bitmap.isRecycled()) {
throw new IllegalStateException("Cannot pool recycled bitmap");
}
// isMutable : canvas를 얻을 수 있는 bitmap 인지 여부
if (!bitmap.isMutable()
|| strategy.getSize(bitmap) > maxSize
|| !allowedConfigs.contains(bitmap.getConfig())) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(
TAG,
"Reject bitmap from pool"
+ ", bitmap: "
+ strategy.logBitmap(bitmap)
+ ", is mutable: "
+ bitmap.isMutable()
+ ", is allowed config: "
+ allowedConfigs.contains(bitmap.getConfig()));
}
bitmap.recycle();
return;
}
// pool에 넣으려는 bitmap의 사이즈를 얻어온다.
final int size = strategy.getSize(bitmap);
strategy.put(bitmap);
tracker.add(bitmap);
puts++;
currentSize += size;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
}
dump(); // 현재 비트맵 풀의 상태를 출력하는 메서드
evict();
}
private void evict() {
trimToSize(maxSize);
}
- Bitmap 을 Pool 에 넣을 수 있는 조건을 충족시킨다면, strategy & tracker 에 bitmap 이 들어가게 된다.
- Lru 기반인 strategy 는 최근에 들어온 bitmap 일수록 리스트의 맨 앞으로 배치시켜야 하는데, 그 로직이 LruPoolStrategy 구현체 내부에 존재하게 된다.
private synchronized void trimToSize(long size) {
while (currentSize > size) {
// LRU List에서 삭제한다.
final Bitmap removed = strategy.removeLast();
if (removed == null) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Size mismatch, resetting");
dumpUnchecked();
}
currentSize = 0;
return;
}
// BitmapList에서 삭제한다.
tracker.remove(removed);
currentSize -= strategy.getSize(removed);
evictions++;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
}
dump();
removed.recycle();
}
}
- trimToSize()를 이용하면 pool 이 최대 사용량을 넘어선 경우, 참조를 가장 적게하고 있는 lastIndex 부터 객체를 지워가며 크기를 줄여줄 수 있다.
@Override
@Nullable
public Bitmap get(int width, int height, Bitmap.Config config) {
int size = Util.getBitmapByteSize(width, height, config);
Key bestKey = findBestKey(size, config);
// 비트맵을 찾은 경우, pool에서 제거한다.
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
decrementBitmapOfSize(bestKey.size, result);
result.reconfigure(width, height, config);
}
return result;
}
- 이미지를 로드하기 위해 pool 에서 주어진 크기와 설정에 맞는 비트맵을 가져온다.
- 비트맵 재사용 BitmapPool 만들기
import android.graphics.Bitmap
import android.os.Build
import java.util.LinkedList
// maxByteSize : 캐시 풀에 저장할 수 있는 최대 바이트 크기
class LruBitmapPool(private val maxByteSize: Int) {
private val TAG = "LruBitmapPool"
// 현재 Pool 내에 있는 Bitmap들의 총 메모리 사용량
private var useByteSize = 0
// 가장 최근에 사용된 비트맵을 맨 앞에 두고, 오래된 비트맵을 맨 뒤에 두는 LRU 리스트
private val lruList = LinkedList<Bitmap>()
// 비트맵의 크기별로 정렬된 리스트 (오름차순)
private val sizeList = LinkedList<Bitmap>()
init {
require(maxByteSize > 0) { "Invalid maxByteSize" }
}
// Bitmap을 Pool에 추가하는 메서드
@Synchronized
fun add(bmp: Bitmap) {
// 비트맵이 null인지, 이미 해제된(recycled) 상태인지, 또는 변경 불가능한 상태인지 확인
requireNotNull(bmp) { "Bitmap is null, null can't be added to pool" }
require(!bmp.isRecycled) { "Bitmap is already recycled" }
require(bmp.isMutable) { "Bitmap is not mutable" }
// sizeList에 Bitmap을 크기별로 적절한 위치에 삽입
var idx = 0
for (b in sizeList) {
if (getByteSize(b) >= getByteSize(bmp)) {
break
}
idx++
}
sizeList.add(idx, bmp)
// lruList에는 최근 Bitmap을 맨 앞에 추가
lruList.addFirst(bmp)
useByteSize += getByteSize(bmp)
// 메모리 사용량이 최대 용량(maxByteSize)을 초과할 경우
// trimToSize 메서드를 호출하여 오래된 비트맵을 제거
if (useByteSize > maxByteSize) {
trimToSize(maxByteSize)
}
}
// 지정된 크기(w, h)와 구성(config)의 비트맵을 반환하는 메서드
@Synchronized
fun getBitmap(w: Int, h: Int, config: Bitmap.Config): Bitmap {
var bmp: Bitmap? = null
val it = sizeList.iterator()
while (it.hasNext()) {
val b = it.next()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// allocationByteCount를 이용해 메모리 할당 크기를 확인
if (b.allocationByteCount >= w * h * getBytePerPixel(config)) {
b.reconfigure(w, h, config)
bmp = b
break
}
} else {
if (getByteSize(b) > w * h * getBytePerPixel(config)) {
break
}
// KitKat 미만에서는 폭, 높이, 픽셀 구성이 일치하는 경우에만 재사용이 가능
if (b.width == w && b.height == h && b.config == config) {
bmp = b
break
}
}
}
if (bmp != null) {
// 적절한 Bitmap을 찾은 경우, Pool에서 제거하고 사용량을 업데이트
sizeList.remove(bmp)
lruList.remove(bmp)
useByteSize -= getByteSize(bmp)
} else {
// 찾지 못한 경우 새 Bitmap을 생성해 반환
bmp = Bitmap.createBitmap(w, h, config)
}
return bmp
}
// 현재 Pool의 크기가 지정된 크기를 초과할 경우 오래된 비트맵을 제거하는 메서드
@Synchronized
fun trimToSize(trimToSize: Int) {
while (lruList.isNotEmpty() && useByteSize > trimToSize) {
val bmp = lruList.removeLast()
sizeList.remove(bmp)
useByteSize -= getByteSize(bmp)
// 제거된 Bitmap은 recycle을 호출해 리소스를 해제
bmp.recycle()
}
}
// 모든 비트맵을 Pool에서 제거하는 메서드
fun evictAll() {
trimToSize(-1)
}
companion object {
// 비트맵의 구성에 따라 픽셀당 바이트 수를 반환하는 정적 메서드
fun getBytePerPixel(config: Bitmap.Config): Int {
return when (config) {
Bitmap.Config.RGBA_F16 -> 8
Bitmap.Config.ARGB_8888 -> 4
Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
Bitmap.Config.ALPHA_8 -> 1
else -> -1
}
}
// 주어진 비트맵의 메모리 크기를 반환하는 정적 메서드
fun getByteSize(bmp: Bitmap): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
bmp.allocationByteCount
} else {
bmp.byteCount
}
}
}
}
- 사용 예제
class MyActivity : AppCompatActivity() {
private lateinit var bitmapPool: LruBitmapPool
private lateinit var imageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
bitmapPool = LruBitmapPool(10 * 1024 * 1024)
imageView = findViewById(R.id.myImageView)
loadBitmap()
}
private fun loadBitmap() {
val width = 100
val height = 100
val config = Bitmap.Config.ARGB_8888
val opts = BitmapFactory.Options().apply {
inMutable = true // 비트맵을 변경 가능하도록 설정
inBitmap = bitmapPool.getBitmap(width, height, config) // 비트맵 풀에서 재사용할 비트맵 할당
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.example_image, opts)
val oldBitmap: Bitmap? = imageView.getTag(R.string.bitmap_tag_key) as? Bitmap
imageView.setImageBitmap(bitmap)
imageView.setTag(key, bitmap)
if (oldBitmap != null) {
bitmapPool.add(oldBitmap)
}
}
private fun releaseBitmap() {
val drawable = imageView.drawable as? BitmapDrawable
val bitmap: Bitmap? = drawable?.bitmap
if (bitmap != null) {
val taggedBitmap: Bitmap? = imageView.getTag(R.string.bitmap_tag_key) as? Bitmap
if (bitmap === taggedBitmap) {
imageView.setImageBitmap(null)
bitmapPool.add(bitmap)
}
}
}
override fun onDestroy() {
super.onDestroy()
bitmapPool.evictAll()
}
}
2. LruCache 클래스
- LruCache 는 안드로이드에서 캐시를 관리하기 위해 사용하는 메모리 캐시 객체이다.
- LruCache 는 LRU(Least Recent Used) 알고리즘을 사용하는데 간단히 말해서 최근에 조회된 것을 캐시에서 삭제하는 것을 늦추기 위한 객체이다.
- 즉, 오랫동안 접근되지 않은 메모리가 우선적으로 삭제된다.
- LruCache 는 제네릭으로 선언되어 있는데 K 가 캐시에 접근하기 위한 키값이고 V 는 캐시에서 가져올 객체의 타입이다. 또한 LruCache 객체는 생성자의 인자로 maxSize 를 int 값으로 받는다.
public class LruCache<K, V> {
public LruCache(int maxSize) {
...
}
...
}
- 사용 예제
class MainActivity : AppCompatActivity() {
private lateinit var memoryCache: LruCache<String, Bitmap>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8
memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
}
val imageView = findViewById<ImageView>(R.id.imageView)
loadBitmap(R.drawable.example_image, imageView)
}
private fun loadBitmap(resId: Int, imageView: ImageView) {
val imageKey: String = resId.toString()
val bitmap: Bitmap? = memoryCache.get(imageKey)?.also {
imageView.setImageBitmap(it)
} ?: run {
imageView.setImageResource(R.drawable.image_placeholder)
val task = BitmapWorkerTask(imageView)
task.execute(resId)
null
}
}
// TODO AsyncTask(Deprecated) -> Coroutine
private inner class BitmapWorkerTask(private val imageView: ImageView) : AsyncTask<Int, Unit, Bitmap>() {
override fun doInBackground(vararg params: Int?): Bitmap? {
if (params.isNotEmpty() && params[0] != null) {
val imageId = params[0]!!
return decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap ->
memoryCache.put(imageId.toString(), bitmap)
}
}
return null
}
override fun onPostExecute(result: Bitmap?) {
if (result != null) {
imageView.setImageBitmap(result)
}
}
}
private fun decodeSampledBitmapFromResource(res: Resources, resId: Int, reqWidth: Int, reqHeight: Int): Bitmap? {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resId, this)
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
inJustDecodeBounds = false
}
return BitmapFactory.decodeResource(res, resId, options)
}
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
}
Disk Cache
- Memory Cache 에 넣기엔 많은 캐시를 요구하는 경우, 혹은 앱이 백그라운드로 전환되어도 적재한 캐시가 삭제되지 않기를 바란다면 Disk Cache 를 이용하는 것이 좋다.
- 하지만 Disk 로부터 캐싱된 비트맵을 가져올 때는 Memory 에서 로드하는 것보다 오랜 시간이 걸린다.
DiskLruCache
비트맵 캐싱 | App quality | Android Developers
단일 비트맵을 사용자 인터페이스(UI)에 로드하는 것은 간단하지만 한 번에 더 큰 이미지의 집합을 로드해야 하면 더 복잡해집니다. 많은 경우(ListView, GridView 또는 LruCache 클래스와 같은 구성요소
developer.android.com
[Android] Hello👋, Out Of Memory
많은 이미지를 사용하거나 고해상도 이미지를 이미지뷰에 로드해야하는 경우 발생하게 되는데, 이는 안드로이드 앱에서 사용할 수 있는 힙 메모리는 정해져있는데 반해 그 크기를 넘겨버렸기
velog.io
안드로이드 비트맵 재사용 BitmapPool만들기
이전편에서 언급한 Bitmap를 재사용하려면 사용하지 않는 비트맵을 담아 둘 Pool이 필요하게 된다. 이번편에서는 이 풀(pool)을 만들어 보기로 하겠다. 원리는 간단하다. 사용하지 않는 Bitmap을 리스
jamssoft.tistory.com
비트맵 캐싱 | App quality | Android Developers
단일 비트맵을 사용자 인터페이스(UI)에 로드하는 것은 간단하지만 한 번에 더 큰 이미지의 집합을 로드해야 하면 더 복잡해집니다. 많은 경우(ListView, GridView 또는 LruCache 클래스와 같은 구성요소
developer.android.com
[Android] LruCache 사용해 이미지 캐싱하기(Bitmap Cache)
LruCache 란? LruCache객체는 안드로이드에서 캐시를 관리하기 위해 사용하는 메모리 캐시 객체이다. LruCache 객체는 LRU(Least Recent Used) 알고리즘을 사용하는데 간단히 말해서 최근에 조회된 것을 캐시
kotlinworld.com
'안드로이드 > Android' 카테고리의 다른 글
| [Android] Image Loader Library (0) | 2024.11.11 |
|---|---|
| [Android] SharedPreference vs Datastore (0) | 2024.11.05 |
| [Android] 직렬화(Serialization, Parcelable), 역직렬화(Deserialization) (0) | 2024.10.27 |