由LruCache和DiskLruCache提供三级缓存支持的ImageLoader

从三天前一直报错到今天中午,总算出了个能用的版本了。

一如既往先发链接:

https://github.com/mlxy/ImageLoader

缓存处理

·LruCacheHelper:

封装第一级缓存,也就是内存缓存的处理。

LruCache是Android自带的缓存处理类,如名字所说,和使用软引用的映射相比,优势在于可以忽略缓存上限处理的细节问题,初始化时在构造函数中给一个缓存上限即可。一般做法是使用最大内存的八分之一:

Runtime.getRuntime().maxMemory() / 8

但是我觉得八分之一实在太少,所以干脆给了三分之一。

另外在初始化时需要重写LruCache类的sizeOf方法来自行计算图片的大小并返回,默认情况返回的是图片数量。

封装类给出四个接口,分别是打开和关闭,保存和读取。

没什么好说的,直接放代码。

 1 public class LruCacheHelper {
 2     private LruCacheHelper() {}
 3
 4     private static LruCache<String, Bitmap> mCache;
 5
 6     /** 初始化LruCache。 */
 7     public static void openCache(int maxSize) {
 8         mCache = new LruCache<String, Bitmap>((int) maxSize) {
 9             @Override
10             protected int sizeOf(String key, Bitmap value) {
11                 return value.getRowBytes() * value.getHeight();
12             }
13         };
14     }
15
16     /** 把图片写入缓存。 */
17     public static void dump(String key, Bitmap value) {
18         mCache.put(key, value);
19     }
20
21     /** 从缓存中读取图片数据。 */
22     public static Bitmap load(String key) {
23         return mCache.get(key);
24     }
25
26     public static void closeCache() {
27         // 暂时没事干。
28     }
29 }

LruCacheHelper

·DiskLruCacheHelper:

DiskLruCache工具的使用以及这个类的基本介绍可以参考我前两天写的基于Demo解析缓存工具DiskLruCache

为了适应这个工程的需要对这个封装类做了一点变动,直接保存和读取Bitmap。

依然没什么好说的,直接看代码。

 1 public class DiskLruCacheHelper {
 2     private DiskLruCacheHelper() {}
 3
 4     private static DiskLruCache mCache;
 5
 6     /** 打开DiskLruCache。 */
 7     public static void openCache(Context context, int appVersion, int maxSize) {
 8         try {
 9             if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
10                     || !Environment.isExternalStorageRemovable()) {
11                 mCache = DiskLruCache.open(context.getExternalCacheDir(), appVersion, 1, maxSize);
12             } else {
13                 mCache = DiskLruCache.open(context.getCacheDir(), appVersion, 1, maxSize);
14             }
15         } catch (IOException e) { e.printStackTrace(); }
16     }
17
18     /** 写出缓存。 */
19     public static void dump(Bitmap bitmap, String keyCache) throws IOException {
20         if (mCache == null) throw new IllegalStateException("Must call openCache() first!");
21
22         DiskLruCache.Editor editor = mCache.edit(Digester.hashUp(keyCache));
23
24         if (editor != null) {
25             OutputStream outputStream = editor.newOutputStream(0);
26             boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
27
28             if (success) {
29                 editor.commit();
30             } else {
31                 editor.abort();
32             }
33         }
34     }
35
36     /** 读取缓存。 */
37     public static Bitmap load(String keyCache) throws IOException {
38         if (mCache == null) throw new IllegalStateException("Must call openCache() first!");
39
40         DiskLruCache.Snapshot snapshot = mCache.get(Digester.hashUp(keyCache));
41
42         if (snapshot != null) {
43             InputStream inputStream = snapshot.getInputStream(0);
44             Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
45             return bitmap;
46         }
47
48         return null;
49     }
50
51     /** 检查缓存是否存在。 */
52     public static boolean hasCache(String keyCache) {
53         try {
54             return mCache.get(Digester.hashUp(keyCache)) != null;
55         } catch (IOException e) {
56             e.printStackTrace();
57         }
58
59         return false;
60     }
61
62     /** 同步日志。 */
63     public static void syncLog() {
64         try {
65             mCache.flush();
66         } catch (IOException e) { e.printStackTrace(); }
67     }
68
69     /** 关闭DiskLruCache。 */
70     public static void closeCache() {
71         syncLog();
72     }
73 }

DiskLruCacheHelper

ImageLoader主类

从接口说起,类依然是四个接口,初始化,关闭,载入图片,取消载入。

载入分三步,逐级访问三级缓存。

·一:

首先使用内存缓存的封装类调取内存缓存,如果内存中有,就直接显示。

/** 从内存缓存中加载图片。 */
private boolean loadImageFromMemory(View parent, String url) {
    Bitmap bitmap = LruCacheHelper.load(url);
    if (bitmap != null) {
        setImage(parent, bitmap, url);
        return true;
    }

    return false;
}

返回一个标志位用以判断是否已经加载成功。

如果没成功就要访问第二级缓存也即磁盘缓存了,使用封装类检查缓存存在与否,之后分成两个分支。

·二:

如果磁盘缓存已经存在了,就启动读取磁盘缓存的任务。

启动时记得把任务加入一个HashMap中,用于在被外部中断或程序执行结束时取消任务。

任务使用Android自带的AsyncTask异步任务类。

编写一个类,继承AsyncTask并指定泛型。

class LoadImageDiskCacheTask extends AsyncTask<String, Void, Bitmap>

泛型第一位是启动任务时传入的参数类型,我们这里要传入的是图片的URL,所以用String。

这个参数在用AsyncTask.execute(Params...)启动任务时传入,在继承AsyncTask类必须重写的抽象方法doInBackground(Params...)中接收。

第二位是进度的类型。在任务执行的过程中,可以调用publishProgress(Progress)方法不断更新任务进度,比如已下载的文件大小或者已经删除的文件数量之类。

之后重写onProgressUpdate(Progress)方法,在进度更新时做出相应处理,比如修改进度条的值。

在这里我们不需要进度的处理,所以直接给Void,注意V大写。

泛型第三位就是任务结束后返回的结果的类型了。

重写onPostExecute(Result)方法,参数就是doInBackground方法返回的结果。在这里接收图片并显示就可以了。

注意,doInBackground方法是在新线程中执行,而onPostExecute是在主线程中执行的,这也是这个类高明的地方之一,使用AsyncTask类从头至尾都不需要手动处理线程问题,只需要关注业务逻辑。

之后可能研究一下这个类再单独写一篇博文。

在磁盘缓存读取成功之后我们也在内存缓存中保存一份。

·三:

如果没有磁盘缓存,比如第一次打开应用程序的时候,就需要从网络上重新下载图片了。

依旧是继承AsyncTask类,在doInBackground方法中联网下载图片,下载成功后分别保存到磁盘缓存和内存缓存,之后再onPostExecute方法中显示图片。

逻辑和第二步是一样的。

·显示图片:

但是。

如果就这么不管不顾地开始用,比如用在一个纯图片的ListView中,就会发现在滑动ListView的时候有时图片会显示不出来,有时还会不停闪烁。

问题就出在多线程上。

如果使用Google官方推荐的ListView优化方式,也就是在列表适配器中的getView方法里复用convertView

if (convertView == null) {
    imageView = (ImageView) View.inflate(MainActivity.this, R.layout.image_view, null);

    convertView = imageView;
} else {
    imageView = (ImageView) convertView;
}

的话,由于读取图片需要一定的时间,当图片读取完毕时,传给ImageLoader的那个ImageView可能已经不是当初的那个ImageView了。

我在解决这个问题时,发现网上多数的建议是给ImageView绑定URL作为Tag,然后在显示图片时检查Tag和URL是否一致,不一致就不显示。

但是不显示明显不行啊。

我的解决办法是改变思路。

在调用ImageLoader.load时传入的不是符合直觉的ImageView和URL,而是getView的第三个参数,ImageView的父视图parent和URL,到了显示图片的时候再在主线程中用View.findViewWithTag方法来现场获取ImageView并设置图片。

这样就成功地避免了图片的显示错位。

·OutOfMemory异常:

其实这个异常在正常情况下不是很容易出现了,这里只提供一个思路。

给ListView绑定RecyclerListener,实现onMovedToScrapHeap(View)方法,这个方法在列表项移出屏幕外时会被调用,我们在这个方法中取消图片的加载任务,始终保持只加载屏幕内的图片,基本就不会出现内存不够用的情况了。

当然,如果图片实在太大,那就要在解析Bitmap的时候配合Options来自行缩放图片大小,那就是另一回事了。

最后还是代码说话:

  1 public class ImageLoader {
  2     private static final int MEMORY_CACHE_SIZE_LIMIT =
  3             (int) (Runtime.getRuntime().maxMemory() / 3);
  4     private static final int LOCAL_CACHE_SIZE_LIMIT =
  5             100 * 1024 * 1024;
  6
  7     private static final int NETWORK_TIMEOUT = 5000;
  8
  9     private HashMap<String, AsyncTask> taskMap = new HashMap<>();
 10
 11     public ImageLoader(Context context) {
 12         initMemoryCache();
 13         initDiskCache(context);
 14     }
 15
 16     /** 初始化内存缓存器。 */
 17     private void initMemoryCache() {
 18         LruCacheHelper.openCache(MEMORY_CACHE_SIZE_LIMIT);
 19     }
 20
 21     /** 初始化磁盘缓存器。 */
 22     private void initDiskCache(Context context) {
 23         int appVersion = 1;
 24         try {
 25             appVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
 26         } catch (PackageManager.NameNotFoundException e) {
 27             e.printStackTrace();
 28         }
 29
 30         DiskLruCacheHelper.openCache(context, appVersion, LOCAL_CACHE_SIZE_LIMIT);
 31     }
 32
 33     /** 载入图片。
 34      *  @param parent 要显示图片的视图的父视图。
 35      *  @param url 要显示的图片的URL。
 36      * */
 37     public void load(View parent, String url) {
 38         // 尝试从内存缓存载入图片。
 39         boolean succeeded = loadImageFromMemory(parent, url);
 40         if (succeeded) return;
 41
 42         boolean hasCache = DiskLruCacheHelper.hasCache(url);
 43         if (hasCache) {
 44             // 有磁盘缓存。
 45             loadImageFromDisk(parent, url);
 46         } else {
 47             // 联网下载。
 48             loadFromInternet(parent, url);
 49         }
 50     }
 51
 52     /** 取消任务。 */
 53     public void cancel(String tag) {
 54         AsyncTask removedTask = taskMap.remove(tag);
 55         if (removedTask != null) {
 56             removedTask.cancel(false);
 57         }
 58     }
 59
 60     /** 从内存缓存中加载图片。 */
 61     private boolean loadImageFromMemory(View parent, String url) {
 62         Bitmap bitmap = LruCacheHelper.load(url);
 63         if (bitmap != null) {
 64             setImage(parent, bitmap, url);
 65             return true;
 66         }
 67
 68         return false;
 69     }
 70
 71     /** 从磁盘缓存中加载图片。 */
 72     private void loadImageFromDisk(View parent, String url) {
 73         LoadImageDiskCacheTask task = new LoadImageDiskCacheTask(parent);
 74         taskMap.put(url, task);
 75         task.execute(url);
 76     }
 77
 78     /** 从网络上下载图片。 */
 79     private void loadFromInternet(View parent, String url) {
 80         DownloadImageTask task = new DownloadImageTask(parent);
 81         taskMap.put(url, task);
 82         task.execute(url);
 83     }
 84
 85     /** 把图片保存到内存缓存。 */
 86     private void putImageIntoMemoryCache(String url, Bitmap bitmap) {
 87         LruCacheHelper.dump(url, bitmap);
 88     }
 89
 90     /** 把图片保存到磁盘缓存。 */
 91     private void putImageIntoDiskCache(String url, Bitmap bitmap) throws IOException {
 92         DiskLruCacheHelper.dump(bitmap, url);
 93     }
 94
 95     /** 重新设置图片。 */
 96     private void setImage(final View parent, final Bitmap bitmap, final String url) {
 97         parent.post(new Runnable() {
 98             @Override
 99             public void run() {
100                 ImageView imageView = findImageViewWithTag(parent, url);
101                 if (imageView != null) {
102                     imageView.setImageBitmap(bitmap);
103                 }
104             }
105         });
106     }
107
108     /** 根据Tag找到指定的ImageView。 */
109     private ImageView findImageViewWithTag(View parent, String tag) {
110         View view = parent.findViewWithTag(tag);
111         if (view != null) {
112             return (ImageView) view;
113         }
114
115         return null;
116     }
117
118     /** 读取图片磁盘缓存的任务。 */
119     class LoadImageDiskCacheTask extends AsyncTask<String, Void, Bitmap> {
120         private final View parent;
121         private String url;
122
123         public LoadImageDiskCacheTask(View parent) {
124             this.parent = parent;
125         }
126
127         @Override
128         protected Bitmap doInBackground(String... params) {
129             Bitmap bitmap = null;
130
131             url = params[0];
132             try {
133                 bitmap = DiskLruCacheHelper.load(url);
134
135                 if (bitmap != null && !isCancelled()) {
136                     // 读取完成后保存到内存缓存。
137                     putImageIntoMemoryCache(url, bitmap);
138                 }
139             } catch (IOException e) {
140                 e.printStackTrace();
141             }
142
143             return bitmap;
144         }
145
146         @Override
147         protected void onPostExecute(Bitmap bitmap) {
148             // 显示图片。
149             if (bitmap != null) setImage(parent, bitmap, url);
150             // 移除任务。
151             if (taskMap.containsKey(url)) taskMap.remove(url);
152         }
153     }
154
155     /** 下载图片的任务。 */
156     class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
157         private final View parent;
158         private String url;
159
160         public DownloadImageTask(View parent) {
161             this.parent = parent;
162         }
163
164         @Override
165         protected Bitmap doInBackground(String... params) {
166             Bitmap bitmap = null;
167
168             url = params[0];
169             try {
170                 // 下载并解析图片。
171                 InputStream inputStream = NetworkAdministrator.openUrlInputStream(url, NETWORK_TIMEOUT);
172                 bitmap = BitmapFactory.decodeStream(inputStream);
173
174                 if (bitmap != null && !isCancelled()) {
175                     // 保存到缓存。
176                     putImageIntoMemoryCache(url, bitmap);
177                     putImageIntoDiskCache(url, bitmap);
178                 }
179             } catch (IOException e) {
180                 e.printStackTrace();
181             }
182
183             return bitmap;
184         }
185
186         @Override
187         protected void onPostExecute(Bitmap bitmap) {
188             // 显示图片。
189             if (bitmap != null) setImage(parent, bitmap, url);
190             // 移除任务。
191             if (taskMap.containsKey(url)) taskMap.remove(url);
192         }
193     }
194
195     /** 使用完毕必须调用。 */
196     public void close() {
197         for (Map.Entry<String, AsyncTask> entry : taskMap.entrySet()) {
198             entry.getValue().cancel(true);
199         }
200
201         DiskLruCacheHelper.closeCache();
202         LruCacheHelper.closeCache();
203     }
204 }

ImageLoader

碎碎念

我怎么觉得我今天行文风格有点异常……

时间: 2025-01-12 17:18:56

由LruCache和DiskLruCache提供三级缓存支持的ImageLoader的相关文章

Android三级缓存机制工具类的实现

一.三级缓存概述 (一)三级缓存的三级 第一级是内存,最快,不需要网络 第二级是本地,不需要网络 第三级是网络,需要网络请求 三级缓存机制的思想: 如果在内存中获取到数据,就不去本地和网络中获取. 如果在本地中获取到数据就不去网络中获取, 如果内存和本地中不存在数据,就要去网络中请求数据 三级缓存技术能有效节省用户的流量,但是也会增加一些内存负担. 二.使用示例展示三级缓存工具栏类的使用 程序运行后的页面: 虽然只用一个按钮和一个图片显示,但是通过测试(联网状态和断网状态对比)能知道图片是从网络

网络图片的获取以及二级缓存策略(Volley框架+内存LruCache+磁盘DiskLruCache)

在开发安卓应用中避免不了要使用到网络图片,获取网络图片很简单,但是需要付出一定的代价——流量.对于少数的图片而言问题不大,但如果手机应用中包含大量的图片,这势必会耗费用户的一定流量,如果我们不加以处理,每次打开应用都去网络获取图片,那么用户可就不乐意了,这里的处理就是指今天要讲的缓存策略(缓存层分为三层:内存层,磁盘层,网络层). 关于缓存层的工作,当我们第一次打开应用获取图片时,先到网络去下载图片,然后依次存入内存缓存,磁盘缓存,当我们再一次需要用到刚才下载的这张图片时,就不需要再重复的到网络

Android之本地缓存——LruCache(内存缓存)与DiskLruCache(硬盘缓存)统一框架

本文参考郭霖大神的DiskLruCache解析,感兴趣的朋友可以先到http://blog.csdn.net/guolin_blog/article/details/28863651了解. 一.前言 该框架或者说库,主要是用于本地的图片缓存处理. 数据的存入 当你取到图片的元数据,会将数据存入硬盘缓存以及内存缓存中. 数据的获取 取数据的时候,先从内存缓存中取: 如果没有取到,则从硬盘缓存中取(此时如果硬盘缓存有数据,硬盘缓存会重新将数据写入内存缓存中): 如果硬盘缓存中没有取到,则从网上重新获

Android --- 简单实现三级缓存LruCache

三级缓存: 1.网络缓存 从网络获取资源 2.本地缓存 从本地获取数据 3.内存缓存 从内存获取数据 内存缓存:主要是用到了LruCache这个类,这个类比较适合用来缓存图片,它会将强引用对象放在LineedHashMap中,当缓存数据大小达到预定值的时候会将在该集合中比较少使用的对象从内存中移除. package com.itljw.zhbj.util; import android.graphics.Bitmap; import android.util.LruCache; /** * Cr

属性动画与图片三级缓存

属性动画 动画: UI渐变, 变量值的变化 ObjectAnimator : ofInt("backgroundColor",start,end); ValueAnimator: for(int i = start; i< end; i++) { a = i; } ValueAnimator animation=ValueAnimator.ofInt(start,end); animation.setDuration(DURATION); animation.addUpdateL

图片三级缓存的原理

三级缓存的概念: 内存-->硬盘-->网络 由内存.硬盘.网络缓存形成. 关于三级缓存用到的技术: Android高效加载大图.多图解决方案.有效避免程序OOM使用的核心技术就是LruCache. LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时.对此,Google又提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证. 用法和流程: 当每次加载图片的时候都优先去内存加载图

浅谈图片载入的三级缓存(一)

之前被人问及过.图片的三级缓存是什么啊,来给我讲讲,图片三级缓存,好高大尚的名字,听着挺厉害,应该是非常厉害的技术.当时不会啊,也就没什么了.没有说出来呗,前一阶端用到了BitmapUtils的图片缓存框架,索性就自己找些知识点来研究一些图片的三级缓存是什么吧.真所谓是知识你要是不知道,那就真的说不出所以然来.可是当你真正的去了解了.三级缓存也不是那么高端的技术. 好了,闲话不多说了.開始图片的三级缓存原理吧. 什么是图片的三级缓存 1.内存缓存 优先载入,速度最快 2.本地缓存 次优先载入 速

Android实战——RxJava2解锁图片三级缓存框架

RxJava2解锁图片三级缓存框架 本篇文章包括以下内容 前言 图片三级缓存的介绍 框架结构目录的介绍 构建项目整体框架 实现图片三级缓存 演示效果 源码下载 结语 前言 RxJava2作为如今那么主流的技术,不学习学习都不行了,本篇文章需要你有RxJava2的基础,如果需要对RxJava2学习的同学,可以关注我的博客,查看Android实战--RxJava2+Retrofit+RxBinding解锁各种新姿势 .项目代码实现模仿Picasso,大伙们可以看下最后的代码效果,那么废话不多说,He

使用LruCache和DiskLruCache手写一个ImageLoader

一.概述 在分析OkHttp3的缓存机制之前先手写一个实现了三级缓存的ImageLoader来整体感受一下LruCache和DiskLruCache的用法.本例实现了三级缓存,利用LruCache实现内存缓存,利用DiskLruCache实现磁盘缓存.整体的流程是:当用户请求一张图时,首先检查内存中是否有缓存图片,如果有就直接返回,如果没有就检查磁盘中是否有,有就返回,没有就启用网络下载图片,然后把图片缓存在磁盘缓存和内存缓存中.下面看看具体的实现步骤. 二.源程序介绍 1.ImageLoade