剖析LRU实现和Glide中缓存机制

在这里插入图片描述

前言

在之前一篇文章《剖析Picasso中的内存缓存机制——LruCache》我详细的说了说Picasso中的缓存机制,今天我来聊一聊LRU和Google官方推荐图片框架——Glide中的缓存机制,大家可以对比一下。

LRU是Least Recently Used的缩写,即最近最少使用,当超过容量时,自动删除最近最少使用的项目。

LRU在android开发中最常见的就是图片加载框架中的缓存逻辑。在Google官方推荐图片框架Glide中就有LRU的影子。

LinkedListMap

在java中可以利用LinkedListMap很方便的实现LRU,虽然LinkedHashMap实现了按访问排序和移除最近最少,但是默认是不使用的,需要我们改造一下,代码如下:

class LRUCache extends LinkedHashMap<Integer, Integer> {
    //LinkedHashMap实现了按访问排序和移除最近最少,但是默认是不使用的,需要我们改造一下
    private int capacity;

    public LRUCache(int capacity) {
        //这里的第三个参数true就是使用访问排序,这样每次访问都是更新链表
        //注意,这里的第一个参数是map的初始大小,并不是限定容量
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        //这个函数默认返回false,如果不重写这个方法,永远不会进行清除
        //所以这里要重写一下,判断是否进行清除。
        return size() > capacity;
    }
}

注意看在我们的构造函数中调用了LinkedListMap的三参数构造函数super(capacity, 0.75F, true),这里的第三个参数true就是使用访问排序,这样每次访问都是更新链表(注意,这里的第一个参数是map的初始大小,并不是限定容量)

另外removeEldestEntry默认返回false,如果不重写这个方法,永远不会进行清除。所以这里要重写一下,判断是否进行清除。

那么这其中的原理是什么呢?

原理

首先就是LinkedHashMap,它与HashMap的区别就是在Map的结构外还有一个链表结构,Map的每个key-value都会以Node的形式同时存储在Map和链表中。

当put一个key和value时,加入Map的同时,也加入链表尾部,这样实现了有序(HashMap本来是无序的)。

这里就不展开说了,简单知道有这个链表就可以了,正是这个链表完成了LRU的实现。

简单来说就是get(使用了)的时候将该Node从链表中移除,并添加到链表尾部。

put的时候,检查Map的容量,如果超出了,就从移除链表头部Node(时间最久的,即最近最少使用),同时从map中移除。

那么代码中是如何实现的?

先看get函数,这个函数LinkedHashMap进行了重写:

public V get(Object var1) {
    Node var2;
    if ((var2 = this.getNode(hash(var1), var1)) == null) {
        return null;
    } else {
        if (this.accessOrder) {
            this.afterNodeAccess(var2);
        }

        return var2.value;
    }
}

public V getOrDefault(Object var1, V var2) {
    Node var3;
    if ((var3 = this.getNode(hash(var1), var1)) == null) {
        return var2;
    } else {
        if (this.accessOrder) {
            this.afterNodeAccess(var3);
        }

        return var3.value;
    }
}

可以看到有这么一段代码:

if (this.accessOrder) {
    this.afterNodeAccess(var3);
}

这里的accessOrder就是LinkedHashMap的三参数构造函数super(capacity, 0.75F, true)中的第三个参数,是否使用访问排序。

默认是false,如果为ture就使用访问排序,这时候get函数就会调用afterNodeAccess,这个函数是HashMap的,但是在HashMap中是个空函数,没有任何实现:

void afterNodeAccess(HashMap.Node<K, V> var1) {
}

所以如果使用HashMap,这个访问排序就没有什么意义。但是在LinkedHashMap中对这个函数进行了重写:

void afterNodeAccess(Node<K, V> var1) {
    LinkedHashMap.Entry var2;
    if (this.accessOrder && (var2 = this.tail) != var1) {
        LinkedHashMap.Entry var3 = (LinkedHashMap.Entry)var1;
        LinkedHashMap.Entry var4 = var3.before;
        LinkedHashMap.Entry var5 = var3.after;
        var3.after = null;
        if (var4 == null) {
            this.head = var5;
        } else {
            var4.after = var5;
        }

        if (var5 != null) {
            var5.before = var4;
        } else {
            var2 = var4;
        }

        if (var2 == null) {
            this.head = var3;
        } else {
            var3.before = var2;
            var2.after = var3;
        }
        
        this.tail = var3;
        ++this.modCount;
    }

}

简单来说就是将get函数访问的这个Node从链表中移除,添加到结尾。
所以可以看到,如果想实现LRU,accessOrder就必须为true,否则无效。

自动清理

这样实现了访问排序,那么如何实现的自动清理呢?

我们来看看put函数,put函数LinkedHashMap没有重写,所以在它的父类HashMap中:

public V put(K var1, V var2) {
    return this.putVal(hash(var1), var1, var2, false, true);
}

final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
    HashMap.Node[] var6;
    int var8;
    if ((var6 = this.table) == null || (var8 = var6.length) == 0) {
        var8 = (var6 = this.resize()).length;
    }

    Object var7;
    int var9;
    if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
        var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
    } else {
        ...

        if (var10 != null) {
            Object var13 = ((HashMap.Node)var10).value;
            if (!var4 || var13 == null) {
                ((HashMap.Node)var10).value = var3;
            }

            this.afterNodeAccess((HashMap.Node)var10);
            return var13;
        }
    }

    ++this.modCount;
    if (++this.size > this.threshold) {
        this.resize();
    }

    this.afterNodeInsertion(var5);
    return null;
}

这里重点关注链表的逻辑,可以看到如果Map中不存在这个key,就新建一个

if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
    var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
} else {

但是如果存在,除了重新赋值之外,还执行了afterNodeAccess,将它移到了链表尾部。

if (var10 != null) {
    ...

    this.afterNodeAccess((HashMap.Node)var10);
    return var13;
}

因为重新赋值就是访问了该元素,所以需要重新排序。

那么这里受不受accessOrder的控制,因为accessOrder是LinkedHashMap的一个变量,而put是HashMap实现的,所以在put的代码中并没有判断accessOrder。但是在上面afterNodeAccess函数的源码中可以看到一开始就判断了accessOrder,所以这里也是受accessOrder的控制的。这也是为什么在get函数中判断了accessOrder,在afterNodeAccess又一次判断的原因。

最后还执行了afterNodeInsertion,这个函数与afterNodeAccess一样,在HashMap中是空函数,没有任何代码。在LinkedHashMap实现了,如下:

void afterNodeInsertion(boolean var1) {
    LinkedHashMap.Entry var2;
    if (var1 && (var2 = this.head) != null && this.removeEldestEntry(var2)) {
        Object var3 = var2.key;
        this.removeNode(hash(var3), var3, (Object)null, false, true);
    }

}

可以看到这个函数实现了移除Map中的元素,这样就实现了清理。但是并不是每次都清理,所以需要removeEldestEntry判断。

这个函数是LinkedHashMap的函数,但是在LinkedHashMap中这个函数也是一个空函数,默认返回false

protected boolean removeEldestEntry(java.util.Map.Entry<K, V> var1) {
    return false;
}

所以永远不会清理,这也是我们为什么一定要重写这个函数的原因。通过我们上面的重写,当Map容量大于我们规定的上限时就返回true,这样就执行了清理。

@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
    //这个函数默认返回false,如果不重写这个方法,永远不会进行清除
    //所以这里要重写一下,判断是否进行清除。
    return size() > capacity;
}

注意

所以要用LinkedHashMap实现LRU,有两个非常重要的点:

  • 1、accessOrder必须为true。否则链表不会重排。

注意:如果为false,还是可以清理的(下面会说到),这时候如果清理实际上就是清理存入最久的,也就相当于一个普通的队列。

  • 2、必须重写removeEldestEntry。否则永远不会清理。

那么上面提到的一个问题,清理是否受accessOrder影响?是不是accessOrder为false就不清理了?

答案是不影响,重排和清理是互不影响的,在afterNodeInsertion的整个流程中没有accessOrder的出现。

所以上面提到了,如果accessOrder为false也可以清理,只不过清理的是存入最久的(忽略访问),并不是LRU算法。

Glide内存缓存

glide的内存缓存有两级:LruCache、ActiveResources

其中LruCache老生常谈了,这里就不细说了。

ActiveResources实际上内含一个HashMap,Map中value则是资源的弱引用。

那么这两级是如何工作的?

读取缓存

先来看看load函数的源码,代码如下:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        //生成缓存的key
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());
        //从LruCache获取缓存图片
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }
        //从弱引用获取图片
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }

        EngineJob current = jobs.get(key);
        if (current != null) {
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

    ...
}

可以看到

  • 先通过buildKey函数生成缓存用到的可以

  • 然后通过loadFromCache函数尝试从LruCache中来获取,如果获取成功则通过onResourceReady函数返回

  • LruCache中不存在的话再通过loadFromActiveResources函数从ActiveResources中获取,如果获取成功则通过onResourceReady函数返回

  • 缓存不存在的话创建一个任务来加载图片

这里注意:如果LruCache中有,则取出存入ActiveResources,并从LruCache移除。

写入缓存

如果内存本地都没有,则从网络获取,获取后先存入ActiveResources,ActiveResources中存储的是EngineResource对象的弱引用。

EngineResource是将资源进行封装的一个类,它有一个计数acquired,记录资源被引用的次数,当资源被取出使用时+1(acquired函数),当资源被释放时-1(release函数)。当acquired为0时,会将它从ActiveResources中移除,存入LruCache。

代码如下:

void release() {
  synchronized (listener) {
    synchronized (this) {
      if (acquired <= 0) {
        throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
      }
      if (--acquired == 0) {
        listener.onResourceReleased(key, this);
      }
    }
  }
}

listener是Engine对象

@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
  activeResources.deactivate(cacheKey);
  if (resource.isCacheable()) {
    cache.put(cacheKey, resource);
  } else {
    resourceRecycler.recycle(resource);
  }
}

可以看到如果开启内存缓存,则存入LruCache,否则直接释放。

两级缓存

这样我们就比较明白glide内存的两级缓存是怎么回事了,实际上是对缓存的资源进行了划分:使用中的和使用过的。

使用中的放入ActiveResources,这样可以防止被LruCache算法回收掉;而使用过的放在LruCache中,通过算法控制内存总量。

release何时执行

上面我们知道当资源被使用时会调用EngineResource的acquired函数,释放的时候会调用EngineResource的release函数。

使用的时候我们比较好理解,取出的时候其实就是使用的时候,这是一个主动的动作。

但是何时释放?glide中是怎么监控资源释放的?

通过查找EngineResource的release函数的调用,找到在Engine中

public void release(Resource<?> resource) {
  if (resource instanceof EngineResource) {
    ((EngineResource<?>) resource).release();
  } else {
    throw new IllegalArgumentException("Cannot release anything but an EngineResource");
  }
}

继续查找这个函数在哪里调用,发现在SingleRequest中

@Override
public synchronized void clear() {
  assertNotCallingCallbacks();
  stateVerifier.throwIfRecycled();
  if (status == Status.CLEARED) {
    return;
  }
  cancel();
  // Resource must be released before canNotifyStatusChanged is called.
  if (resource != null) {
    releaseResource(resource);
  }
  if (canNotifyCleared()) {
    target.onLoadCleared(getPlaceholderDrawable());
  }

  status = Status.CLEARED;
  
  if (toRelease != null) {
    engine.release(toRelease);
  }
}

那么这个clear函数又在哪里调用?在ViewTarget中

@Synthetic void pauseMyRequest() {
  Request request = getRequest();
  // If the Request were cleared by the developer, it would be null here. The only way it's
  // present is if the developer hasn't previously cleared this Target.
  if (request != null) {
    isClearedByUs = true;
    request.clear();
    isClearedByUs = false;
  }
}

ViewTarget是对要加载图片的ImageView进行封装,而资源的释放也必然与View有关系。

ViewTarget有一个字段protected final T view;这就是要加载图片的ImageView,另外在ViewTarget中可以看到对这个view添加了attach监听:

view.addOnAttachStateChangeListener(attachStateListener);

这个attachStateListener的源码:

attachStateListener = new OnAttachStateChangeListener() {
  @Override
  public void onViewAttachedToWindow(View v) {
    resumeMyRequest();
  }

  @Override
  public void onViewDetachedFromWindow(View v) {
    pauseMyRequest();
  }
};

这样就很明显了,每个加载图片的view都会注册一个OnAttachStateChangeListener,当这个view从界面移除的时候,也就是资源不再被引用的时候,就会调用pauseMyRequest,最终会将EngineResource的引用计数-1。

这样就保证了当ActiveResources中的资源不再被引用时,将这个资源转移到LruCache中。

总结

LRU算法是非常常用的算法之一,基于LinkedListMap可以很容易实现。Glide也是基于这种算法进行缓存的,不过Glide除了使用LRU还有另外一级缓存。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值