學習內容:
- 如何有效加載 Bitmap
- Android 常用的緩存策略
- LurChche - 內存緩存
- DiskLurCache - 存儲緩存
- 優化列表的卡頓現象
1. Bitmap 的高效加載
如何加載圖片?
四類方法:
- BitmapFactory.decodeFile / decodeResource / decodeStream / decodeByteArray
- 分別對應從 文件系統 / 資源 / 輸入流 / 以及字節數組 中加載 Bitmap 對象
- 關系:decodeFile 和 decodeResource 間接調用了 decodeStream
如何高效加載圖片?
核心思想:采用 BitmapFactory.Options 加載所需尺寸的圖片
說明:主要是 inSampleSize 參數,即采樣率。inSampleSize 為 1 時,表示原始大小;當 inSampleSize 為 2 時,采樣后的圖片 寬/高 均變為原來的 1/2,即整個圖片縮小為原來的 1/4。inSampleSize 必須大于 1 才能起作用,效果以此類推。
-
具體方法(獲取采樣率):
- 將 BitmapFactory.Options 的 inJustDecodeBounds 參數設為 true 并加載圖片
- 從 BitmapFactory.Options 中取出圖片的原始寬/高信息,對應于 outWidth 和 outHeight 參數
- 計算所需的采樣率 inSampleSize
- 將 BitmapFactory.Options 的 inJustDecodeBounds 參數設為 false,然后重新加載圖片
inJustDecodeBounds 參數設為 true 時,BitmapFactory 只會解析圖片的原始 寬/高 信息,并不會真正的加載圖片
2. Android 中的緩存策略
為什么需要緩存?
兩方面原因:提高程序效率 + 節約流量開銷
緩存策略
一般來說,緩存策略主要包括 緩存的添加、獲取 和 刪除 這三個操作。
目前常用的一種緩存算法是 LRU,即最近最少使用算法,核心思想是緩存滿了時,優先淘汰最近最少使用的緩存對象。
2.1 LruCache
兼容性
LruCache 是 Android 3.1 提供的一個緩存類,如果需要兼容 3.1 以下的版本,則需要使用 support-v4 兼容包中提供的 LruCache
實現思想
LruCache 是一個泛型類,內部采用一個 LinkedHashMap 以強引用的方式存儲外界的緩存對象,通過 get 和 set 方法完成緩存的獲取和添加,當緩存滿時,LruCache 會移除較早使用的緩存對象,然后添加新的緩存對象。
LruCache 是線程安全的。
關于 強引用、軟引用和弱引用:
- 強引用:直接的對象引用
- 軟引用:當一個對象只有軟引用存在時,系統內存不足時此對象會被 GC 回收
- 弱引用:當一個對象只有弱引用存在時,此對象隨時被 GC 回收
原理
留待后續學習。
具體使用
- 創建:提供緩存的總容量大小并重寫 sizeOf 方法
- 獲?。篻et 方法
- 添加:put 方法
- 刪除:remove 方法刪除指定的緩存對象
2.2 DiskLruCache
簡述
DiskLruCache 用于實現存儲設備緩存,通過將緩存對象寫入文件系統從而實現緩存的效果。
源碼地址:https://github.com/JakeWharton/DiskLruCache
使用方式
-
DiskLruCache 的創建
- 通過
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
方法創建自身。 - 參數:
- directory:磁盤緩存在文件系統中的存儲路徑
- appVersion:應用的版本號,一般設為 1 即可
- valueCount:單個節點所對應的數據的個數,一般設為 1 即可
- maxSize:緩存總大小,超過這個設定值后,會清除一些緩存
- 典型代碼如下:
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB File diskCacheDir = getDiskCacheDir(mContext,"bitmap"); if (!diskCacheDir.exists()) { distCacheDir.mkdirs(); } mDiskLurCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
- 通過
-
DiskLruCache 的緩存添加
核心:通過 Editor 完成,Editor 表示一個緩存對象的編輯對象
-
步驟:
-
獲取圖片 url 對應的 key
public static String hashKeyFromUrl(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private static String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }
-
根據 key 通過 edit() 獲取 Editor 對象,得到輸出流,并再下載圖片時通過該輸出流寫入到文件系統,最后通過 commit() 提交。
注意:此部分應當通過 子線程 執行,避免下載圖片造成 ANR;String key = Util.hashKeyFromUrl(url); //得到DiskLruCache.Editor DiskLruCache.Editor editor = diskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(Util.IMG_URL, outputStream)) { publishProgress(""); //寫入緩存 editor.commit(); } else { //寫入失敗 editor.abort(); } }
-
關于下載圖片:
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (Exception e) { Log.e(TAG, "Error in downloadBitmap - " + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { } } return false; }
-
-
DiskLruCache 的緩存查找
-
將 url 轉換為 key,然后通過 DiskLruCache 的 get 方法得到一個 Snapshot 對象,接著通過 Snapshot.getInputStream() 即可得到緩存的文件輸入流,進而得到緩存圖片:
private Bitmap getCache() { try { String key = Util.hashKeyFromUrl(url); DiskLruCache.Snapshot snapshot = diskLruCache.get(key); if (snapshot != null) { InputStream in = snapshot.getInputStream(0); return BitmapFactory.decodeStream(in); } } catch (IOException e) { e.printStackTrace(); } return null; }
-
-
關于 FileInputStream 下的縮放
不適用 BitmapFactory.Options 縮放的方法:原因在于 兩次 decodeStream 調用影響了文件流的位置屬性,導致第二次 decodeStream 得到 null。
-
解決方法:文件描述符
Bitmap bitmap = null; String key = Util.hashKeyFromUrl(url); DiskLruCache.Snapshot snapshot = diskLruCache.get(key); if (snapshot != null) { FileInputStream in = (FileInputStream)snapshot.getInputStream(0); FileDescriptor fd = fildInputStream.getFD(); bitmap = mImageResizer.decodeSampleBitmapFromFileDecriptor(fd,reqWidth,reqHeight); if(bitmap != null){ addBitmapToMemoryCache(key, bitmap); } }
2.3 ImageLoader 的實現
優秀的 ImageLoader 應當具備的功能:
- 圖片的異步加載
- 圖片的同步加載
- 圖片壓縮:降低 OOM
- 內存緩存:核心,提高效率并降低流量消耗
- 磁盤緩存:核心,提高效率并降低流量消耗
- 網絡拉取:當兩種緩存都不可用時,通過網絡拉取圖片
代碼實現
圖片壓縮部分
public class ImageResizer {
/**
* 從資源文件中加載圖片。
* @param res
* @param resId
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
//第一個options設置為true,去加載圖片
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res,resId,options);
//計算采樣率
options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,resId,options);
}
/**
* 從內存卡中加載圖片
* @param fd
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampleBitmapFromFile(FileDescriptor fd,int reqWidth,int reqHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd,null,options);
}
/**
*計算圖片的采樣率
* 原理,如果設置的圖片寬、高小于原圖的寬、高。則inSampleSize呈2的指數縮小
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
private static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
if(reqHeight==0 || reqWidth ==0){
return 1;
}
//默認的采樣率
int inSampleSize = 1;
//獲取原圖的寬高
int width = options.outWidth;
int height = options.outHeight;
if(width > reqWidth || height > reqHeight){
int halfWidth = width / 2;
int halfHeight = height / 2;
while((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
ImageLoader 部分
public class ImageLoader {
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
//CPU 核心數
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//CPU 核心線程數
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
//線程超時時長
private static final long KEEP_ALIVE = 10L;
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
}
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(Runnable), sThreadFactory);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
private Context mContext;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
//無參構造方法,初始化 LruCache內存緩存 和 DiskLruCache磁盤緩存
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
//提供給外界的創建接口
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
//內存緩存 的 添加
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
//內存緩存 的 獲取
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
/**
* load bitmap from memory cache or disk or network async,then bind imageview and bitmap
* NOTE THAT: should run in UI thread
* 異步加載接口
*/
public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
}
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
//同步加載接口
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromDiskCache,url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error,DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFromUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
return bitmap;
}
//磁盤緩存 的 添加
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
//磁盤緩存 的 讀取,并 添加 到 內存緩存
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread,it is not recommended!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.SnapShot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
//將圖片寫入到本地文件中
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
//下載圖片
private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (IOException e) {
Log.e(TAG, "Error in downloadBitmap:" + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(in);
}
return bitmap;
}
//將 url 轉換為 key
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hasCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & byte[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBloackSize() * (long) stats.getAvailableBlocks();
}
private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}
3. ImageLoader 的使用
3.1 照片墻效果
-
想要實現寬高相等的 ImageView 時,自定義一個 ImageView 子類,并在 onMeasure 方法中將 heightMeasureSpec 替換為 widthMeasureSpec 即可。這樣做省時省力
@Override protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){ super.onMeasure(widthMeasureSpec,widthMeasureSpec); }
-
具體實現:
- ImageAdapter 的 getView 方法中,通過 ImageLoader.bindBitmap() 方法將圖片加載過程交給 ImageLoader
3.2 優化列表的卡頓現象
一些建議
- 不要再 getView 中執行耗時操作,比如直接加載圖片
- 控制異步任務的執行頻率;如果用戶可以頻繁上下滑動,會在一瞬間產生大量異步任務,會造成線程池的擁堵并帶來大量的 UI 更新操作。應當考慮在滑動時停止加載,列表停下來之后再加載圖片。
- 開啟硬件加速,通過設置 android:hardwareAccelerated="true"
大概就這么多吧。
本章結束。