图片如何高效加载与缓存
这是我写的第一篇博客,我也只是个大三的学生,代码和文章仍有很多的不足之处,还请各位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