美文网首页
Imageloader源码解析-从头教你如何开始看“硬盘缓存策略

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

作者: 孤独的追寻着 | 来源:发表于2017-09-03 21:37 被阅读0次

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

    这篇文章是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

    相关文章

      网友评论

          本文标题:Imageloader源码解析-从头教你如何开始看“硬盘缓存策略

          本文链接:https://www.haomeiwen.com/subject/rrbqbttx.html