Glide 缓存机制分析二,内存缓存(3.7.0为例 5)

缓存分为磁盘和内存两种,上一篇讲了磁盘缓存,其中提到了图片缓存到本地后,在 EngineJob 中切回主线程,handleResultOnMainThread() 中会把图片缓存到内存中,同时把图片回调到上一层,传给view,这里注意下保存到内存中的方法 listener.onEngineJobComplete(key, engineResource)

    @Override
    public void onEngineJobComplete(Key key, EngineResource<?> resource) {
        Util.assertMainThread();
        if (resource != null) {
            resource.setResourceListener(key, this);
            if (resource.isCacheable()) {
                activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
            }
        }
        jobs.remove(key);
    }

Glide 的内存分为 HashMap软引用 + LruCache 两种,这里的 activeResources 就是软引用缓存。内存为什么分两种,有什么好处?如果单独使用 LruCache,假如新图片所占内存比较大,甚至超过了 LruCache 的最大值,或者说 LruCache 中有一些图片,剩余的内存不多了,新图片添加后,就会新图片添加不到 LruCache 中,或者回收 LruCache 之前的图片,如果回收的图片正在UI上显示着,效果就不好了。

先分析从内存中取图片,再分析往内存中存图片。Glide 加载图片,会先从 LruCache 中获取,如果找到了,会把图片从 LruCache 中移除,放入软引用中;如果 LruCache 中没找到,继续从软引用中找;Engine 中 load() 方法,是获取图片的入口

    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) {
        ...

        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            return null;
        }

        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            return null;
        }

        ...
    }

这里可以看到,一系列view的参数及配置数据组成了一个key,经过 loadFromCache() 和 loadFromActiveResources() 两层获取,loadFromCache() 是从 LruCache 中获取,loadFromActiveResources() 是从软引用中获取,分别看下代码

     private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }

        EngineResource<?> cached = getEngineResourceFromCache(key);
        if (cached != null) {
            cached.acquire();
            activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
        }
        return cached;
    }

    private EngineResource<?> getEngineResourceFromCache(Key key) {
        Resource<?> cached = cache.remove(key);

        final EngineResource result;
        if (cached == null) {
            result = null;
        } else if (cached instanceof EngineResource) {
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true );
        }
        return result;
    }

isMemoryCacheable 这个值是控制是否使用内存的开关,在 Glide 加载图片时 skipMemoryCache(boolean skip) 设置的,这里 isMemoryCacheable=!skip; 注意 getEngineResourceFromCache() 方法第一行代码,这里是 cache.remove(key),是移除,然后添加到 activeResources 中,并调用了 cached.acquire() 方法,计数加一,表示有一个地方使用。再看看 loadFromActiveResources()

    private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> active = null;
        WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
        if (activeRef != null) {
            active = activeRef.get();
            if (active != null) {
                active.acquire();
            } else {
                activeResources.remove(key);
            }
        }
        return active;
    }

这里是直接从软引用中获取,如果有值,也是 active.acquire() 计数加一;可能有同学有疑惑了,LruCache 中如果有值会添加到软引用中,如果没有值,再来内存中查找,为什么这么设计?假如有两个相同大小的 ImageView 在两个不同的页面加载相同的url图片,一个先加载了,根据开头的逻辑,知道服务端返回的图片会添加到软引用中,第二个加载url时,此时先检查LruCache中没有,然后检查软引用,会在它里面找到图片返回。看到这,是否有疑惑,LruCache 移除了图片放到
软引用中,那软引用释放了内存,LruCache 中也没了,还得从磁盘中重新获取?google 厉害着呢,不会犯这个低级错误,软引用包裹的图片,会在释放资源被回收时,重新把图片放入 LruCache 缓存中。这就得说说如何缓存内存了。

软引用 Map<Key, WeakReference<EngineResource<?>>> activeResources = new HashMap<Key, WeakReference<EngineResource<?>>>(); 可以看出 WeakReference里面存储的是 EngineResource 类型,往 activeResources 中添加数据时发现 activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue())),ResourceWeakReference 继承了 WeakReference,添加图片的同时,也往里面传了一个 ReferenceQueue 队列

    private static class ResourceWeakReference extends WeakReference<EngineResource<?>> {
        private final Key key;

        public ResourceWeakReference(Key key, EngineResource<?> r, ReferenceQueue<? super EngineResource<?>> q) {
            super(r, q);
            this.key = key;
        }
    }

    private ReferenceQueue<EngineResource<?>> getReferenceQueue() {
        if (resourceReferenceQueue == null) {
            resourceReferenceQueue = new ReferenceQueue<EngineResource<?>>();
            MessageQueue queue = Looper.myQueue();
            queue.addIdleHandler(new RefQueueIdleHandler(activeResources, resourceReferenceQueue));
        }
        return resourceReferenceQueue;
    }

并且 ReferenceQueue 被创建的时候,又和 Looper 的队列产生了关联,先说 ResourceWeakReference 和 ReferenceQueue 的关系,它们两个会产生关联,ResourceWeakReference 构造方法中的形参 r 和 q 关联了起来,如果 r 对象被软引用释放了,那么 activeResources 集合中对应的 value 即 ResourceWeakReference 会被添加到 q 集合中,我写两个简单的 java 例子

    private static void test1(){
        ReferenceQueue<String> resourceReferenceQueue =  new ReferenceQueue<>();

        String s1 = new String("s1");
        String s2 = new String("s2");
        String s3 = new String("s3");
        HashMap<String, WeakReference<String>> hashMap = new HashMap<>();
        hashMap.put("a", new ResourceWeakReference("a", s1, resourceReferenceQueue));
        hashMap.put("b", new ResourceWeakReference("b", s2, resourceReferenceQueue));
        hashMap.put("c", new ResourceWeakReference("c", s3, resourceReferenceQueue));

        System.out.println(hashMap +"   " + resourceReferenceQueue.poll());

        System.out.println("**************" );

        s1 = null;
        s3 = null;
        System.gc();
        System.gc();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(resourceReferenceQueue.poll());
        System.out.println(resourceReferenceQueue.poll());
        System.out.println(resourceReferenceQueue.poll());

        System.out.println("**************" );

        for (String s : hashMap.keySet()){
            System.out.println(s + "  " + hashMap.get(s) + "  " + hashMap.get(s).get());
        }
    }

    private static class ResourceWeakReference extends WeakReference<String> {
        private final String key;
        
        // q 里面的对象,实际是外层的 ResourceWeakReference
        
        public ResourceWeakReference(String key, String r, ReferenceQueue<String> q) {
            super(r, q);
            this.key = key;
        }
    }

创建 s1、s2、s3 三个对象,添加到软引用集合中,然后切断其中两个值的引用,此时强制gc,看打印的日志

{a=com.example.cn.desigin.test.RxTest$ResourceWeakReference@404b9385, b=com.example.cn.desigin.test.RxTest$ResourceWeakReference@6d311334,
c=com.example.cn.desigin.test.RxTest$ResourceWeakReference@682a0b20}   null
**************
com.example.cn.desigin.test.RxTest$ResourceWeakReference@682a0b20
com.example.cn.desigin.test.RxTest$ResourceWeakReference@404b9385
null
**************
a  com.example.cn.desigin.test.RxTest$ResourceWeakReference@404b9385  null
b  com.example.cn.desigin.test.RxTest$ResourceWeakReference@6d311334  s2
c  com.example.cn.desigin.test.RxTest$ResourceWeakReference@682a0b20  null

从打印的日志,第一行是集合中元素的值和队列中是否有值,最后三行是for循环,遍历Map集合中的value值。

getReferenceQueue() 方法中,往 Looper 队列中添加了 MessageQueue.IdleHandler 回调,queueIdle() 返回值是 true,说明只要UI一旦有变动,绘制完毕后就会触发这个回调,queue 它里面装的都是被释放的资源,在这里把图片从软引用中移除了

    private static class RefQueueIdleHandler implements MessageQueue.IdleHandler {
        private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
        private final ReferenceQueue<EngineResource<?>> queue;

        public RefQueueIdleHandler(Map<Key, WeakReference<EngineResource<?>>> activeResources,
                ReferenceQueue<EngineResource<?>> queue) {
            this.activeResources = activeResources;
            this.queue = queue;
        }

        @Override
        public boolean queueIdle() {
            ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
            if (ref != null) {
                activeResources.remove(ref.key);
            }

            return true;
        }
    }

这里只是清除,那么怎么把软引用中的图片重新添加到LruCache中呢? EngineResource 有 acquire() 和 release() 方法,acquired 是int值,这两个方法分别是计数加一和减一,使用图片加一,页面关闭就减一,当 acquired 为0时,就会触发 listener.onResourceReleased(key, this) 回调,对应的是 Engine 中的

    @Override
    public void onResourceReleased(Key cacheKey, EngineResource resource) {
        Util.assertMainThread();
        activeResources.remove(cacheKey);
        if (resource.isCacheable()) {
            cache.put(cacheKey, resource);
        } else {
            resourceRecycler.recycle(resource);
        }
    }


    
在这里,重新加入 LruCache 中。 acquire() 方法在获取图片后回调给上一层时调用,那么 release() 呢? Engine 中有个 release(Resource resource) 方法调用了 EngineResource 的 release() 方法,那么 Engine 中方法又是哪里调用呢? 是在 GenericRequest 中,这个是准备请求信息的类中有几处调用,这里要注意clear()方法,它有好几处调用,有一处是 RequestManager 中,当Activity关闭页面,随着 Activity 的声明周期变化,会执行RequestManager的 onDestroy() 方法

    @Override
    public void onDestroy() {
        requestTracker.clearRequests();
    }

    //RequestTracker
    public void clearRequests() {
        for (Request request : Util.getSnapshot(requests)) {
            request.clear();
        }
        pendingRequests.clear();
    }

request 就是 GenericRequest,看到这也明白了Glide是如何控制图片的声明周期了。

从缓存中取图片,重要的便是标识 EngineKey key,它是在 Engine 的 load() 方法中创建,里面包含 id、width、height 等信息,Glide根据尺寸缓存图片也是由这里控制的,我们看看id是什么,它是由 fetcher.getId() 返回的,直接一步到位,看 HttpUrlFetcher 中的 getId()

    @Override
    public String getId() {
        return glideUrl.getCacheKey();
    }
    // GlideUrl
    public String getCacheKey() {
      return stringUrl != null ? stringUrl : url.toString();
    }


我们传进去一个String类型的url,会先转换为Uri,然后在 UriLoader 中 new GlideUrl(model.toString()) 转换为 GlideUrl 对象,这里id对应的就是一开始传进来的url的值,这也就是缓存是以 url 为准的。 一般我们的url都是固定不变的,但也有些例外,比如把图片上传到三方的云端,有些云服务器为了提高图片的查找效率,会在图片后面拼接一些自己的参数,关键这些参数是可变的,这就导致多个url对应同一张图片,如果按照Glide现在的逻辑,就会造成浪费,缓存效果很差。

怎么办呢?既然知道了图片的加载流程与id的获取方式,那么我们自己直接定义个 GlideUrl 的子类来封装url,重写 getCacheKey() 方法,在它里面对url做处理

    static class OGlideUrl extends GlideUrl {
        String url;
        public OGlideUrl(String url) {
            super(url);
            this.url = url;
        }

        @Override
        public String getCacheKey() {
            //去除Glide缓存key中多余的参数,这里是举个例子,以自己实际业务为主
            if (url.contains("?")) {
                return url.substring(0, url.lastIndexOf("?"));
            } else {
                return url;
            }
        }
    }

Glide.with(this).load(new OGlideUrl("http://static.v5kf.com/images/v5_chat/eval/pic.jpg")).into(iv);


有些童鞋可能就纳闷了,Glide 的构造方法中没有注册 OGlideUrl 类型的请求方法,这个需要我们自定义 GlideModule 中 registerComponents() 方法中注册 glide.register(OGlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());
吗?答案是不需要,因为 Glide.with(this).load() 方法,调用 

    private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
        ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
        ...
        return optionsApplier.apply(
                new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                        glide, requestTracker, lifecycle, optionsApplier));
    }

注意方法中第一行代码,进一步调用的是 GenericLoaderFactory 中的 getFactory(Class<T> modelClass, Class<Y> resourceClass) 方法,此时 modelClass 是 OGlideUrl.class 类型,

    private <T, Y> ModelLoaderFactory<T, Y> getFactory(Class<T> modelClass, Class<Y> resourceClass) {
        ...
        if (result == null) {
            for (Class<? super T> registeredModelClass : modelClassToResourceFactories.keySet()) {
                if (registeredModelClass.isAssignableFrom(modelClass)) {
                    Map<Class, ModelLoaderFactory> currentResourceToFactories = modelClassToResourceFactories.get(registeredModelClass);
                    if (currentResourceToFactories != null) {
                        result = currentResourceToFactories.get(resourceClass);
                        if (result != null) {
                            break;
                        }
                    }
                }
            }
        }
        return result;
    }

for循环中,注意 registeredModelClass.isAssignableFrom(modelClass) 这行代码,意思是 modelClass 是 registeredModelClass 对象本身或子类,都为true,看到这就明白了,这里传 Glide.with(this).load(OGlideUrl) 和 Glide.with(this).load(GlideUrl) 对应的都是 ModelLoaderFactory 是相同的,所以自定义对象继承 GlideUrl 即可,其他的不用操作。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页