본문 바로가기

안드로이드/이미지

이미지 캐싱

이미지를 로딩할 때마다 서버에서 불러온다면 네트워크 요청이 자주 발생하게 되고, 이로 인하여 앱의 성능이 저하될 수 있다.

이를 개선하기 위해 Cache를 사용하는데, 한 번 불러온 이미지를 Cache에 저장하여 여러번 사용되는 이미지에 대해 네트워크 요청을 줄이고, 빠르게 불러와 데이터 로딩 속도를 줄여 앱의 성능을 향상 시킬 수 있다.

 

이미지를 사용할 때 주로 사용하는 라이브러리인 Glide와 Coin 등에서는 이러한 기능들을 제공해주기 때문에 따로 설정할 필요는 없지만 어떻게 캐싱을 하는지는 알고 있어야 한다.

 

캐싱의 종류

1. 메모리 캐싱

메모리 캐싱은 말 그대로 메모리에 캐싱을 저장하여 보다 빠르게 접근할 수 있지만 휘발성이며, 

메모리라는 제한으로 인해 많은 데이터를 저장할 수 없다는 특징을 갖고 있다.

 

2. 디스크 캐싱

디스크 캐싱은 디스크에 저장하여 필요할 때 불러오는 방법으로 메모리 캐싱보다 속도가 느리지만 메모리 보다 많은 양의 데이터를 저장할 수 있다는 특징을 갖고 있다.

 

메모리 캐싱

안드로이드에서 메모리 캐싱은 LruCache를 사용한다.

class MemoryCache(maxSize: Int) {
    private val memoryCache = object: LruCache<String, Bitmap>(maxSize){
        override fun sizeOf(key: String, value: Bitmap): Int {
            return value.getByteCount() / 1024
        }
    }

    fun add(key: String, bitmap: Bitmap) {
        if (get(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }

    fun get(key: String): Bitmap? {
        return memoryCache[key]
    }
}

LruCache에 해당 key가 있다면 네트워크 요청을 하지 않고 메모리 있는 Bitmap을 사용하고, 없다면 네트워크 요청을 통해 서버에서 이미지를 불러온다.

 

LRUCache

LRU(Least Recently Used) 
: 가장 오랫동안 사용하지 않은 항목을 교체하는 알고리즘

 

 

LruCache는 내부적으로 LinkedHashMap을 사용하고 있다.

internal actual class LruHashMap<K : Any, V : Any> actual constructor(
    initialCapacity: Int,
    loadFactor: Float,
) {

    actual constructor(original: LruHashMap<out K, V>) : this() {
        for ((key, value) in original.entries) {
            put(key, value)
        }
    }

    private val map = LinkedHashMap<K, V>(initialCapacity, loadFactor, true)

    actual val isEmpty: Boolean get() = map.isEmpty()
    actual val entries: Set<Map.Entry<K, V>> get() = map.entries

    actual operator fun get(key: K): V? = map[key]

    actual fun put(key: K, value: V): V? = map.put(key, value)

    actual fun remove(key: K): V? = map.remove(key)
}

 

LinkedHashMap

1. key의 순서 보장

순서가 보장되지 않는 HashMap과는 다르게 LinkedHashMap은 Douoble-LinkedList를 사용하여 입력된 key의 순서를 보장한다.

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

 

2. Lru 알고리즘 최적화

LinkedHashMap을 초기화 할 때, accessOrder 값을 True로 설정하면 최근 사용한 Key의 노드를 가장 마지막 노드로 이동시킨다.

private val map = LinkedHashMap<K, V>(
    initialCapacity = initialCapacity, 
    loadFactor = loadFactor, 
    accessOrder = true // 기본 값 false
)

위의 LruHashMap의 LinkedHashMap 초기화를 보면 accessOrder 값을 True로 초기화 한 것을 볼 수 있다.

 

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e); // True인 경우, 사용 시 afterNodeAccess 호출
    return e.value;
}

 

accessOrder가 True일 때, get 메서드로 key를 사용하면 afterNodeAccess()를 사용하여 노드를 마지막으로 보내는 작업을 수행한다.

// Called after update, but not after insertion
void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last;
    LinkedHashMap.Entry<K,V> first;
    if ((putMode == PUT_LAST || (putMode == PUT_NORM && accessOrder)) && (last = tail) != e) {
        // move node to last
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    } else if (putMode == PUT_FIRST && (first = head) != e) {
        // move node to first
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = null;
        if (a == null)
            tail = b;
        else
            a.before = b;
        if (b != null)
            b.after = a;
        else
            first = a;
        if (first == null)
            tail = p;
        else {
            p.after = first;
            first.before = p;
        }
        head = p;
        ++modCount;
    }
}

 

동시성 제어로 인한 Thread-Safe

LruCache 내부에서 synchronized를 사용하여 동기화 작업을 수행하고 있는 것을 볼 수 있다.

public operator fun get(key: K): V? {
        var mapValue: V?
        lock.synchronized {
            mapValue = map[key]
            if (mapValue != null) {
                hitCount++
                return mapValue
            }
            missCount++
        }
        
        ...      
 }

 

 

'안드로이드 > 이미지' 카테고리의 다른 글

이미지 리사이징  (0) 2025.01.22
[안드로이드] 이미지 Glide  (0) 2023.11.03