深入解析开源项目之Universal-Image-Loader(二)缓存篇

来源:互联网 发布:spss数据分析游戏 编辑:程序博客网 时间:2024/06/03 17:52

珍惜作者劳动成果,如需转载,请注明出处。
http://blog.csdn.net/zhengzechuan91/article/details/50292871

Universal-Image-Loader 是一个优秀的图片加载开源项目,Github地址在 (Github地址) ,很多童鞋都在自己的项目中用到了。优秀的项目从来都是把简单留给开发者,把复杂封装在框架内部。ImageLoader作为Github上Star数过万的项目,备受开发者青睐,所以我们有必要搞清楚它的内部实现。

在上一篇博客中我们分析了ImageLoader框架的整体实现原理,还没有看过的直接到 深入解析开源项目之ImageLoader(一)框架篇 。


ImageLoader缓存之内存篇

MemeryCache关系图

由上图我们可以看出:

  1. MemoryCache接口定义了Bitmap缓存相关操作;

  2. 抽象类BaseMemoryCache中则使用HashMap保存了Bitmap的软/弱引用;

  3. LimitedMemoryCache类则使用LinkedList保存了Bitmap强引用,并加入了对最大可用内存的限定。


BaseMemoryCache中的代码很简单,只是简单的将Bimtap从保存软/弱引用的HashMap中添加或移除。

接下来我们看看LimitedMemoryCache中是怎样实现将Bitmap缓存在内存中的:

/* LimitedMemoryCache.java */    public boolean put(String key, Bitmap value) {        //这个字段表示是否添加到强引用成功        boolean putSuccessfully = false;        int valueSize = getSize(value);        int sizeLimit = getSizeLimit();        int curCacheSize = cacheSize.get();        //如果Bitmap尺寸没超过限定尺寸        if (valueSize < sizeLimit) {            while (curCacheSize + valueSize > sizeLimit) {                //如果添加Bitmap后超过了限定尺寸,根据抽象方                //法removeNext()移除强引用列表中的Bitmap,                //直到小于限定尺寸                Bitmap removedValue = removeNext();                if (hardCache.remove(removedValue)) {                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));                }            }            //添加到强引用的列表中            hardCache.add(value);            cacheSize.addAndGet(valueSize);            putSuccessfully = true;        }        //添加到软/弱引用中        super.put(key, value);        return putSuccessfully;    }

在看看LimitedMemoryCache的移除操作:

/* LimitedMemoryCache.java */    public Bitmap remove(String key) {        Bitmap value = super.get(key);        if (value != null) {            //软/弱引用存在,移除强引用            if (hardCache.remove(value)) {                cacheSize.addAndGet(-getSize(value));            }        }        //移除软/弱引用        return super.remove(key);    }

再看看/cache/memory/impl实现中,分为三类:

实现MemeryCache 继承LimitedMemoryCache 继承BaseMemoryCache FuzzyKeyMemoryCache FIFOLimitedMemoryCache WeakMemoryCache LimitedAgeMemoryCache LargestLimitedMemoryCache LruMemoryCache LRULimitedMemoryCache UsingFreqLimitedMemoryCache

下面逐一对上面的实现分析:

实现MemeryCache


FuzzyKeyMemoryCache

用到了装饰模式,对MemoryCache的put(String key, Bitmap value)方法进行加强处理:先移除key相同的Bitmap,再添加新的key对应的Bitmap:

/* FuzzyKeyMemoryCache.java */    public boolean put(String key, Bitmap value) {        synchronized (cache) {            String keyToRemove = null;            for (String cacheKey : cache.keys()) {                if (keyComparator.compare(key, cacheKey) == 0) {                    //找到Uri相同的                    keyToRemove = cacheKey;                    break;                }            }            //先移除Uri相同的Cache            if (keyToRemove != null) {                cache.remove(keyToRemove);            }        }        //再将新的key对应的值放入Cache        return cache.put(key, value);    }

LimitedAgeMemoryCache

也是对MemoryCache的装饰,对MemeryCache的get(String key)做了加强处理:当我们在获取内存Cache中的Bitmap时,如果超过最大存活时间则不在返回

/* LimitedAgeMemoryCache.java */    public Bitmap get(String key) {        Long loadingDate = loadingDates.get(key);        if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) {            //如果超过最大存活时间,则从Cache中移除掉            cache.remove(key);            loadingDates.remove(key);        }        return cache.get(key);    }

LruMemoryCache

在LinkedHashMap中保存了Bitmap的强引用,并限定了强引用的最大内存,这里稍微解释下LinkedHashMap的构造方法:

/* LruMemoryCache.java */        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);

主要是最后一个参数:返回true表示排序的顺序是从最远使用到最近使用,而返回false顺序则为插入时的顺序。

再来看LruMemoryCache#put()方法的实现:

/* LruMemoryCache.java */    public final boolean put(String key, Bitmap value) {        synchronized (this) {            size += sizeOf(key, value);            //如果之前没有没有添加过,添加后返回null            Bitmap previous = map.put(key, value);            //说明该key对应的Bitmap已经存在            if (previous != null) {                size -= sizeOf(key, previous);            }        }        //遍历LinkedList,移除最远使用的Bitmap,直到小于最大        //限定的内存大小        trimToSize(maxSize);        return true;    }

移除的操作如下:

/* LruMemoryCache.java */    public final Bitmap remove(String key) {        synchronized (this) {            //移除key对应的Bitmap,存在则返回该Bitmap,否则返            //回null            Bitmap previous = map.remove(key);            //如果移除的Bitmap存在            if (previous != null) {                size -= sizeOf(key, previous);            }            return previous;        }    }

我们看到map实例化时并不是线程安全的,所以在所有的操作中都有同步锁。


继承LimitedMemoryCache

我们知道这类Cache缓存都是软/弱引用和限定内存的强引用的结合(强引用是List,软引用是Map),重写LimitedMemoryCache主要是来实现removeNext()方法,以指定超过内存最大限定后移除Bitmap的规则。


FIFOLimitedMemoryCache

先进先出队列

/* FIFOLimitedMemoryCache.java */    public boolean put(String key, Bitmap value) {        //成功添加到强引用和软/弱引用中,其中在超出内存限定后会        //对内存中的Bitmap缩减        if (super.put(key, value)) {            //添加到队列中            queue.add(value);            return true;        } else {            return false;        }    }

我们看到queue是没限定内存的,难道不怕在我们滑动列表的过程中内存溢出?

如果你注意到了这个细节,说明你代码看的很仔细,我们来分析下这块的逻辑:我们在调用super.put(key, value)时,如果发现内存超出了限定值,我们会根据removeNext()来依次删除强引用中的Bitmap,而在FIFOLimitedMemoryCache中,removeNext()实现如下:

/* FIFOLimitedMemoryCache.java */    protected Bitmap removeNext() {        return queue.remove(0);    }

LinkedList默认就是根据插入时的顺序的,所以直接返回第一个元素,同时从queue中移除了第一个元素,所以也达到了限定内存的目的。

/* FIFOLimitedMemoryCache.java */    public Bitmap remove(String key) {        Bitmap value = super.get(key);        //如果软/弱引用存在        if (value != null) {            //从队列移除            queue.remove(value);        }        //从强引用和软/弱引用移除        return super.remove(key);    }

这个queue与父类强引用其实是一样的,都是LinkedList,queue之所以存在是因为为了代码的一致性,父类中并没有对外部(包括子类)暴露强引用,我们没办法对强引用直接操作,所以大家都是缓存一份自己的队列。


LargestLimitedMemoryCache

在put()时保存了每个Bitmap的size,这个类主要实现了超过内存限定后删除Bitmap的removeNext()方法:找出占用内存最大的图片返回。


LRULimitedMemoryCache

利用LinkedHashMap的特性实现LRU的特点,重点也是重写了removeNext()方法:

/* LRULimitedMemoryCache.java */    //用LinkedHashMap来保存Bitmap    private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));

跟LruMemoryCache中的Map实现方式相似:

/* LRULimitedMemoryCache.java */    protected Bitmap removeNext() {        Bitmap mostLongUsedValue = null;        synchronized (lruCache) {            Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();            if (it.hasNext()) {                Entry<String, Bitmap> entry = it.next();                mostLongUsedValue = entry.getValue();                it.remove();            }        }        return mostLongUsedValue;    }

由于LinkedHashMap的特点,以上方法每次迭代lruCache时,都是先删除最远使用的。


UsingFreqLimitedMemoryCache

用一个HashMap来保存每个Bitmap的使用次数,当调用put()方法时,使用次数置零,当调用get()方法时,使用次数加1。removeNext()的实现方式比较简单:直接找出hashmap中使用次数最少的,然后返回。

到此,对ImageLoader内存缓存的分析结束。


ImageLoader缓存之磁盘篇

看完内存缓存的分析,再来看看磁盘缓存的逻辑。

相关的接口和类的关系如下:

ImageLoader磁盘关系图


DiskCache

首先是DiskCache接口定义了磁盘的一些基本操作:

/* DiskCache.java */public interface DiskCache {    File getDirectory();    File get(String imageUri);    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;    boolean save(String imageUri, Bitmap bitmap) throws IOException;    boolean remove(String imageUri);    void close();    void clear();}

BaseDiskCache

而基类BaseDiskCache则实现了这些基本操作,并定义了一些默认值

/* BaseDiskCache.java */    //默认的buffer大小为32k    public static final int DEFAULT_BUFFER_SIZE = 32 * 1024;    //默认的图片格式为PNG    public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;    //默认的图片质量为100    public static final int DEFAULT_COMPRESS_QUALITY = 100;

save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener)方法从输入流读取数据,然后根据imageUri,将数据读取到磁盘上。

这个方法最主要的代码是:

/* BaseDiskCache.java */loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);

我们看看copyStream()方法的实现:

/* BaseDiskCache.java */    //返回true表示成功的读取了输入流,返回false则表示listener    //中断了输入流的读取。    public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)            throws IOException {        int current = 0;        int total = is.available();        if (total <= 0) {            total = DEFAULT_IMAGE_TOTAL_SIZE;        }        final byte[] bytes = new byte[bufferSize];        int count;        //拦截加载        if (shouldStopLoading(listener, current, total)) return false;        while ((count = is.read(bytes, 0, bufferSize)) != -1) {            os.write(bytes, 0, count);            current += count;            //拦截加载            if (shouldStopLoading(listener, current, total)) return false;        }        //如果是buffer类型,flush()        os.flush();        return true;    }

我们看看shouldStopLoading()listener中是怎么拦截的:

/* BaseDiskCache.java */    private static boolean shouldStopLoading(CopyListener listener, int current, int total) {        if (listener != null) {            boolean shouldContinue = listener.onBytesCopied(current, total);            if (!shouldContinue) {                if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {                    return true;                 }            }        }        return false;    }

当设置了listener拦截数据后,每次得到current和total并处理后,通知回来是否还要继续读取输入流,如果不继续读取,则会判断当前加载进度是否超过了75%,超过75%会强制加载完,否则会停止读取。

也就是说,当我们不想继续传输的时候,我们只需要将listener的onBytesCopied()方法返回false即可。

而save(String imageUri, Bitmap bitmap)方法则是通过Bitmap#compress(CompressFormat format, int quality, OutputStream stream)方法则是将Bitmap直接缓存在imageUri所在的磁盘目录下。

BaseDiskCache中的File都是通过getFile(String imageUri)来取的:
我们看看这个方法的代码:

/* BaseDiskCache.java */    protected File getFile(String imageUri) {        String fileName = fileNameGenerator.generate(imageUri);        File dir = cacheDir;        if (!cacheDir.exists() && !cacheDir.mkdirs()) {            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {                dir = reserveCacheDir;            }        }        return new File(dir, fileName);    }

我们发现除了缓存目录cacheDir,还可以通过构造方法设置备用的目录reserveCacheDir,以便在cacheDir不可用时使用。


UnlimitedDiskCache

UnlimitedDiskCache是BaseDiskCache(无抽象方法)的默认实现,没有增加任何处理。


LimitedAgeDiskCache

而LimitedAgeDiskCache则是在BaseDiskCache的基础上,用HashMap缓存了每个文件保存时的时间戳。

/* LimitedAgeDiskCache.java */    public File get(String imageUri) {        File file = super.get(imageUri);        if (file != null && file.exists()) {            boolean cached;            Long loadingDate = loadingDates.get(file);            if (loadingDate == null) {                cached = false;                loadingDate = file.lastModified();            } else {                cached = true;            }            //如果超过了maxFileAge则直接删除掉            if (System.currentTimeMillis() -                loadingDate > maxFileAge) {                file.delete();                loadingDates.remove(file);            } else if (!cached) {                //如果文件存在但是loadingDates中找不到时,将                //此文件的时间戳加到loadingDates中                loadingDates.put(file, loadingDate);            }        }        return file;    }

LruDiskCache

接下来我们主要分析下LruDiskCache,这个类直接实现了DiskCache,并借助于专门封装LRU算法的DiskLruCache类来完成磁盘的实际操作。


DiskLruCache

让我们先来看看LruDiskCache主要用到的DiskLruCache中的接口:

返回值类型 方法 方法说明 DiskLruCache open() 创建DiskLruCache实例 Snapshot get() 获取缓存实体Entry的快照-读 Editor edit() 获取实体Entry的编辑器-写 boolean remove() 移除LRU中的实体Entry-删

实例化 - open()

DiskLruCache的构造方法是private类型的,所以只能通过open()方法获取DiskLruCache的实例,其它的操作都要是这个实例来完成。

参数的含义分别如下:

返回类型 参数 说明 File directory 缓存文件存储路径 int appVersion 应用版本号 int valueCount 每个key对应的value的数量 long maxSize 缓存的最大size int maxFileCount 缓存的最大数量

而LruDiskCache中获取DiskLruCache代码如下:

/* LruDiskCache.java *///appVersion和valueCount为1,maxSize和maxFileCount//外部传入。cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);

读 - get()

根据key查找Entry的快照,并提供了Snapshot#getFile(int index)来获取这个文件和Snapshot#getInputStream(int index)来获取输入流,其中参数index即为open()方法参数valueCount的下标。

LruDiskCache中获取文件代码如下:

/* LruDiskCache.java *///根据key取到实体的快照snapshotsnapshot = cache.get(getKey(imageUri));//通过snapshot的getFile()获取文件return snapshot == null ? null : snapshot.getFile(0);

这里getFile()参数为0,这是因为我们在前面open()方法时定义了valueCount = 1,每个key只有一个value与之对应。


写 - edit()

根据key查找Entry的编辑器,Entry写入相关的操作都是通过Editor来完成的,然后提供了Editor#newInputStream(int index)构建输入流或Editor#newOutputStream(int index)构建输出流。

LruDiskCache中写入磁盘的代码如下:

/* LruDiskCache.java *///根据key去获取编辑器editorDiskLruCache.Editor editor = cache.edit(getKey(imageUri));//通过editor构建输出流,这里生成了dirty文件OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);boolean copied = false;//将输入流imageStream写入dirty(tmp格式)文件copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);

edit()只是获取到一个Editor的实例,最后别忘了调用Editor#commit()提交或调用Editor#abort()丢弃此次编辑。而commit()方法通过将dirty文件renameTo()为clean文件完成文件写入的。

这里说说Editor#newInputStream(int index)和Editor#newOutputStream(int index)这两个方法

前者构建的输入流是read时需要的,实际调用的是Entry#getCleanFile(int i)文件的输入流

/* DiskLruCache$Editor.java */        public InputStream newInputStream(int index) throws IOException {            synchronized (DiskLruCache.this) {                try {                    return new FileInputStream(entry.getCleanFile(index));                } catch (FileNotFoundException e) {                    return null;                }            }        }

这个clean文件是在commit()时,从dirty文件renameTo()来的。

而后者构建的输出流是write时需要的,实际调用的是Entry#getDirtyFile(int i)文件的输出流,这个dirty文件(带tmp后缀)生成是在调用Editor#newOutputStream(int index)时,在LruDiskCache中是在save()时生成:

/* DiskLruCache$Editor.java */    public OutputStream newOutputStream(int index) throws IOException {        synchronized (DiskLruCache.this) {            File dirtyFile = entry.getDirtyFile(index);                FileOutputStream outputStream;            try {                outputStream = new FileOutputStream(dirtyFile);            } catch (FileNotFoundException e) {            }            return new FaultHidingOutputStream(outputStream);            }

删 - remove()

移除LRU缓存中的key对应的实体。这个方法一般不需要手动调用,因为会在调用DiskLruCache#close()、DiskLruCache#flush()或执行cleanupCallable任务(DiskLruCache#remove(String key)、DiskLruCache#get(String key)、DiskLruCache#commit()、DiskLruCache#abort()都有调用到)时都会自动调用trimToSize()和trimToFileCount()分别根据设置的最大size和最大数量来遍历LRU队列lruEntries前面的实体Entry。


对缓存部分的思考

看完了缓存部分的代码,我们可能还会有一些疑惑:

首先,磁盘缓存时文件名的生成是根据Uri还是[Uri][width][height],我们看到在ImageLoader中使用displayImage()加载图片时,直接判断的是MemoryCache,并没有判断DiskCache,而实际使用到DiskCache是在我们取不到MemoryCache时,就会去加载LoadAndDisplayImageTask任务,而这个任务构造方法传入的参数是Uri,所以磁盘加载是根据Uri生成的文件名。

还有,网络上下载的图片文件名可能会有一些非法字符,保存在磁盘的时候可能会异常,这个问题是怎么解决的呢?

/* BaseDiskCache.java */    protected File getFile(String imageUri) {        //根据fileNameGenerator规则生成imageUri文件名        String fileName = fileNameGenerator.generate(imageUri);        File dir = cacheDir;        if (!cacheDir.exists() && !cacheDir.mkdirs()) {            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {                dir = reserveCacheDir;            }        }        return new File(dir, fileName);    }

通过取imageUri的hashcode或md5就能很好的解决这个问题。

最后一个问题,在分析框架的时候我们说通过配置ImageLoaderConfiguration$Builder的denyCacheImageMultipleSizesInMemory()就能来保证我们不同尺寸的图片只会有一份数据,而默认的不同尺寸的会有多份数据,这个是怎么做到的呢?

分析框架时我们就知道,内存中存放的key的格式为[imageUri]_[width]x[height],这里width和height是ImageAware的值,默认情况下,会产生多个尺寸的key。

但当设置了denyCache*()这个方法后,在build()的时候,MemoryCache默认会创建FuzzyKeyMemoryCache实例进行包装:

/* ImageLoaderConfiguration$Builder.java */    if (denyCacheImageMultipleSizesInMemory) {        memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator());    }

第二个参数自定义的Comparator实际就是只对比key的Uri部分,如果两者Uri相同,则认为是同一个key。

这样,在我们put()的时候,如果发现Uri相同的,则删除之前的,在将新的key对应的Bitmap放入,就达到了一个Uri只对应一份数据的目的。

至此,我们已经分析完了ImageLoader中关于框架和缓存的全部。如果对这个项目还有疑问,可以留言讨论。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 买车4s店不给临时车牌怎么办 百度云谣绑定的邮箱被绑定了怎么办 百度网盘分享的视频打不开怎么办 在赶集网登录时忘记验证码该怎么办 支付宝绑定的手机号是空号了怎么办 微信聊天后电话号码重复是怎么办 老婆在外省工作不回到我身边怎么办 知道扣扣号怎样盗取他的密码怎么办 喜欢养猫又怕猫破坏家里怎么办 约她她每次都找借口怎么办? 遇到总是找借口不还钱的人怎么办 装修好了业主找借口不给钱怎么办 荒野行动手机换了帐号登不上怎么办 换新手机后微信头像都没有了怎么办 苹果系统维护换不了微信头像怎么办 系统通知栏不显示qq图标怎么办 快递号码写错了而且发货了怎么办 包裹遗忘在郑州东站安检口了怎么办 锁书包的锁头钥匙全掉了怎么办 平板电脑恢复出厂设置变英语怎么办 给国外银行汇款账号写错账号怎么办 顺丰快递暴力运输产品坏了怎么办 亚航订机票名字少写一个字母怎么办 如果淘宝付款七天内不发货怎么办 浪琴手表调了一下日期不走了怎么办 收件人号码写错快递柜已签收怎么办 医院名字写错了怎么办保险报销 电脑在使用中出现了英文字慕怎么办 下雨天了怎么办我好想你是什么歌 安卓手机不支持微信运动怎么办 装系统时无法跳过密匙怎么办 在msdn里下的系统没有网怎么办 w7电脑更新后系统没法激活怎么办 手机使用加速器后网速变卡怎么办 奥特曼ol分解了迪迦石像怎么办 左右棋牌游戏兑换总说系统护怎么办 四季海棠扦插以后黄叶卷叶怎么办 竹节海棠浇水多了叶子蔫了怎么办 社保停缴了里面的钱怎么办 王者荣耀英雄释放技能有延迟怎么办 买的桑拿木板颜色太深了怎么办