概述:
现在android应用中不可避免的要使用图片,有些图片是可以变化的,需要每次启动时从网络拉取,这种场景在有广告位的应用以及纯图片应用(比如百度美拍)中比较多。
现在有一个问题:假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量。在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的应用,其用户数量级肯定要受到影响。当然,我想,向百度美拍这样的应用,必然也有其内部的图片缓存策略。总之,图片缓存是很重要而且是必须的。
应用实例
比如一个新闻客户端,要在listView中显示新闻、图片等信息,如果没有网络,就会显示以前显示过的新闻。图片。原因:这些数据被缓存到SD卡中,也就是下面说的二级缓存中(一级缓存中的数据 在退出程序时就会被释放的。程序结束,下载的图片对象被销毁,所以说数据不会存储在以及缓存中。)
动态地显示列表中的图片
1.图片缓存优先级:Bitmap优于手机本地的图片文件优于服务器端的图片文件
一级缓存:内存缓存,缓存的是bitmap对象(这些对象在内存如何存储呢?一般而言有两种方式:List和Map,List根据下标来得到对象,Map根据Key得到对象,实际图片存储都是用Map)。
对于绝大多数图片缓存,用的都是LruCatch,(我下面这段代码只是简单的存储在Map中,没有用较复杂的数据结构。)比如这篇博文:https://blog.youkuaiyun.com/lovoo/article/details/51456515
LruCache非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
二级缓存:本地(SD卡)缓存,缓存的是图片文件,/storage/sdcard/Android/data/packageName/files/图片文件名(xxx.jpg)
三级缓存:远程服务器缓存,缓存的是图片文件,远程服务器上的应用中
2.如何使用三级缓存?-----如何根据图片的URL动态显示图片?
String imagePath = http://192.168.10.165:8080//L05_Web/images/f10.jpg和ImageView对象
1)。根据Url从一级缓存中取对应的bitmap对象
如果有,显示(结束)
如果没有,进入2
2)。从二级缓存中查找:得到文件名,并在sd卡的缓存目录下加载对应的图片得到Bitmap对象
如果有:显示,缓存到一级缓存中(结束)
如果没有,进入3
3)。显示代表提示正在加载的图片,启动分线程联网请求得到Bitmap对象
如果没有:显示提示错误的图片(结束)
如果有:
显示
缓存到一级缓存
缓存到二级缓存
package com.atguigu.app05_handler;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
/**
* 用于加载图片并显示的类
* @author 张晓飞
*
*/
/*
String iamgePath = http://192.168.10.165:8080//L05_Web/images/f10.jpg和ImageView对象
1). 根据url从一级缓存中取对应的bitmap对象
如果有, 显示(结束)
如果没有, 进入2)
2). 从二级缓存中查找: 得到文件名并在sd卡的缓存目录下加载对应的图片得到Bitmap对象
如果有: 显示, 缓存到一级缓存中(结束)
如果没有, 进入3)
3). 显示代表提示正在加载的图片, 启动分线程联网请求得到Bitmap对象
如果没有: 显示提示错误的图片(结束)
如果有:
显示
缓存到一级缓存
缓存到二级缓存
*/
public class ImageLoader {
private Context context;
private int loadingImageRes;
private int errorImageRes;
public ImageLoader(Context context, int loadingImageRes, int errorImageRes) {
super();
this.context = context;
this.loadingImageRes = loadingImageRes;
this.errorImageRes = errorImageRes;
}
//用于缓存bitmap的容器对象
private Map<String, Bitmap> cacheMap = new HashMap<String, Bitmap>();
/**
* 加载图片并显示
* @param imagePath
* @param imageView
*/
public void loadImage(String imagePath, ImageView imageView) {
//将需要显示的图片url保存到视图上
imageView.setTag(imagePath); // 1
/*
1). 根据url从一级缓存中取对应的bitmap对象
如果有, 显示(结束)
如果没有, 进入2)
*/
Bitmap bitmap = getFromFirstCache(imagePath);
if(bitmap!=null) {
imageView.setImageBitmap(bitmap);
return;
}
/*
2). 从二级缓存中查找: 得到文件名并在sd卡的缓存目录下加载对应的图片得到Bitmap对象
如果有: 显示, 缓存到一级缓存中(结束)
如果没有, 进入3)
/storage/sdcard/Android/data/packageName/files/图片文件名(xxx.jpg)
*/
bitmap = getFromSecondCache(imagePath);
if(bitmap!=null) {
imageView.setImageBitmap(bitmap);
cacheMap.put(imagePath, bitmap);
return;
}
/*
3). 显示代表提示正在加载的图片, 启动分线程联网请求得到Bitmap对象
如果没有: 显示提示错误的图片(结束)
如果有:
缓存到一级缓存(分线程)
缓存到二级缓存(分线程)
显示(主线程)
*/
loadBitmapFromThirdCache(imagePath, imageView);
}
/**
* 根据图片url从三级缓存中取对应的bitmap对象并显示
* @param imagePath
* @param imageView
* AsyncTask
* loadBitmapFromThirdCache("../b.jpg", imageView)
* loadBitmapFromThirdCache("../f.jpg", imageView)--->imageView.setTag("../f.jpg")
*/
private void loadBitmapFromThirdCache(final String imagePath, final ImageView imageView) {
new AsyncTask<Void, Void, Bitmap>() {
protected void onPreExecute() {
imageView.setImageResource(loadingImageRes);
}
//联网请求得到bitmap对象
@Override
protected Bitmap doInBackground(Void... params) {
//在分线程执行, 可能需要等待一定时间才会执行
//在等待的过程中imageView中的tag值就有可能改变了
//如果改变了, 就不应该再去加载图片(此图片此时不需要显示)
Bitmap bitmap = null;
try {
//在准备请求服务器图片之前, 判断是否需要加载
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了
return null;
} // 2
//得到连接
URL url = new URL(imagePath);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
//连接
connection.connect();
//发请求读取返回的数据并封装为bitmap
int responseCode = connection.getResponseCode();
if(responseCode==200) {
InputStream is = connection.getInputStream();//图片文件流
//将is封装为bitmap
bitmap = BitmapFactory.decodeStream(is);
is.close();
if(bitmap!=null) {
//缓存到一级缓存(分线程)
cacheMap.put(imagePath, bitmap);
//缓存到二级缓存(分线程)
// /storage/sdcard/Android/data/packageName/files/
String filesPath = context.getExternalFilesDir(null).getAbsolutePath();
// http://192.168.10.165:8080//L05_Web/images/f10.jpg
String fileName = imagePath.substring(imagePath.lastIndexOf("/")+1);// f10.jpg
String filePath = filesPath+"/"+fileName;
bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(filePath));
}
}
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
protected void onPostExecute(Bitmap bitmap) {//从联网请求图片到得到图片对象需要一定的时间, 视图可能被复用了,不需要显示
//在主线程准备显示图片之前, 需要判断是否需要显示
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了
return;
} // 3
//如果没有: 显示提示错误的图片(结束)
if(bitmap==null) {
imageView.setImageResource(errorImageRes);
} else {//如果有, 显示
imageView.setImageBitmap(bitmap);
}
}
}.execute();
}
/**
* 根据图片url从二级缓存中取对应的bitmap对象
* @param imagePath
* @return
*/
private Bitmap getFromSecondCache(String imagePath) {
// /storage/sdcard/Android/data/packageName/files/
String filesPath = context.getExternalFilesDir(null).getAbsolutePath();
// http://192.168.10.165:8080//L05_Web/images/f10.jpg
String fileName = imagePath.substring(imagePath.lastIndexOf("/")+1);// f10.jpg
String filePath = filesPath+"/"+fileName;
return BitmapFactory.decodeFile(filePath);
}
/**
* 根据图片url从一级缓存中取对应的bitmap对象
* @param imagePath
* @return
*/
private Bitmap getFromFirstCache(String imagePath) {
return cacheMap.get(imagePath);
}
}
运行效果:
但是,图片一直在变换,并不固定。
3)。在ListView使用图片三级缓存会存在图片闪动的bug(不包括红色的代码)
ps:反正我是没听说在GridView、RecycleView中出现了这种BUG
图片闪动是listView中特有的bug
1。原因
1.convertView被复用了(convertView用于将之前加载好的布局进行缓存,以便之后进行重用。)
2.开启了多线程
getView方法的调用:
getview()一般会在,第一次显示,页面刷新,或者当adapter对应的数据源变化时候会主动调用,notifyDataSetChanged等方法主动刷新界面时被调用,也就是当listview需要显示item的时候就会调用getview()
每当Listiiew滑动的时候,就会调用getView(),由于需要加载图片,所以必须开启线程。
当向下滑动到C、D、F、G的时候,F的imageView会复用A的imageView,G的imageView会复用B的imageview,于是就会出现F显示a.jpg,G显示b.jpg。等到线程调用结束,也就是得到了f.jpg/g.jpg并完成对imageView1、imageView2的赋值时,才会显示f.jpg,G.jpg.这就是闪动的原因(这是开启线程比较少时的情况分析,理解上面代码时,最好不用这个方式,会将你带入误区的,进去了就出不来了,出不来了,出不来了----------------------------------------------------------)。
当快速向上滑动时,比如滑动100个item,那么就会调用100此getView,image1/2/3/4会复用至少7/8次,会闪动25次。(为什么不是25次?线程运行完成需要时间的嘛!在这么短的时间里又不是所有的都运行完成了。)
2。解决
在这里说一下解决问题关键:
在分线程执行, 可能需要等待一定时间才会执行
在等待的过程中imageView中的tag值就有可能改变了,也就是说imageView已经加载了别的图片了
如果改变了, 就不应该再去加载图片(此图片此时不需要显示)
比如,在快速滑动的过程中,会开启多个线程,属于F这个item的线程可能要等待一定时间才会执行,等待的过程中,tag值就可能发生了改变。
总结:解决以上问题只需要三部:就是以上红色代码标志的地方。
a。每次getView()都将图片的url保存到ImageView上:imageView.setTag(imagePath)
b。在分线程准备请求服务器加载图片之前,比较准备加载图片的url与ImageView中保存的最新图片的url是同一个
如果不是同一个,当前加载图片的任务不应该执行
如果相同,继续执行加载远程图片
c。在主线程准备显示图片之前,比较加载到图片的url与ImageView中保存的最新图片的url是同一个
如果不是同一个,不需要显示此图片
如果相同,显示图片
另附一句:getTag()、setTag()是View中的方法。
思考:为什么TextView中不会出现闪动的问题?
很简单,没有使用多线程,不会出现使用多线程读取服务器数据,即先调用别的线程已经读取了数据,然后再读取此线程中的数据的情况。
思考:为什么会说和convertView有关呢?
由于convertView可以被复用,也就是说,在快速向下滑动的时候,imageView1会被多次复用,只有imageView1中线程的图片才会被加载到这个被多次复用的imageView1中。这么多属于imageView1的线程,由于可以“先调用别的线程已经读取了数据,然后再读取此线程中的数据。”所以imageView1在特定时间内会不断地出现闪动。所以说convertView+多线程导致了图片加载的闪动
视频学习地址:http://www.gulixueyuan.com/course/112/task/1732/show
图片三级缓存进阶:https://blog.youkuaiyun.com/lovoo/article/details/51456515