Imageloader源码解析-从头教你如何开始看“硬盘缓存策略”源码

从基本配置中寻找代码的入口:

这篇文章是ImageLoader的硬盘缓存策略完成解析,写起来比较详细,所以字数和图片都比较多,希望大家能认识读完。


Paste_Image.png

一般情况下,我们都要配置上图的Imageloer配置,明显可以看出来,这里用到了“建造者模式”来完成基本数据参数的注入,我们都知道,Imageloader是通过三级访问,来实现对图片文件的展示的,
三级分别是:网络(Net)、硬盘(Disk)、内存(Memory)。这次我写的文章不是总结性的文章,而是先带大家,如何一步一步去看源代码,之后再进行总结。

硬盘缓存策略的配置入口ImageLoaderConfiguration.Builder. diskCache(disck) :

首先我们来了解一下,默认情况下,硬盘缓存会使用哪种策略:

Paste_Image.png

点击 ctrl+鼠标选中 ,能够跳转到 ImageLoaderConfiguration类 这个类的职责是完成ImageLodaer的配置 diskCache(disck) 是其建造类的方法,注入了硬盘缓存算法:

Paste_Image.png

通过注释我们可以看出来,这个函数方法的作用就是设置图片硬盘缓存策略的,默认使用的的硬盘策略是UnlimitedDiskCache这个缓存策略,默认的路径通过StorageUtils.getCacheDirectory(Context)来获取到,我们暂时不考虑他们内部的实现过程,我们只需要知道,默认情况下,我们什么都不设置的时候回使用UnlimitedDiskCache这种缓存策略,和默认的cache路径从StorageUtils.getCacheDirectory(Context)获取。

探寻硬盘缓存策略ImageLoader实现多少种策略:

接下来我们会先考虑一个问题,我们如何知道,Imageloade中到底有多少种缓存策略呢。。根据java建立包的规范,我们能考虑到,硬盘缓存的所有实现方法应该是放在同一目录下的,所以我们先看看UnlimitedDiskCache所在的目录“com.nostra13.universalimageloader.cache.disc.impl”,建包规范或者了熟悉面对对象编程的朋友通过这个目录,可以知道,这个目录下应该就是硬盘缓存的具体实现,(我使用的是Android studio 可以直接打开jar包) 我们直接点开 Universal-ImageLoader.jar包 的com.nostra13.universalimageloader.cache.disc目录 如下:

Paste_Image.png

以下是我看过类后画出的UML类图:

Paste_Image.png

我这里来解释下上面的图DiskCache是一个接口,通过“实现关系”规范BaseDisckCache和LruDiskCache必须实现的方法,BaseDiskCache是一个抽象类,实现的就是最简单的保存网络图片文件到硬盘,获取本地硬盘,并且没有任何限制,LruDiskCache类实现DiskCache,是基于“最近最少使用”算法来实现的,而LimitedAgeDiskCache和UnlimitedDiskCache跟BaseDiskCache有很多共同点,相对之下,LimitedAgeDiskCache只是对时间进行控制,对超时的图片文件进行删除处理,而UnlimitedDiskCache没有任何限制,他们对缓存的大小都没有控制,而LruDiskCache会控制缓存大小和缓存的文件多少,所以他们都继承BaseDiskCache抽象类。

我们来总结一下,Imageloder中的硬盘缓存有三种策略:
LruDiskCache:最近最少使用缓存策略 考虑文件缓存大小和缓存文件多少
LimitedAgeDiskCache:设置文件存活时间,当文件超过这个时间时就删除该文件,不考虑文件缓存大小
UnlimitedDiskCache:没有任何限制的存取策略,不考虑文件缓存大小

上面是硬盘缓存的三种策略,通过分析我们知道,LruDiskCache是里面非常好的策略,所以我们设置的时候尽量设置LruDiskCache,因为默认情况下我们使用的是UnlimitedDiskCache,这样对于用户来说是非常不友好的,因为每个用户的手机配置是不相同的,有些存储比较少的时候,这样就能给用户更加友好的体验,安卓的发展就靠大家了。。。

为什么默认策略是UnlimitedDiskCache:

掌握了大致的方向之后,我们可以思考一个问题:为什么默认的情况下是UnlimitedDiskCache,在代码中,Imageloder框架的如何实现的呢。。。

默认配置,也是属于配置,所以我们看一下ImageLoaderConfiguration类,在Android studio 中进入ImageLoaderConfiguration类 ,键盘设置ctrl+7(这个7是左边键盘的不能按数字键),你会看到:

Paste_Image.png

你能看到有一个Builder类,这是建造者模式的标配,再看下来,我要找的是createDefault(Context context)这个方法函数进入过程如下:

createDefault(Context context)-->.build()-->initEmptyFieldsWithDefaultValues()(这个函数就是初始化默认值和空值)-->

Paste_Image.png

看图:这里是硬盘缓存为空时的核心,显判断diskCacheFileNameGenerator是否为空,不为空就创建默认的,这里的diskCacheFileNameGenerator是硬盘的名字生成器,因为硬盘缓存策略都要命名,所以这里传入的名字生成器不能为null 下来就是重点了,我们继续点.createDiskCache进去,我们可以看到,我们调到这个新的类型里面去,这个类叫DefaultConfigurationFactory,其只要功能就是生成一些默认的配置,我们不管其他,继续看我们之前那方法,createDiskCache()意思就是说创建一个硬盘缓存策略:

Paste_Image.png

看注释,我们就懂:创建一个取决于传入参数的继承于DiskCache的类,这句话怎么理解呢?意思就是说创建一个继承于DiskCache的类,但是具体实现的策略由传入的参数的决定,我们看86-95的判断就知道,影响的参数是diskCacheSize和diskCacheFileCount,如果有其中一个值大于0,我们使用的策略就是LruDiskCache,所以当我们需要使用LruDiskCache硬盘缓存策略的就只需要设置其中一个值为正整数就行了,当我们什么都不设置的时候,默认就会执行96-97行,使用的就是UnlimitedDiskCache。
下面说一下整个createDiskCache思路:
85行是:创建磁盘缓存文件夹,如果主磁盘缓存文件夹不可用,将使用该磁盘缓存文件夹,也就是一个备份区。
86行:对diskCacheSize或者diskCacheFileCount进行判断,如果大于0,执行87行 然后返回一个LruDiskCache实现对象。
87行:获取到一个保存图片的私人文件夹。
89-90行:返回LruDiskCache实现对象。
96-97行:获取获取文件夹路径,并返回没有任何限制的实现硬盘缓存。

探寻硬盘缓存策略ImageLoader三种策略的具体实现:

DiskCache:

之前已经说了三种策略都实现于DiskCache,接下来我们看看其源代码:

 public interface DiskCache {
         /**
        硬盘缓存的接口
       */
/**
    返回硬盘缓存的保存文件夹
 */
File getDirectory();

/**
 * 获取到硬盘缓存的图片文件
 *
 * @param 图片唯一url
 * @return File of cached image or <b>null</b> if image wasn't cached
 */
File get(String imageUri);

/**
 * 在磁盘缓存中保存图像流。
 * 此方法不应关闭传入的图像流
 *
 */
boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

/**
 * 保存磁盘缓存中的图像位图。
 *
 */
boolean save(String imageUri, Bitmap bitmap) throws IOException;

/**
 * 删除输入URI关联的图像文件
 */
boolean remove(String imageUri);

/** Closes disk cache, releases resources. */
void close();

/** Clears disk cache. */
void clear();
  }

这个接口就是规定硬盘缓存策略必须实现的方法,具体方法实现由具体类来实现,在这里使用接口是为了遵循面向对象的六大原则中的开闭原则,和里式替换原则。

BaseDiskCache:

下面,我们来看看BaseDiskCache的抽象类,我们主要看核心的三个方法:
1.boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener):在磁盘缓存中保存图像流
2.boolean save(String imageUri, Bitmap bitmap):保存磁盘缓存中的图像位图
3.File get(String imageUri):获取到硬盘缓存的图片文件
我们一个个方法来看看 BaseDiskCache是如何实现的:

save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener)
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
    //根据url返回一个文件
    File imageFile = getFile(imageUri);
    //生成一个以.tmp结尾的临时文件
    File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
    //声明是否保存成功
    boolean loaded = false;
    try {
        //创建一个缓冲输入流
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
        try {
            //调用IoUtils的.copyStream复制流的函数方法读写到输入流中
            loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
        } finally {
            //关闭临时文件
            IoUtils.closeSilently(os);
        }
    } finally {
        //结束后加载成功并转移到imageFile不成功时 loaded设置为false
        if (loaded && !tmpFile.renameTo(imageFile)) {
            loaded = false;
        }
        //loaded为false,删除临时文件
        if (!loaded) {
            tmpFile.delete();
        }
    }
    return loaded;
}

上面就是直接对图像流进行保存,注释写的很多,我就不一一说了,下面我们看一下getFile(imageUri)的实现:

//通过imgeurl返回一个非空的文件,文件可以引用一个不存在的文件。
protected File getFile(String imageUri) {
    //根据文件名成生成器生成文件名 这个文件名生成器有 两种实现方式,后面我们再说
    String fileName = fileNameGenerator.generate(imageUri);
    File dir = cacheDir;
    //如果满足cacheDir不存在并且!cacheDir.mkdirs()说明磁盘不可操作,我就使用备用的文件夹
    if (!cacheDir.exists() && !cacheDir.mkdirs()) {
        if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
            dir = reserveCacheDir;
        }
    }
    return new File(dir, fileName);
}

这里应该很好理解,就是获取cache文件的根目录,来生成一个以fileName命名的文件。

下面我们来看一下IoUtils这个Io流的工具类吧:

Paste_Image.png

上面就是IoUtIls类的结构,主要的职能是复制图片文件,停止加载文件,当我们调用copyStream方法有三、四个参数两种多态,最终都会调用四个参数的,只是三个参数的不设置一次读取缓存流的大小,默认为DEFAULT_BUFFER_SIZE = 32 * 1024,下来我们来看看四个参数的:

public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
    //根据url返回一个文件
    File imageFile = getFile(imageUri);
    //生成一个以.tmp结尾的临时文件
    File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
    //声明是否保存成功
    boolean loaded = false;
    try {
        //创建一个缓冲输入流
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
        try {
            //调用IoUtils的.copyStream复制流的函数方法读写到输入流中
            loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
        } finally {
            //关闭临时文件
            IoUtils.closeSilently(os);
        }
    } finally {
        //结束后加载成功并转移到imageFile不成功时 loaded设置为false
        if (loaded && !tmpFile.renameTo(imageFile)) {
            loaded = false;
        }
        //loaded为false,删除临时文件
        if (!loaded) {
            tmpFile.delete();
        }
    }
    return loaded;
}

//通过imgeurl返回一个非空的文件,文件可以引用一个不存在的文件。
protected File getFile(String imageUri) {
    //根据文件名成生成器生成文件名 这个文件名生成器有 两种实现方式,后面我们再说
    String fileName = fileNameGenerator.generate(imageUri);
    File dir = cacheDir;
    //如果满足cacheDir不存在并且!cacheDir.mkdirs()说明磁盘不可操作,我就使用备用的文件夹
    if (!cacheDir.exists() && !cacheDir.mkdirs()) {
        if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
            dir = reserveCacheDir;
        }
    }
    return new File(dir, fileName);
}

下面直接看一下IOUtils类:

 /**
 * 复制流、通过监听器发进度监听,监听器可以中断复制过程
 *
 * @param is         输入流
 * @param os         输出流
 * @param listener   复制过程和可以中断复制的监听器
 * @param bufferSize  用于复制的缓冲流大小
 * @return 如果为true 复制完成 如果falae中断复制
 * @throws IOException
 */
public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)
        throws IOException {
    int current = 0;
    //获取输入流的总大小
    int total = is.available();
    //如果为负数 就设置为图片默认的总大小为500 * 1024 即为500kb
    if (total <= 0) {
        total = DEFAULT_IMAGE_TOTAL_SIZE;
    }
    //创建字节数组
    final byte[] bytes = new byte[bufferSize];
    int count;
    //首先判断是否应该停止正在复制的流 返回true 中断复制
    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;
    }
    //刷新输出流
    os.flush();
    return true;
}

//是否应该停止正在复制的任务
private static boolean shouldStopLoading(CopyListener listener, int current, int total) {
    //如果中断监听器为空直接返回false不中断
    if (listener != null) {
        //是否中断看客户端具体实现监听方法
        boolean shouldContinue = listener.onBytesCopied(current, total);
        //如果判断为中断后 ,还需要判断 是否加载少于75% 我们才会返回true中断
        if (!shouldContinue) {
            if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
                return true; // if loaded more than 75% then continue loading anyway
            }
        }
    }
    return false;
}

//关闭流
public static void closeSilently(Closeable closeable) {
    if (closeable != null) {
        try {
            closeable.close();
        } catch (Exception ignored) {
        }
    }
}
    
//读取 并关闭流
public static void readAndCloseStream(InputStream is) {
    final byte[] bytes = new byte[DEFAULT_BUFFER_SIZE];
    try {
        while (is.read(bytes, 0, DEFAULT_BUFFER_SIZE) != -1);
    } catch (IOException ignored) {
    } finally {
        closeSilently(is);
    }
}
public boolean save(String imageUri, Bitmap bitmap)

下面我们讲一下保存磁盘缓存中的图像位图怎么处理的:

   public boolean save(String imageUri, Bitmap bitmap) throws IOException {
    File imageFile = getFile(imageUri);
    File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
    OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
    //是否保存成功
    boolean savedSuccessfully = false;
    try {
        //直接调用Bitmap的复制方法
        savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
    } finally {
        IoUtils.closeSilently(os);
        //如果成功 但是没有转移成功 判断为保存失败
        if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
            savedSuccessfully = false;
        }
        //保存失败删除临时文件
        if (!savedSuccessfully) {
            tmpFile.delete();
        }
    }
    //释放Bitmap 因为btmap是非常耗费内存的
    bitmap.recycle();
    return savedSuccessfully;
}

比之前的简单,获取图片文件更加简单,我这里就不写了。

继承于BaseDiskCache的LimitedAgeDiskCache和UnlimitedDiskCache和其父类有何不同呢?

我们先说UnlimitedDiskCache,这个方法是没有任何限制,所以他直接继承了BaseDiskCache然后啥米事情都没干。

那么LimitedAgeDiskCache是怎么限制时间的呢?

我们看到源码中扩展了两个变量:
private final long maxFileAge;
private final Map<File, Long> loadingDates = Collections.synchronizedMap(new HashMap<File, Long>());
我们暂时不考虑这两个变量有啥用,我们关注重点的那两个方法,先看save():

Paste_Image.png

我们能看到,保存文件的方法,还是使用父类的,但是这里他扩展了一个方法,rememberUsage(imageUri),这也是常用的扩展方法的方法,rememberUsage(imageUri)具体实现为:

    private void rememberUsage(String imageUri) {
    File file = getFile(imageUri);
    long currentTime = System.currentTimeMillis();
    file.setLastModified(currentTime);
    loadingDates.put(file, currentTime);
}

这个方法就是实现,获取文件,然后设置当前时间为最后修改的时间,存储到强引用loadingDates变量中,现在我们大概能猜出,loadingDates的作用了吧,就是为了以文件为key,更改时间为values来记录,文件的保存时间。
下面我们看看:public File get(String imageUri)

    public File get(String imageUri) {
    //从父类获取文件 一模一样
    File file = super.get(imageUri);
    //如果file不为空,并且存在
    if (file != null && file.exists()) {
        //是否缓存有时间保存缓存
        boolean cached;
        //直接从loadingDates获取
        Long loadingDate = loadingDates.get(file);
        //没有缓存记录存在
        if (loadingDate == null) {
            //设置cached为false 说明之前没有访问过
            cached = false;
            //loadingDate从文件中获取最后更改时间
            loadingDate = file.lastModified();
        } else {
            //有访问过缓存
            cached = true;
        }
        //判断当前时间是否过时
        if (System.currentTimeMillis() - loadingDate > maxFileAge) {
            //过时就删除文件
            file.delete();
            //删除缓存记录
            loadingDates.remove(file);
        } else if (!cached) {
            //没有过时 又没有本地缓存有访问记录 就添加
            loadingDates.put(file, loadingDate);
        }
    }
    return file;
}

上面不管是save还有get方法,我们发现都是采用新生成一个子类,重写部分方法来实现功能的扩展。当然我们也可以通过装饰者模式来实现功能的拓展。这是我发现的两张扩展比较好的方法。

DisKLrucache作为一个重点,另外篇幅讲解:http://www.jianshu.com/p/d03f10b18dff

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容