从设计到实现,一步步教你实现Android-Universal-ImageLoader-缓存

转载请标明出处,本文出自:chaossss的博客


Android-Universal-ImageLoader Github 地址


Cache

我们要对图片进行缓存,有两种方式:内存缓存和本地缓存。这两种方式的区别在于,内存缓存是缓存在 Android 系统为应用分配的运行内存之中,读取速度快,但是可能会带来 OOM 的问题;本地缓存一般缓存在 SD 卡中,读取速度较慢,但是缓存空间足。

那么我们要怎么来实现内存缓存和本地缓存呢?根据单一职责原则,如果 MemoryCache 和 DiskCache 的抽象不一致的话,我们就需要分别创建 MemoryCache 和 DiskCache 的抽象基类,分别实现各自的细节。而显然,两者抽象是不一致的,因为 MemoryCache 面对的对象是图片(Bitmap),DiskCache 面对的对象是文件(File)。因此,我们应该分开实现 MemoryCache 和 DiskCache。

MemoryCache

MemoryCache 整体设计及相关基类的实现

那我们现在该干啥呢?想 MemoryCache 的功能啊!对于进行内存缓存我们能想到什么应用场景呢:

  1. 首先每一个 MemoryCache 肯定有相应的增删取功能
  2. 当 MemoryCache 被存满了(Android 应用的内存资源是很宝贵的),我们该怎么处理呢:
    • 回收最近没有用过的
    • 回收最早在内存中缓存的
    • 回收使用频率最低的
    • 在缓存时,key 相同的图片,回收旧的图片,缓存新的图
  3. 有时候我们可能需要缓存高分辨率的高清图片,而这种图片非常大,缓存到内存中就会 OOM,在这种情况下,为了让应用能正常运行,我们应该能在缓存时限制图片的大小

我就想到这么多哈,肯定还会有很多不一样的情况,毕竟需求是层出不穷的……那么根据现在得到的应用场景,我们就要开始设计 MemoryCache 啦。根据分析得到的结果我们可以发现:内存缓存有不一样的缓存策略和缓存限制,但是具有相同的抽象。所以我们首先需要实现 MemoryCache 的抽象:

public interface MemoryCache {

    boolean put(String key, Bitmap value);

    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}

实现了抽象,就得开始考虑具体实现拉。我们刚刚也说了,MemoryCache 具有不同的缓存策略和缓存限制,策略不一样的实现类一般不会存在继承关系,而具有相同限制的 MemoryCache 则可能存在抽象。那么我们可以得到:

public abstract class BaseMemoryCache implements MemoryCache
public abstract class FuzzyKeyMemoryCache implements MemoryCache
public abstract class LruMemoryCache implements MemoryCache

很多人会奇怪了,为什么 LruMemoryCache 和 FuzzyKeyMemoryCache 不是继承于 BaseMemoryCache 呢?我们不妨想象为什么需要 BaseMemoryCache,我们之所以引入 BaseMemoryCache,是因为限制不同的 MemoryCache 具有相同的抽象,在 AUImgLoader 中,不同的限制体现在:缓存图片的大小限制和缓存图片的引用方式限制。

有关引用的知识可以看这Java中的强引用、软引用、弱引用和虚引用

所以,在 BaseMemoryCache 的实现中,我们添加了对应的抽象方法:

protected abstract Reference<Bitmap> createReference(Bitmap value);

反观 LruMemoryCache,我在深入源码剖析LruCache中给大家讲解过 LruCache 的原理,我们在 LruMemoryCache 中进行缓存,是只使用强引用进行缓存的,换言之,LruMemoryCache 的抽象和 BaseMemory 的实现是不一样的,因为 BaseMemoryCache 中多了一个 createReference() 抽象方法,而 LruMemoryCache 不需要这个抽象方法。

而 FuzzyKeyMemoryCache 是一个只考虑缓存策略的内存缓存类,它只考虑怎么去处理 key 相同的图片,具体你用什么方式实现,就由开发者自己决定,因为 FuzzyKeyMemory 的处理方法都是调用抽象接口完成的(源码我就不放了哈,大家可以自己下载)

BaseMemoryCache

经过刚刚的分析,我们得到了 MemoryCache 的三个基类:BaseMemoryCache、LruMemoryCache、FuzzyKeyMemoryCache。接下来,我们就来根据 BaseMemoryCache 实现我们想要的细节。

我们已经提到,BaseMemoryCache 是用于处理缓存限制的基类,而所谓的限制体现在:限制大小和限制引用方式。那么很显然,引用方式只有强引用、弱引用值得我们进行区分(Android 2.3 已经不鼓励开发者使用软引用了,因为在进行垃圾回收时软引用和弱引用都具有被回收的倾向),所以我们可以得到下面两个实现类:

public abstract class LimitedMemoryCache extends BaseMemoryCache
public class WeakMemoryCache extends BaseMemoryCache

千呼万唤始出来,我们最终的细节实现类 LimitedMemoryCache 终于出现了……在 LimitedMemoryCache 中,我们将使用强引用缓存图片,那么,LimitedMemoryCache 还需要提供的就是图片大小的限制:

public LimitedMemoryCache(int sizeLimit) {
        this.sizeLimit = sizeLimit;
        cacheSize = new AtomicInteger();
        if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
            L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB);
        }
    }

剩下的,就是根据我们的缓存策略对 LimitedMemoryCache 进行拓展啦。

MemoryCahce 的工具类

那么经过刚刚的努力,我们现在已经能过在内存中进行缓存,即使 AUImgLoader 自带的缓存实现类不能满足我们的需求,由于 AUImgLoader 的内存缓存功能模块是通过装饰者模式架构的,我们要对已有的类进行拓展也是很简单的。但是现在内存缓存模块只是提供了必要的“内存缓存功能”,我们还需不需要其他的工具来简化我们的使用呢?

大家不妨想想,我们加载了一些尺寸较小的图片在内存中,它们占用的总内存并不大,还不需要将它们缓存到本地,但我们仍需要对它们进行细粒度的管理(否则当图片数量增多,内存空间不够时我们对图片的处理会带来各种问题)。那么我们就需要一个工具类协助我们进行内存缓存的管理,不妨创建一个叫做 MemoryCacheUtils 的类:

public final class MemoryCacheUtils {

    private MemoryCacheUtils() {
    }
}

大家会注意到这个类将是一个无法被继承的类,也无法通过构造方法获得实例对象。毕竟一方面,这个工具类不需要重复创建实例对象,只需要调用类去执行方法就行了;另一方面,工具类并不需要继承,需要什么添加进去就行了。

这里说 MemoryCacheUtils 无法创建实例对象是指一般情况下无法创建,实际上如果使用 Java 的反射机制的话还是可以创建对象的。

那么这个类要帮我们完成什么工作呢?分析实际的使用场景,我们可以得到:

  1. 为即将加入内存缓存的图片生成相应的
  2. 使用 Comparator 判断键是否相等时,可能出现不同的 Uri 生成的键相同的情况,此类需要提供方法解决这个问题
  3. 当你输入一个 Uri 时可能会在内存缓存中找到多个响应图片,所以方法类应返回响应图片列表
  4. 当你输入一个 Uri 时可能会在内存缓存中找到多个已缓存的键,所以方法类应返回键列表
  5. 将对应输入 Uri 的所有缓存图片从内存中移除
public static String generateKey(String imageUri, ImageSize targetSize){}

public static Comparator<String> createFuzzyKeyComparator(){}

public static List<Bitmap> findCachedBitmapsForImageUri(String imageUri, MemoryCache memoryCache){}

public static List<String> findCacheKeysForImageUri(String imageUri, MemoryCache memoryCache){}

public static void removeFromCache(String imageUri, MemoryCache memoryCache){}

具体实现我就不在这多说拉,大家可以自行查看源码进行阅读,里面的逻辑并不难。

MemoryCache 结构图

经过上面的设计和代码实现,我们在 AUImgLoader 中得到下面这张内存缓存功能模块的整体结构图:

做图的时候 BaseMemoryCache 的 createReference() 方法忘了加进去了,大家见谅哈……

DiskCache

DiskCache 整体设计及相关基类的实现

实际上 DiskCache 的设计思想和 MemoryCache 是非常接近的,我们只要遵循刚刚的思路就可以拉。那么首先需要实现 DiskCache 的抽象:

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();
}

然后获得两个基类:

public class LruDiskCache implements DiskCache
public abstract class BaseDiskCache implements DiskCache

同样的,我们根据缓存的限制实现 BaseDiskCache 的细节,就可以完成本地缓存功能模块拉

DiskCache 的辅助工具类

虽然 DiskCache 整体设计和实现都挺简单的,但是大家还需要考虑到一个问题,就是我们每一张图片缓存到本地之后都需要为其命名,那么我们就应该为其实现相关的命名类咯。能够区分每一张图片的命名方式无非就是:哈希和MD5,引用网上一张图,我们应该设计一个文件命名功能模块,提供给 DiskCache 使用:

那么首先实现命名类的抽象:

public interface FileNameGenerator {
    String generate(String imageUri);
}

然后分别实现细节就可以拉:

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

    private static final String HASH_ALGORITHM = "MD5";
    private static final int RADIX = 10 + 26; // 10 digits + 26 letters

    @Override
    public String generate(String imageUri) {
        byte[] md5 = getMD5(imageUri.getBytes());
        BigInteger bi = new BigInteger(md5).abs();
        return bi.toString(RADIX);
    }

    private byte[] getMD5(byte[] data) {
        byte[] hash = null;
        try {
            MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
            digest.update(data);
            hash = digest.digest();
        } catch (NoSuchAlgorithmException e) {
            L.e(e);
        }
        return hash;
    }
}

DiskCache 的工具类

DiskCache 除了在进行本地缓存时需要工具类帮助其生成文件名,还需要工具类帮它完成其他工作:本地缓存管理、本地存储策略等……于是我们需要引入多个工具类协助完成这些职责。

大家需要注意到的是,工具类和辅助工具类将处于不同的包中,因为辅助工具类是完成职责必不可少的一环。而工具类是减少使用者的使用成本,两者间的差别使得类所在的包不一致。

DiskCacheUtils

和 MemoryCacheUtils 一样,DiskCacheUtils 也是一个无法被继承,无法创建实例对象的类。

public final class DiskCacheUtils {

    private DiskCacheUtils() {
    }
}

这个工具类的主要使用场景为:

  1. 找到 Uri 对应的本地缓存文件
  2. 移除 Uri 对应的本地缓存文件

所以我们分别实现对应的方法就可以拉:

public static File findInCache(String imageUri, DiskCache diskCache){}

public static boolean removeFromCache(String imageUri, DiskCache diskCache){}

StorageUtils

为了将图片存储到本地 SD 卡中,我们需要获得本地缓存对应的目录,于是引入了 StorageUtils 负责完成相关的事项。由于工具类都是无法继承和创建对象的类,我就不再放出响应的构造方法和类声明了。在 StorageUtils 中,我们可能存在的使用场景有:

  1. 获得本地缓存目录
  2. 新建额外的本地缓存目录
  3. 为某些图片提供对应的特定缓存目录
  4. 为单个图片提供缓存目录

实现各自对应的方法,就OK拉。

DiskCache 结构图

经过上面的设计和代码实现,我们在 AUImgLoader 中得到下面这张内存缓存功能模块的整体结构图:

反思

大家会发现,无论是 DiskCache 还是 MemoryCache,还是它们的工具类,代码结构清晰,类间耦合度低,许多开发者看到这样的代码都会感叹:这代码写得真漂亮。但我相信观察力敏锐的同学会一眼看出,MemoryCache 和 DiskCache 都使用了装饰者模式进行设计,功能的拓展只要针对对应的抽象进行“装饰”就可以了;工具类则严格遵循设计模式中的设计原则,尽可能独立不同的工具模块。所以大家在开发的时候也应该注意这些开发细节,不断重构代码,让代码结构变得清晰。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-06 13:16:47

从设计到实现,一步步教你实现Android-Universal-ImageLoader-缓存的相关文章

手把手教你构建 Android WebView 的缓存机制 &amp; 资源预加载方案

前言 由于H5具备 开发周期短.灵活性好 的特点,所以现在 Android App大多嵌入了 Android Webview 组件进行 Hybrid 开发 但我知道你一定在烦恼 Android Webview 的性能问题,特别突出的是:加载速度慢 & 消耗流量 今天,我将针对 Android Webview 的性能问题,提出一些有效解决方案. 目录 1. Android WebView 存在什么性能问题? Android WebView 里 H5 页面加载速度慢 耗费流量 下面会详细介绍. 1.

一步步教你js原生瀑布流效果实现

一步步教你js原生瀑布流效果实现 什么是瀑布流效果 首先,让我们先看一段动画: 在动画中,我们不难发现,这个动画有以下特点: 1.所有的图片的宽度都是一样的 2.所有的图片的高度是不一样的 3.图片一张挨着一张竖直排列 4.鼠标向下滚动,一直不停的加载图片 5.浏览器的宽度改变,图片的列数会进行相应的更改 那么这种效果类似现实生活中的瀑布,所以我们叫它瀑布流的效果. Js原生瀑布流效果的实现 从上述分析中,我们可以把整个效果分为以下四个部分: html+css界面搭建 瀑布流效果 浏览器向下滚动

【java项目实战】一步步教你使用MyEclipse搭建java Web项目开发环境(一)

首先,在开始搭建MyEclipse的开发环境之前,还有三步工具的安装需要完成,只要在安装配置成功之后才可以进入下面的java Web项目开发环境的搭建. 1.安装工具 第一步,下载并安装JDK,到官网上下载安装即可,之后需要细心的配置环境变量,我给大家推荐百度文库的一篇文章,猛戳这里. 第二步,下载Tomcat,当然可以去Apache Tomcat的官网,同样,您可以移驾到我的资源下载,外送API文档(免资源分). 第三步,下载MyEclipse,MyEclipse官网,傻瓜式安装即可. ===

一步一步教你在 Android 里创建自己的账号系统(二)--同步数据以及设计账号页面

大家如果喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢 转载请标明出处(http://blog.csdn.net/kifile),再次感谢 在前一篇文章中(一步一步教你在 Android 里创建自己的账号系统(一)),我向大家介绍了如何在 Android 系统中创建自己的账户系统,接下来我会向大家详细介绍一下如何使用账户系统. (一)同步数据 通常而言,我们会在两种情况下使用我们的账号系统: (1)登陆验证 登陆验证其实是一个很实用的功能,试

教你写Android ImageLoader框架之图片加载与加载策略

在教你写Android ImageLoader框架之初始配置与请求调度中,我们已经讲述了ImageLoader的请求配置与调度相关的设计与实现.今天我们就来深入了解图片的具体加载过程以及加载的策略(包括按顺序加载和逆序加载) ,在这其中我会分享我的一些设计决策,也欢迎大家给我提建议. 图片的加载 Loader与LoaderManager的实现 在上一篇文章教你写Android ImageLoader框架之初始配置与请求调度中,我们聊到了Loader与LoaderManager. ImageLoa

教你写Android网络框架之Http请求的分发与执行

前言 在<教你写Android网络框架>专栏的前两篇博客中,我们已经介绍了SimpleNet框架的基本结构,以及Request.Response.请求队列的实现,以及为什么要这么设计,这么设计的考虑是什么.前两篇博客中已经介绍了各个角色,今天我们就来剖析另外几个特别重要的角色,即NetworkExecutor.HttpStack以及ResponseDelivery,它们分别对应的功能是网络请求线程.Http执行器.Response分发,这三者是执行http请求和处理Response的核心. 我

教你写Android网络框架之Request、Response类与请求队列

转载请注明出处,本文来自[ Mr.Simple的博客 ]. 我正在参加博客之星,点击这里投我一票吧,谢谢~ 前言 在教你写Android网络框架之基本架构一文中我们已经介绍了SimpleNet网络框架的基本结构,今天我们就开始从代码的角度来开始切入该网络框架的实现,在剖析的同时我们会分析设计思路,以及为什么要这样做,这样做的好处是什么.这样我们不仅学到了如何实现网络框架,也会学到设计一个通用的框架应该有哪些考虑,这就扩展到框架设计的范畴,通过这个简单的实例希望能给新人一些帮助.当然这只是一家之言

从设计到实现,一步步教你实现Android-Universal-ImageLoader-辅助类

通过前面几篇博文.我们分析了 AUI 的缓存.工具类.显示与载入这几个方面的代码.今天呢,我们继续研究 AUI 的源代码,学习当中的核心辅助工具类. 希望大家能在里面学到东西哈. Download 要下载一张图片,我们想象须要什么哈:首先我们得设定支持的协议.得有下载的链接.由对应的下载链接去下载图片.进而得到图片的输入流,剩下的就交给图片的显示/载入类处理啦. 那么我们能够设计出以下的接口: public interface ImageDownloader { InputStream getS

从设计到实现,一步步教你实现Android-Universal-ImageLoader-工具类

转载请标明出处,本文出自:chaossss的博客 在上一篇博文中我们分析了 AUImgLoader 缓存模块的功能实现和架构设计,今天不妨着手分析 AUImgLoader 的工具类,为后面的分析作铺垫. 在 Utils 包中,有 AUImgLoader 可能用到的工具类.其中 DiskCacheUtils.StorageUtils.MemonryCacheUtils 我们在分析 AUImgLoader 缓存功能模块时已经讲解过了,今天就不再赘述.事实上,AUImgLoader 中作为工具被实现的