1. 图片三级缓存流程图(盗用的网上的)
2. 内存缓存
2.1 java中对象的四种引用类型
-
强引用
Object obj = new Object();
java中所有new出来的对象都是强引用类型,gc宁愿抛出OOM异常,也不会回收它
-
软引用,SoftReference
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj);
当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收
-
弱引用,WeakReference
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj);
当一个对象只有弱引用存在时,此对象随时会被gc回收
-
虚引用
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj);
每次垃圾回收的时候都会被回收
2.2 使用LruCache类来做内存缓存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, SoftReference<Bitmap>>(cacheSize) {
//必须重写此方法,来测量Bitmap的大小
@Override
protected int sizeOf(String key, SoftReference<Bitmap> value) {
return value.get() == null ? 0 : value.get().getByteCount();
}
};
2.3 从内存中获取图片
//从内存中查找图片,有就返回,无则去文件中查找
SoftReference<Bitmap> reference = mLruCache.get(url);
Bitmap cacheBitmap;
if (reference != null) {
//有则显示图片
cacheBitmap = reference.get();
if (cacheBitmap != null) {
imageView.setImageBitmap(cacheBitmap);
Log.d(TAG, "内存中有图片显示");
//不往下走了
return;
}
}
3. 磁盘缓存
3.1 设定文件路径
private File getCacheDir() {
File file;
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
// 有SD卡就保存到sd卡
file = mContext.getExternalCacheDir();
} else {
// 没有就保存到内部储存
file = mContext.getCacheDir();
}
return file;
}
3.2 解析文件生成图片
private Bitmap getBitmapFromFile() {
// 从url中获取文件名字
String fileName = url.substring(url.lastIndexOf("/") + 1);
File file = new File(getCacheDir(), fileName);
// 确保路径没有问题
if (file.exists() && file.length() > 0) {
// 返回图片
return BitmapFactory.decodeFile(file.getAbsolutePath());
} else {
return null;
}
}
3.2 从磁盘缓存中获取图片
Bitmap diskBitmap = getBitmapFromFile();
if (diskBitmap != null) {
imageView.setImageBitmap(diskBitmap);
Log.d(TAG, "磁盘中有图片显示");
//不往下走了
return;
}
4. 从网络中请求图片
4.1 创建一个线程池
private ExecutorService mExecutorService = Executors.newFixedThreadPool(5);
4.2 利用线程池执行任务
mExecutorService.submit(this);
4.3 利用HttpURLConnection请求图片
URL loadUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) loadUrl.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(2000);
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = conn.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(is);
mHandler.post(new Runnable() {
@Override
public void run() {
//主线程显示图片
imageView.setImageBitmap(bitmap);
Log.d(TAG, "网络请求图片");
}
});
is.close();
4.3 将图片存入内存中
mLruCache.put(url, new SoftReference<Bitmap>(bitmap));
4.4 将图片存入磁盘
private void saveBitmapToFile(Bitmap bitmap) throws FileNotFoundException {
String fileName = url.substring(url.lastIndexOf("/") + 1);
File file = new File(getCacheDir(), fileName);
FileOutputStream os = new FileOutputStream(file);
// 将图片转换为文件进行存储
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
}
5. 通过listview异步加载图片优化
5.1 listview列表错乱
现象
快速滑动列表,图片的位置可能会发生错位,即图片和对应的列表位置对应不上。-
原因
重用了 convertView 且有异步操作,两者缺一不可。
我来简单分析一下
当重用convertView时,最初一屏幕显示7个记录,getView被调用7次,也就创建了7个convertView。当Item1被滑出屏幕外,Item8进入屏幕时,这时没有为Item8创建新的view实例,Item8复用的是Item1的view,如果没有异步就没有任何问题,虽然Item8和Item1用的是一个view,但滑动到Item8时已经刷上了Item8的数据,这时Item1的数据和Item8的数据是一样的,因为它们指向的是同一块内存,但此时Item1已经在屏幕外了,你是看不见的。当Item1可见时,又刷上了Item1的数据。
但是当异步就会有问题了。如果Item1下载图片的速度比较慢,Item8下载图片的速度比较快,当Item8可见时,Item8会先显示自己下载的图片,但等到Item1下载完成时你会发现Item8的图片变成了Item1的图片,因为它们复用的是同一个view。如果Item8下载图片速度较慢,Item1下载图片速度较快,当Item8可见时,Item8会显示Item1的图片,之后再显示Item8的图片,就会造成闪烁的状况。
-
解决办法
给ImageView设置一个tag,只有满足条件才设置图片。holder.icon_iv.setTag(mNewsBeans.get(position).getUrl()); if (imageView.getTag() != null && imageView.getTag().equals(url)) { imageView.setImageBitmap(bitmap); Log.d(TAG, "网络请求图片"); }
分析
如果Item1下载图片的速度比较慢,Item8下载图片的速度比较快,当Item8可见时,Item8满足if条件,并显示自己下载的图片,当item1下载完成时,由于当前的tag是Item8得url,而下载图片的url是Item1得url,当然不满足if条件,也就不会再设置图片了。
5.2 listview卡顿
现象以及原因
如果用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,这些异步任务会造成线程池的堵塞,并且会带来大量的UI更新操作,由于一瞬间存在大量的UI更细操作,这些UI操作是运行在主线程的,这样机会造成一定程度的卡顿。-
解决办法
可以在列表滑动的时候停止异步任务,而在停下来以后再加载图片。if (mIsListViewIdle) { ImageLoader.with(mContext).load(mNewsBeans.get(position).getUrl()).placeholder(R.mipmap.ic_launcher).into(holder.icon_iv); } public void onScrollStateChanged(AbsListView view, int scrollState) { //停止滚动 if (scrollState == SCROLL_STATE_IDLE) { mIsListViewIdle = true; notifyDataSetChanged(); } else { mIsListViewIdle = false; } }
题外话,开启硬件加速也可以解决一些卡顿问题
<activity android:hardwareAccelerated="true" ...>
6. 完整的ImageLoader代码
public class ImageLoader {
private static final String TAG = "ImageLoader";
private Context mContext;
private static ImageLoader instance;
private LruCache<String, SoftReference<Bitmap>> mLruCache;
private ExecutorService mExecutorService = Executors.newFixedThreadPool(5);
private Handler mHandler;
private ImageLoader(Context context) {
mContext = context;
mHandler = new Handler(Looper.getMainLooper());
//计算程序分配的最大内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, SoftReference<Bitmap>>(cacheSize) {
//必须重写此方法,来测量Bitmap的大小
@Override
protected int sizeOf(String key, SoftReference<Bitmap> value) {
return value.get() == null ? 0 : value.get().getByteCount();
}
};
}
private static ImageLoader getInstance(Context context) {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader(context);
}
}
}
return instance;
}
public static ImageLoader with(Context context) {
return getInstance(context);
}
public RequestCreator load(String url) {
return new RequestCreator(url);
}
public class RequestCreator implements Runnable {
String url;
int holderResId;
int errorResId;
ImageView imageView;
public RequestCreator(String url) {
this.url = url;
}
public RequestCreator placeholder(int holderResId) {
this.holderResId = holderResId;
return this;
}
public RequestCreator error(int errorResId) {
this.errorResId = errorResId;
return this;
}
public void into(ImageView imageView) {
this.imageView = imageView;
//先设置占位图片
imageView.setImageResource(holderResId);
//从内存中查找图片,有就返回,无则去磁盘中查找
SoftReference<Bitmap> reference = mLruCache.get(url);
Bitmap cacheBitmap;
if (reference != null) {
//有则显示图片
cacheBitmap = reference.get();
if (cacheBitmap != null) {
imageView.setImageBitmap(cacheBitmap);
Log.d(TAG, "内存中有图片显示");
//不往下走了
return;
}
}
//去磁盘中查找图片,有就返回,无则去网络中请求
Bitmap diskBitmap = getBitmapFromFile();
if (diskBitmap != null) {
imageView.setImageBitmap(diskBitmap);
Log.d(TAG, "磁盘中有图片显示");
//不往下走了
return;
}
//磁盘中没有,启动线程池请求图片
mExecutorService.submit(this);
}
@Override
public void run() {
try {
URL loadUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) loadUrl.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(2000);
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = conn.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(is);
mHandler.post(new Runnable() {
@Override
public void run() {
//主线程显示图片
if (imageView.getTag() != null && imageView.getTag().equals(url)) {
imageView.setImageBitmap(bitmap);
Log.d(TAG, "网络请求图片");
}
}
});
is.close();
//保存到内存
mLruCache.put(url, new SoftReference<Bitmap>(bitmap));
//保存到磁盘中
saveBitmapToFile(bitmap);
} else {
Log.d(TAG, "网络请求码非200");
showError();
}
} catch (Exception e) {
Log.d(TAG, "网络请求图片发生异常");
showError();
}
}
/**
* 显示错误图片
*/
private void showError() {
mHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageResource(errorResId);
}
});
}
/**
* 将bitmap保存到磁盘中
*/
private void saveBitmapToFile(Bitmap bitmap) throws FileNotFoundException {
String fileName = url.substring(url.lastIndexOf("/") + 1);
File file = new File(getCacheDir(), fileName);
FileOutputStream os = new FileOutputStream(file);
// 将图片转换为文件进行存储
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
}
/**
* 从文件中获取bitmap
*/
private Bitmap getBitmapFromFile() {
// 从url中获取文件名字
String fileName = url.substring(url.lastIndexOf("/") + 1);
File file = new File(getCacheDir(), fileName);
// 确保路径没有问题
if (file.exists() && file.length() > 0) {
// 返回图片
return BitmapFactory.decodeFile(file.getAbsolutePath());
} else {
return null;
}
}
/**
* 获取缓存路径目录
*/
private File getCacheDir() {
File file;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// 有SD卡就保存到sd卡
file = mContext.getExternalCacheDir();
} else {
// 没有就保存到内部储存
file = mContext.getCacheDir();
}
return file;
}
}
}
用法
ImageLoader.with(this).load(imgUrl).placeholder(resId).error(resId).into(imageView);
7. 扩展
现在流行的图片加载框架,在缓存处理这块做了很多细节处理。后续可以考虑在缓存这块扩展一下,比如设置是否缓存的开关,设置文件缓存的路径和大小等等。