关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 安卓开发 »Android-Universal-Image-Loader缓存处理机制分析

    Android-Universal-Image-Loader缓存处理机制分析

    2014-09-10 00:00:00 出处:孙国威的博客
    分享

    讲到缓存,平时流水线上的码农一定觉得这是一个高大上的东西。看过网上各种讲缓存原理的文章,总感觉那些文章讲的就是玩具,能用吗?这次我将带你一起看过UIL这个国内外大牛都追捧的图片缓存类库的缓存处理机制。看了UIL中的缓存实现,才发现其实这个东西不难,没有太多的进程调度,没有各种内存读取控制机制、没有各种异常处理。反正UIL中不单代码写的简单,连处理都简单。但是这个类库这么好用,又有这么多人用,那么非常有必要看看他是怎么实现的。先了解UIL中缓存流程的原理图。

    原理示意图

    主体有三个,分别是UI,缓存模块和数据源(网络)。它们之间的关系如下:

    ① UI:请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。

    ② 内存缓存:缓存搜索,假如能找到Key值对应的Bitmap,则返回数据。否则执行第三步。

    ③ 硬盘存储:使用唯一Key值对应的文件名,检索SDCard上的文件。

    ④ 假如有对应文件,使用BitmapFactory.decode*方法,解码Bitmap并返回数据,同时将数据写入缓存。假如没有对应文件,执行第五步。

    ⑤ 下载图片:启动异步线程,从数据源下载数据(Web)。

    ⑥ 若下载成功,将数据同时写入硬盘和缓存,并将Bitmap显示在UI中。

    接下来,我们回顾一下UIL中缓存的配置(具体的见《UNIVERSAL IMAGE LOADER.PART 2》)。重点关注注释部分,我们可以根据自己需要配置内存、磁盘缓存的实现。

    File cacheDir = StorageUtils.getCacheDirectory(context,
    "UniversalImageLoader/Cache");
    
    ImageLoaderConfiguration config = new
    ImageLoaderConfiguration .Builder(getApplicationContext())
    .maxImageWidthForMemoryCache(800)
    .maxImageHeightForMemoryCache(480)
    .httpConnectTimeout(5000)
    .httpReadTimeout(20000)
    .threadPoolSize(5)
    .threadPriority(Thread.MIN_PRIORITY + 3)
    .denyCacheImageMultipleSizesInMemory()
    .memoryCache(new UsingFreqLimitedCache(2000000)) // 你可以传入自己的内存缓存
    .discCache(new UnlimitedDiscCache(cacheDir)) // 你可以传入自己的磁盘缓存
    .defaultDisplayImageOptions(DisplayImageOptions.createSimple())
    .build();

    UIL中的内存缓存策略

    1. 只使用的是强引用缓存

    LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)

    2.使用强引用和弱引用相结合的缓存有

    UsingFreqLimitedMemoryCache(假如缓存的图片总量超过限定值,先删除使用频率最小的bitmap)

    LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用) FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap) LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象) LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)

    3.只使用弱引用缓存

    WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)

     

    我们直接选择UIL中的默认配置缓存策略进行分析。

    ImageLoaderConfiguration config = ImageLoaderConfiguration.createDefault(context);

    ImageLoaderConfiguration.createDefault(…)这个方法最后是调用Builder.build()方法创建默认的配置参数的。默认的内存缓存实现是LruMemoryCache,磁盘缓存是UnlimitedDiscCache。

    LruMemoryCache解析

    LruMemoryCache:一种使用强引用来保存有数量限制的Bitmap的cache(在空间有限的情况,保留最近使用过的Bitmap)。每次Bitmap被访问时,它就被移动到一个队列的头部。当Bitmap被添加到一个空间已满的cache时,在队列末尾的Bitmap会被挤出去并变成适合被GC回收的状态。
    注意:这个cache只使用强引用来保存Bitmap。

    LruMemoryCache实现MemoryCache,而MemoryCache继承自MemoryCacheAware。

    public interface MemoryCache extends MemoryCacheAware<String, Bitmap>

    下面给出继承关系图

    Android-Universal-Image-Loader.LruMemoryCache

    LruMemoryCache.get(…)

    我相信接下去你看到这段代码的时候会跟我一样惊讶于代码的简单,代码中除了异常判断,就是利用synchronized进行同步控制。

    /**
         * Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head
         * of the queue. This returns null if a Bitmap is not cached.
         */
        @Override
        public final Bitmap get(String key) {
            if (key == null) {
                throw new NullPointerException("key == null");
            }
    
            synchronized (this) {
                return map.get(key);
            }
        }

    我们会好奇,这不是就简简单单将Bitmap从map中取出来吗?但LruMemoryCache声称保留在空间有限的情况下保留最近使用过的Bitmap。不急,让我们细细观察一下map。他是一个LinkedHashMap<String, Bitmap>型的对象。

    LinkedHashMap中的get()方法不仅返回所匹配的值,并且在返回前还会将所匹配的key对应的entry调整在列表中的顺序(LinkedHashMap使用双链表来保存数据),让它处于列表的最后。当然,这种情况必须是在LinkedHashMap中accessOrder==true的情况下才生效的,反之就是get()方法不会改变被匹配的key对应的entry在列表中的位置。

    @Override public V get(Object key) {
     2         /*
     3          * This method is overridden to eliminate the need for a polymorphic
     4          * invocation in superclass at the expense of code duplication.
     5          */
     6         if (key == null) {
     7             HashMapEntry<K, V> e = entryForNullKey;
     8             if (e == null)
     9                 return null;
    10             if (accessOrder)
    11                 makeTail((LinkedEntry<K, V>) e);
    12             return e.value;
    13         }
    14 
    15         // Replace with Collections.secondaryHash when the VM is fast enough (http://b/8290590).
    16         int hash = secondaryHash(key);
    17         HashMapEntry<K, V>[] tab = table;
    18         for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
    19                 e != null; e = e.next) {
    20             K eKey = e.key;
    21             if (eKey == key || (e.hash == hash && key.equals(eKey))) {
    22                 if (accessOrder)
    23                     makeTail((LinkedEntry<K, V>) e);
    24                 return e.value;
    25             }
    26         }
    27         return null;
    28     }

    代码第11行的makeTail()就是调整entry在列表中的位置,其实就是双向链表的调整。它判断accessOrder

    。到现在我们就清楚LruMemoryCache使用LinkedHashMap来缓存数据,在LinkedHashMap.get()方法执行后,LinkedHashMap中entry的顺序会得到调整。那么我们怎么保证最近使用的项不会被剔除呢?接下去,让我们看看LruMemoryCache.put(…)。

    LruMemoryCache.put(…)

    注意到代码第8行中的size+= sizeOf(key, value),这个size是什么呢?我们注意到在第19行有一个trimToSize(maxSize),trimToSize(…)这个函数就是用来限定LruMemoryCache的大小不要超过用户限定的大小,cache的大小由用户在LruMemoryCache刚开始初始化的时候限定。

    @Override
     2     public final boolean put(String key, Bitmap value) {
     3         if (key == null || value == null) {
     4             throw new NullPointerException("key == null || value == null");
     5         }
     6 
     7         synchronized (this) {
     8             size += sizeOf(key, value);
     9             //map.put()的返回值假如不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry
    10             Bitmap previous = map.put(key, value);
    11             if (previous != null) {
    12                 size -= sizeOf(key, previous);
    13             }
    14         }
    15 
    16         trimToSize(maxSize);
    17         return true;
    18     }

    其实不难想到,当Bitmap缓存的大小超过原来设定的maxSize时应该是在trimToSize(…)这个函数中做到的。这个函数做的事情也简单,遍历map,将多余的项(代码中对应toEvict)剔除掉,直到当前cache的大小等于或小于限定的大小。

    private void trimToSize(int maxSize) {
     2         while (true) {
     3             String key;
     4             Bitmap value;
     5             synchronized (this) {
     6                 if (size < 0 || (map.isEmpty() && size != 0)) {
     7                     throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
     8                 }
     9 
    10                 if (size <= maxSize || map.isEmpty()) {
    11                     break;
    12                 }
    13 
    14                 Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
    15                 if (toEvict == null) {
    16                     break;
    17                 }
    18                 key = toEvict.getKey();
    19                 value = toEvict.getValue();
    20                 map.remove(key);
    21                 size -= sizeOf(key, value);
    22             }
    23         }
    24     }

    这时候我们会有一个以为,为什么遍历一下就可以将使用最少的bitmap缓存给剔除,不会误删到最近使用的bitmap缓存吗?首先,大家要清楚,LruMemoryCache定义的最近使用是指最近用get或put方式操作到的bitmap缓存。其次,之前我们直到LruMemoryCache的get操作其实是通过其内部字段LinkedHashMap.get(…)实现的,当LinkedHashMap的accessOrder==true时,每一次get或put操作都会将所操作项(图中第3项)移动到链表的尾部(见下图,链表头被认为是最少使用的,链表尾被认为是最常使用的。),每一次操作到的项我们都认为它是最近使用过的,当内存不够的时候被剔除的优先级最低。需要注意的是一开始的LinkedHashMap链表是按插入的顺序构成的,也就是第一个插入的项就在链表头,最后一个插入的就在链表尾。假设只要剔除图中的1,2项就能让LruMemoryCache小于原先限定的大小,那么我们只要从链表头遍历下去(从1→最后一项)那么就可以剔除使用最少的项了。

    至此,我们就知道了LruMemoryCache缓存的整个原理,包括他怎么put、get、剔除一个元素的的策略。接下去,大家要开始分析默认的磁盘缓存策略了。

    UIL中的磁盘缓存策略

    像新浪微博、花瓣这种应用需要加载很多图片,本来图片的加载就慢了,假如下次打开的时候还需要再一次下载上次已经有过的图片,相信用户的流量会让他们的叫骂声很响亮。对于图片很多的应用,一个好的磁盘缓存直接决定了应用在用户手机的留存时间。我们自己实现磁盘缓存,要考虑的太多,幸好UIL提供了几种常见的磁盘缓存策略,当然假如你觉得都不符合你的要求,你也可以自己去扩展

    FileCountLimitedDiscCache(可以设定缓存图片的个数,当超过设定值,删除掉最先加入到硬盘的文件) LimitedAgeDiscCache(设定文件存活的最长时间,当超过这个值,就删除该文件) TotalSizeLimitedDiscCache(设定缓存bitmap的最大值,当超过这个值,删除最先加入到硬盘的文件) UnlimitedDiscCache(这个缓存类没有任何的限制)

    在UIL中有着比较完整的存储策略,根据预先指定的空间大小,使用频率(生命周期),文件个数的约束条件,都有着对应的实现策略。最基础的接口DiscCacheAware和抽象类BaseDiscCache

    UnlimitedDiscCache解析

    UnlimitedDiscCache实现disk cache接口,是ImageLoaderConfiguration中默认的磁盘缓存处理。用它的时候,磁盘缓存的大小是不受限的。

    接下来我们来看看实现UnlimitedDiscCache的源代码,通过源代码我们发现他其实就是继承了BaseDiscCache,这个类内部没有实现自己独特的方法,也没有重写什么,那么我们就直接看BaseDiscCache这个类。在分析这个类之前,我们先想想自己实现一个磁盘缓存需要做多少麻烦的事情:

    1、图片的命名会不会重。你没有办法知道用户下载的图片原始的文件名是怎么样的,因此很可能因为文件重名将有用的图片给覆盖掉了。

    2、当应用卡顿或网络延迟的时候,同一张图片反复被下载。

    3、处理图片写入磁盘可能遇到的延迟和同步问题。

    BaseDiscCache构造函数

    首先,我们看一下BaseDiscCache的构造函数:

    cacheDir:文件缓存目录
    reserveCacheDir:备用的文件缓存目录,可以为null。它只有当cacheDir不能用的时候才有用。
    fileNameGenerator:文件名生成器。为缓存的文件生成文件名。

    public BaseDiscCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
            if (cacheDir == null) {
                throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);
            }
            if (fileNameGenerator == null) {
                throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL);
            }
    
            this.cacheDir = cacheDir;
            this.reserveCacheDir = reserveCacheDir;
            this.fileNameGenerator = fileNameGenerator;
        }

    我们可以看到一个fileNameGenerator,接下来我们来了解UIL具体是怎么生成不重复的文件名的。UIL中有3种文件命名策略,这里我们只对默认的文件名策略进行分析。默认的文件命名策略在DefaultConfigurationFactory.createFileNameGenerator()。它是一个HashCodeFileNameGenerator。真的是你意想不到的简单,就是运用String.hashCode()进行文件名的生成。

    public class HashCodeFileNameGenerator implements FileNameGenerator {
        @Override
        public String generate(String imageUri) {
            return String.valueOf(imageUri.hashCode());
        }
    }

    BaseDiscCache.save()

    分析完了命名策略,再看一下BaseDiscCache.save(…)方法。注意到第2行有一个getFile()函数,它主要用于生成一个指向缓存目录中的文件,在这个函数里面调用了刚刚介绍过的fileNameGenerator来生成文件名。注意第3行的tmpFile,它是用来写入bitmap的临时文件(见第8行),然后就把这个文件给删除了。大家可能会困惑,为什么在save()函数里面没有判断要写入的bitmap文件是否存在的判断,我们不由得要看看UIL中是否有对它进行判断。还记得我们在《从代码分析Android-Universal-Image-Loader的图片加载、显示流程》介绍的,UIL加载图片的一般流程是先判断内存中是否有对应的Bitmap,再判断磁盘(disk)中是否有,假如没有就从网络中加载。最后根据原先在UIL中的配置判断是否需要缓存Bitmap到内存或磁盘中。也就是说,当需要调用BaseDiscCache.save(…)之前,其实已经判断过这个文件不在磁盘中。

    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
     2         File imageFile = getFile(imageUri);
     3         File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
     4         boolean loaded = false;
     5         try {
     6             OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
     7             try {
     8                 loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
     9             } finally {
    10                 IoUtils.closeSilently(os);
    11             }
    12         } finally {
    13             IoUtils.closeSilently(imageStream);
    14             if (loaded && !tmpFile.renameTo(imageFile)) {
    15                 loaded = false;
    16             }
    17             if (!loaded) {
    18                 tmpFile.delete();
    19             }
    20         }
    21         return loaded;
    22     }

    BaseDiscCache.get()

    BaseDiscCache.get()方法内部调用了BaseDiscCache.getFile(…)方法,让我们来分析一下这个在之前碰过的函数。 第2行就是利用fileNameGenerator生成一个唯一的文件名。第3~8行是指定缓存目录,这时候你就可以清楚地看到cacheDir和reserveCacheDir之间的关系了,当cacheDir不可用的时候,就是用reserveCachedir作为缓存目录了。

    最后返回一个指向文件的对象,但是要注意当File类型的对象指向的文件不存在时,file会为null,而不是报错。

    protected File getFile(String imageUri) {
     2         String fileName = fileNameGenerator.generate(imageUri);
     3         File dir = cacheDir;
     4         if (!cacheDir.exists() && !cacheDir.mkdirs()) {
     5             if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
     6                 dir = reserveCacheDir;
     7             }
     8         }
     9         return new File(dir, fileName);
    10     }

    总结

    现在,我们已经分析了UIL的缓存机制。其实从UIL的缓存机制的实现并不是很复杂,虽然有各种缓存机制,但是简单地说:内存缓存其实就是利用Map接口的对象在内存中进行缓存,可能有不同的存储机制。磁盘缓存其实就是将文件写入磁盘。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    SQL汉字转换为拼音的函数

    windows 7系统无法运行Photoshop CS3的解决方法

    巧用MySQL加密函数对Web网站敏感数据进行保护

    MySQL基础知识简介

    Windows7和WinXP下如何实现不输密码自动登录系统的设置方法介绍

    windows 7系统ip地址冲突怎么办?windows 7系统IP地址冲突问题的

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。