이미지를 로딩할 때마다 서버에서 불러온다면 네트워크 요청이 자주 발생하게 되고, 이로 인하여 앱의 성능이 저하될 수 있다.
이를 개선하기 위해 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 |