从三天前一直报错到今天中午,总算出了个能用的版本了。
一如既往先发链接:
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 }
·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 }
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 }
碎碎念
我怎么觉得我今天行文风格有点异常……