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

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

一如既往先发链接:

https://github.com/mlxy/ImageLoader

 

缓存处理

 

·LruCacheHelper:

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

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

Runtime.getRuntime().maxMemory() / 8

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

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

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

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

由LruCache和DiskLruCache提供三级缓存支持的ImageLoader
 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。

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

由LruCache和DiskLruCache提供三级缓存支持的ImageLoader
 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来自行缩放图片大小,那就是另一回事了。

 

最后还是代码说话:

由LruCache和DiskLruCache提供三级缓存支持的ImageLoader
  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

 

 

碎碎念

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

你可能感兴趣的:(loader)