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 且有異步操作,兩者缺一不可。
listview布局復用圖.jpg
我來簡單分析一下
當重用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. 擴展
現在流行的圖片加載框架,在緩存處理這塊做了很多細節處理。后續可以考慮在緩存這塊擴展一下,比如設置是否緩存的開關,設置文件緩存的路徑和大小等等。