Android 图片如何高效加载与缓存
2016-02-17 17:31
411 查看
图片如何高效加载与缓存
这是我写的第一篇博客,我也只是个大三的学生,代码和文章仍有很多的不足之处,还请各位dalao在发现不足之处之后再评论区回复……谢谢
图片加载
一级缓存: 自定义的LRUcache类二级缓存: 本地文件 File
网络请求框架: OKHttp
在得到图片加载请求之后,首先检查一级、二级缓存中是否有与请求 tag 相符合的缓存对象,有缓存对象则使用 FetcherByCache 线程来处理,若是没有则使用 Fetcher 线程来获取。
若请求加载的不是网络图片而是 本地图片 ,则使用 FetcherByLocal 线程来进行处理。
其中 FetcherByCache 与 FetcherByLocal 线程是共用同一个线程池(最大线程数 3),而 Fetcher 线程是单独的一个线程池(最大线程数 1)。
其中我们的每一个图片加载请求都有一个唯一的 tag 这个 tag 由图片的请求网址或者是本地路径生成,是对应请求的 ImageView 唯一标识,也是线程池中任务的唯一标识。如果线程池中有与请求的 tag 相同的任务没处理完,则会拒绝执行。
●(防止用户在反复滑动ListView之类的控件时产生许多重复的请求)
Fetcher 线程:
private class Fetcher implements Runnable,ThreadinterFace { private String tag; private String imageUrl; private ImageView imageView; private OnImageLoad onImageLoad; private Bitmap cacheImage; public Fetcher(String tag,String imageUrl,ImageView imageView,OnImageLoad onImageLoad) { this.tag = tag; this.imageUrl = imageUrl; this.imageView = imageView; this.onImageLoad = onImageLoad; } public Fetcher(ImageRequired required) { this.tag = required.getTag(); this.imageUrl = required.getImageURL(); this.imageView = required.getImageView(); this.onImageLoad = required.getOnImageLoad(); } @Override public void run() { if (imageView != null || onImageLoad != null){ OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(5,TimeUnit.SECONDS).build(); Request request = new Request.Builder().url(imageUrl).build(); try { Call call = okHttpClient.newCall(request); Response response = call.execute(); cacheImage = BitmapFactory.decodeStream(response.body().byteStream()); if (cacheImage != null) imageCacher.putCache(tag,cacheImage); } catch (IOException e) { Log.d("On loading image", "\nurl:" + imageUrl +"\nError:"+ e.toString()); onError(0); } runOnUIThread(new Runnable() { @Override public void run() { onDownloadCompleted(cacheImage,imageView,tag,onImageLoad); } }); }else { runOnUIThread(new Runnable() { @Override public void run() { onError(1); } }); } } @Override public void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag,OnImageLoad onImageLoadCompleted) { fetherExecutor.removeName(tag); if (imageView == null && bitmap != null && onImageLoadCompleted != null){ onImageLoadCompleted.onLoadCompleted(bitmap); }else if (imageView != null && bitmap != null && tag.equals(imageView.getTag())){ if (durationMillis > 0 ){ Drawable prevDrawable = imageView.getDrawable(); if (prevDrawable == null) { prevDrawable = new ColorDrawable(TRANSPARENT); } Drawable nextDrawable = new BitmapDrawable(imageView.getResources(), bitmap); TransitionDrawable transitionDrawable = new TransitionDrawable( new Drawable[] { prevDrawable, nextDrawable }); imageView.setImageDrawable(transitionDrawable); transitionDrawable.startTransition(durationMillis); }else { imageView.setImageBitmap(cacheImage); } } } @Override public void onError(int status) { fetherExecutor.removeName(tag); if (onImageLoad != null){ onImageLoad.onLoadFailed(); } } @Override public int hashCode() { return tag.hashCode(); } @Override public boolean equals(Object o) { return this.hashCode() == o.hashCode() && o instanceof Fetcher; } }
● 代码中 fetherExecutor.removeName(tag); 这句话的用途是告知线程池这个图片对应的 tag 的任务已经完成,不再拒绝具有相同 tag 的任务请求。 线程池的代码在最后贴出来。
● 代码中 imageCacher.putCache(tag,cacheImage); 是将图片交由图片缓存类处理,这个 ImageCacher 类同样在接下来的 自定义LRUcache 中写出来
FetcherByCache 线程:
private class FetcherByCache implements Runnable,ThreadinterFace{ private String tag; private String imageUrl; private ImageView imageView; private OnImageLoad onImageLoad; private Bitmap cacheImage; public FetcherByCache(String tag,String imageUrl,ImageView imageView,OnImageLoad onImageLoad) { this.tag = tag; this.imageUrl = imageUrl; this.imageView = imageView; this.onImageLoad = onImageLoad; } public FetcherByCache(ImageRequired required) { this.tag = required.getTag(); this.imageUrl = required.getImageURL(); this.imageView = required.getImageView(); this.onImageLoad = required.getOnImageLoad(); } @Override public void run() { cacheImage = imageCacher.getCache(tag); if (cacheImage != null){ runOnUIThread(new Runnable() { @Override public void run() { onDownloadCompleted(cacheImage,imageView,tag,onImageLoad); } }); }else if (imageUrl != null){ runOnUIThread(new Runnable() { @Override public void run() { onError(1); } }); } } @Override public void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag, OnImageLoad onImageLoadCompleted) { cacheExecutor.removeName(tag); if (imageView == null && bitmap != null && onImageLoadCompleted != null){ onImageLoadCompleted.onLoadCompleted(bitmap); }else if (imageView != null && bitmap != null && tag.equals(imageView.getTag())){ if (durationMillis > 0 ){ Drawable prevDrawable = imageView.getDrawable(); if (prevDrawable == null) { prevDrawable = new ColorDrawable(TRANSPARENT); } Drawable nextDrawable = new BitmapDrawable(imageView.getResources(), bitmap); TransitionDrawable transitionDrawable = new TransitionDrawable( new Drawable[] { prevDrawable, nextDrawable }); imageView.setImageDrawable(transitionDrawable); transitionDrawable.startTransition(durationMillis); }else { imageView.setImageBitmap(bitmap); } } } @Override public void onError(int status) { cacheExecutor.removeName(tag); loadImage(tag, imageUrl, imageView, onImageLoad); } @Override public int hashCode() { return tag.hashCode(); } @Override public boolean equals(Object o) { return this.hashCode() == o.hashCode() && o instanceof FetcherByCache; } }
●这个线程中要注意调用的 public void onError(int status)
当缓存不存在的时候,我们就重新执行图片拉取请求,但这次的就是网络获取
FetcherByLocal 线程:
这个线程由于是读取本地图片,所以允许带上 BitmapFactory.Options 进行加载,所以我们也会缓存带选项加载的图片,同时还要与不带选项的图片缓存进行区分,我们在 tag 这里进行区分就行。
private class FetcherByLocal implements Runnable,ThreadinterFace{ private OnImageLoad onImageLoad; private ImageView imageView; private String filePath; private String tag; private BitmapFactory.Options options; private Bitmap bitmap; public FetcherByLocal(ImageView imageView,OnImageLoad onImageLoad, String filePath,BitmapFactory.Options options) { this.onImageLoad = onImageLoad; this.imageView = imageView; this.filePath = filePath; this.options = options; this.tag = buildTag(filePath); } @Override public void run() { File file = new File(filePath); if (options == null){ bitmap = imageCacher.getByLruCache(filePath); }else { bitmap = imageCacher.getByLruCache(filePath+"withop"); } if (bitmap == null && file.exists() && file.canRead()){ if (options != null){ bitmap = BitmapFactory.decodeFile(filePath,options); imageCacher.putInLruCaches(filePath+"withop", bitmap); } else{ bitmap = BitmapFactory.decodeFile(filePath,options); imageCacher.putInLruCaches(filePath, bitmap); } runOnUIThread(new Runnable() { @Override public void run() { onDownloadCompleted(bitmap, imageView, filePath, onImageLoad); } }); }else if (bitmap != null){ runOnUIThread(new Runnable() { @Override public void run() { onDownloadCompleted(bitmap,imageView,filePath,onImageLoad); } }); }else{ fetherExecutor.removeName(filePath); Log.e("On loading image","Image file is not exist or is not readable"); } } @Override public void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag, OnImageLoad onImageLoadCompleted) { if (options != null) { fetherExecutor.removeName(tag+"withop"); }else { fetherExecutor.removeName(tag); } if (bitmap == null){ Log.e("On loading image", "Load image failed. Path: " + tag); return; } if (onImageLoadCompleted != null){ onImageLoadCompleted.onLoadCompleted(bitmap); } if (imageView != null && imageView.getTag().equals(this.tag)){ if (durationMillis > 0 ){ Drawable prevDrawable = imageView.getDrawable(); if (prevDrawable == null) { prevDrawable = new ColorDrawable(TRANSPARENT); } Drawable nextDrawable = new BitmapDrawable(imageView.getResources(), bitmap); TransitionDrawable transitionDrawable = new TransitionDrawable( new Drawable[] { prevDrawable, nextDrawable }); imageView.setImageDrawable(transitionDrawable); transitionDrawable.startTransition(durationMillis); }else { imageView.setImageBitmap(bitmap); } } } @Override public void onError(int status) { if (onImageLoad != null){ onImageLoad.onLoadFailed(); } } @Override public int hashCode() { return tag.hashCode(); } @Override public boolean equals(Object o) { return this.hashCode() == o.hashCode() && o instanceof FetcherByLoacl; } }
图片加载请求
(pause部分的代码目前还在完善中,现在大家略过即可)图片加载前将 ImageView 设置为空并设置一个背景色,这样可以避免 ListView 重复利用Item View 的时候造成未加载的 ImageView 会显示图片错乱的现象
public void loadImage(String tag,String imageURL,ImageView imageView,OnImageLoad onImageLoad){ if (tag == null || TextUtils.isEmpty(tag)){ Log.e("On loading image","TAG is empty or null"); return; } tag = buildTag(tag); if (pause){ ImageRequired imageRequired = new ImageRequired(imageView, tag, imageURL, onImageLoad); if (imageView != null){ imageView.setBackgroundColor(Color.LTGRAY); imageView.setImageDrawable(null); } requiredList.remove(imageRequired); requiredList.add(imageRequired); }else { if (imageCacher == null) imageCacher = new OCImageCacher(); if (imageView != null){ if (imageView.getDrawable() != null && (imageView.getTag() != null && imageView.getTag().equals(tag))){ Log.d("Skipped","TAG:"+tag); return; }else { imageView.setImageBitmap(null); imageView.setBackgroundColor(Color.LTGRAY); imageView.setTag(tag); } } if (imageURL == null || imageCacher.isCacheExist(tag)){ Log.d("OnThread","CacheThread"); cacheExecutor.execute(new FetcherByCache(tag,imageURL,imageView,onImageLoad), tag); }else { Log.d("OnThread","FetcherThread"); fetherExecutor.execute(new Fetcher(tag,imageURL,imageView,onImageLoad),tag); } } }
● 在代码中的
if (imageView.getDrawable() != null && (imageView.getTag() != null && imageView.getTag().equals(tag)))
要说一下,就是要检查当前请求的 ImageView 是否已经存在图像,若是已经存在图像的我们就选择跳过。
( 比如用户当前显示着的这部分ListView已经加载完成了图像,这时候用户锁屏再解锁,那这部分的图片就不会重新请求加载 )
自定义线程池
我们贴上代码再说public class OCThreadExecutor extends ThreadPoolExecutor { private List<String> threadNames; public OCThreadExecutor(int maxRunningThread, String poolName) { super(maxRunningThread, maxRunningThread, 0l, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new OCThreadFactory(poolName)); this.threadNames = new ArrayList<>(); } public void execute(Runnable runnable, String taskTag) { if (!threadNames.contains(taskTag)) { this.threadNames.add(0, taskTag); execute(runnable); } else { Log.d("OCExecutor","Same thread tag,thread skipped!"+" TAG:"+taskTag); } } public void removeName(String tag) { threadNames.remove(tag); } public boolean remove(Runnable task,String tag){ threadNames.remove(tag); return remove(task); } static class OCThreadFactory implements ThreadFactory { private final String name; public OCThreadFactory(String name) { this.name = name; } @Override public Thread newThread(Runnable r) { return new OCThread(r, name); } } static class OCThread extends Thread { public OCThread(Runnable runnable, String name) { super(runnable, name); setName(name); } @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); super.run(); } } }
这个自定义线程池的使用放弃了原本的执行方法,而是用
public void execute(Runnable runnable, String taskTag)
来执行任务,在调用这方法的时候,会将 tag 记录在类里的一个 List 中,防止执行重复的任务。
在线程执行完毕之后再调用
public void removeName(String tag)
将列表中的 tag 移除,以允许执行相同的任务。
图片缓存 自定义LRUcache
首先我们先看自定义的 LRUcache 类private class CustomLRUCache extends LruCache<String,Bitmap>{ public CustomLRUCache(int maxSize) { super(maxSize); } List<String> keys = new ArrayList<>(); @Override protected int sizeOf(String key, Bitmap value) { return value.getHeight()*value.getRowBytes(); } public void putANDcount(String key, Bitmap value){ put(key, value); keys.add(key); } public boolean isExist(String key){ return keys.contains(key) ; } public Bitmap getCache(String key){ Bitmap cache = get(key); if (cache == null){ this.keys.remove(key); return null; }else { return cache; } } public void releaseCaches(){ evictAll(); this.keys.clear(); } }
●protected int sizeOf(String key, Bitmap value)
度量每个 Bitmap 对象的大小。这就不用多说了
●public boolean isExist(String key)
检查是否 曾经存在有 我们的对象。为啥这么说呢,我们接着看。
●public void putANDcount(String key, Bitmap value)
调用LRUcache的方法 put 之后,将这个 Bitmap 的tag存入 List 中,以供我们之后查找一级缓存。
●public Bitmap getCache(String key)
获取缓存对象,如果从缓存中获取到的对象是 NULL ,则我们将 List 中的 tag 去除。因为我们没有办法检查LRU缓存,所以我们只能交 由上一级的方法来处理 ( 请求缓存方法,下面就是 )
●public void releaseCaches()
释放所有的缓存,同时清空存放 tag 的 List
在这个创建这个类的时候我们应该这样创建:
lruCache = new CustomLRUCache((int)Runtime.getRuntime().totalMemory()/8);
括号中的语句的意思是,使用 app 所能使用的最大内存的 1/8 作为缓存。根据每个设备的RAM不同,阀值不同,app的可以使用最大内存各有不同。
请求缓存的方法:
protected Bitmap getCache(String tag){ Bitmap cacheImage = getByLruCache(tag); if (cacheImage != null){ Log.d("OnCacher",tag+" Got by LRUCache"); return cacheImage; } else if (canCacheAsFile){ cacheImage = getByFileCache(tag); if (cacheImage != null) putInLruCaches(tag,cacheImage); else { Log.d("OnCacher",tag+" No cache here [cache file has been deleted]"); return null; } return cacheImage; }else{ Log.d("OnCacher",tag+" No cache here"); return null; } }
●咱们如果一级缓存没有
Bitmap cacheImage = getByLruCache(tag); if (cacheImage != null){ ... }
就再判断app是否有文件读取的权限,如果有的话,我们就进行二级缓存的获取
cacheImage = getByFileCache(tag);
如果还是没有,那么我们就返回 NULL ,再给上一级的方法进行处理。
======================================
关于图片的加载获取其实也没多少可以说的,主要的方法就是上面的这些,下面是包装好的代码使用方法,已经相关的接口啥的:
接口OnImageLoad
public interface OnImageLoad { void onLoadCompleted(Bitmap image); void onLoadFailed(); }
接口ThreadinterFace
这个没什么用途,只不过是让线程内部比较规范而已
interface ThreadinterFace { void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag, OnImageLoad onImageLoadCompleted); void onError(int status); }
调用方法样例
//获取网络图片 OCImageLoader.loader().loadImage(tag, url, viewHolder.imageView,onImageLoad); //获取本地图片 OCImageLoader.loader().loadLocalImage(path,viewHolder.imageView,onImageLoad,option);
运行状态
如果看不清图片里的字,请直接打开图片的地址即可查看高清版本。。。设备一: Nexus 6 3GB RAM API23 ,第一张为本地图片,大小为 2.84MB
设备二:虚拟机 1GB RAM API22,第一张为本地图片,大小为 2.96MB
相关文章推荐
- Python动态类型的学习---引用的理解
- 按右键另存图片只能存BMP
- photoshop去除图片上的水印
- 土人系列AS入门教程 -- 对象篇
- C#托管堆对象实例包含内容分析
- upload上传单张图片
- 图片引发的溢出危机(图)
- 如何高效的使用内存
- C#实现获取不同对象中名称相同属性的方法
- javascript asp教程第十一课--Application 对象
- 浅析SQL Server中的执行计划缓存(上)
- 高效的mysql分页方法及原理
- Enterprise Library for .NET Framework 2.0缓存使用实例
- C#实现把彩色图片灰度化代码分享
- PowerShell中使用Out-String命令把对象转换成字符串输出的例子
- PowerShell中编程清空IE缓存方法
- PowerShell中使用.NET将程序集加入全局程序集缓存
- C#将图片和字节流互相转换并显示到页面上
- C#监控文件夹并自动给图片文件打水印的方法
- VBS教程:对象-正则表达式(RegExp)对象