由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

 

 

碎碎念

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

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。