转载

由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

碎碎念

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

正文到此结束
Loading...