Android開發筆記(七十七)圖片快取演算法
阿新 • • 發佈:2019-01-10
ImageCache
由於手機流量有限,又要加快app的執行效率,因此好的app都有做圖片快取。圖片快取說起來簡單,做起來就用到很多知識點,可算是集Android技術之大全了。只要理解圖片快取的演算法,並加以實踐把它做好,我覺得差不多可以懂半個Android的開發。快取策略
圖片快取一般分為三級,分別是記憶體、磁碟檔案與網路圖片。正常情況下,app會先到記憶體尋找圖片,如果有找到,則直接顯示記憶體中的圖片。如果記憶體沒找到,再到磁碟尋找,如果有找到,則讀取磁碟圖片並顯示。如果磁碟也沒找到,就得根據uri去網路下載圖片,下載成功後顯示圖片。經過三級的快取,即使網速很慢或者斷網,app也能迅速載入部分圖片,從而提高了使用者體驗。記憶體快取的資料結構可使用對映表HashMap,通過唯一的uri來定點陣圖像的Bitmap物件;排隊演算法一般採用先進先出FIFO策略,考慮到FIFO需要對佇列兩端做操作,從佇列頂端移除溢位的影象,把新增的影象加到佇列末端,所以排隊的快取採用雙端佇列LinkedList。對映表和雙端佇列的介紹參見《
磁碟操作分兩塊,一塊是建立圖片檔案的快取目錄,首先檢查快取目錄是否存在,不存在則先建立目錄;其次根據雜湊值檢查圖片檔案是否存在,存在則讀取影象,不存在則跳到網路處理;目錄與檔案的介紹參見《Android開發筆記(三十二)檔案基礎操作》。另一塊是從檔案中讀寫Bitmap物件,圖片檔案的讀寫操作參見《Android開發筆記(三十三)文字檔案和圖片檔案的讀寫》。
下載策略
圖片在記憶體和磁碟都找不到,那隻好到網路上獲取圖片了。根據http地址獲取圖片,採用的是GET方式,具體編碼參見《由於訪問網路屬於非同步操作,不能在主執行緒中直接處理,因此必須另外開執行緒,溝通非同步方式的Handler介紹參見《Android開發筆記(四十八)Thread類實現多執行緒》。另外,考慮到圖片快取可能同時訪問多張圖片,所以為提高效率要引入執行緒池,由執行緒池物件統一管理圖片下載任務,執行緒池的介紹參見《Android開發筆記(七十六)執行緒池管理》。
顯示策略及相關優化
歷經千辛萬苦,終於把圖片從三級快取中找出來了,現在要在ImageView控制元件上顯示圖片,通常會使用淡入淡出動畫效果,不至於很突兀,淡入淡出動畫的用法參見《Android開發筆記(十五)淡入淡出動畫另外,為提高使用者體驗,經常在圖片載入之前,就在原圖位置先放一張佔位圖片;如果圖片載入失敗,也在原圖位置提示錯誤圖片或者預設圖片;這些佔位圖片和錯誤圖片可在配置快取資訊時進行設定。
圖片快取在提高效能的同時,不要忘記預防記憶體洩漏。因為Handler物件和Bitmap物件都存在記憶體洩漏的風險,所以我們要及時釋放Handler物件的引用,並及時回收Bitmap物件的資料,具體優化處理參見《Android開發筆記(七十五)記憶體洩漏的處理》。
程式碼示例
下面是圖片快取的一個簡單實現程式碼例子:import java.io.File;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
public class ImageCache {
private final static String TAG = "ImageCache";
//記憶體中的圖片快取
private HashMap<String, Bitmap> mImageMap = new HashMap<String, Bitmap>();
//uri與檢視控制元件的對映關係
private HashMap<String, ImageView> mViewMap = new HashMap<String, ImageView>();
//快取佇列,採用FIFO先進先出策略,需操作佇列首尾兩端,故採用雙端佇列
private LinkedList<String> mUriList = new LinkedList<String>();
private ImageCacheConfig mConfig;
private String mDir = "";
private ThreadPoolExecutor mPool;
private static Handler mMyHandler;
private static ImageCache mCache = null;
private static Context mContext;
public static ImageCache getInstance(Context context) {
if (mCache == null) {
mCache = new ImageCache();
mCache.mContext = context;
}
return mCache;
}
public ImageCache initConfig(ImageCacheConfig config) {
mCache.mConfig = config;
mCache.mDir = mCache.mConfig.mDir;
if (mCache.mDir==null || mCache.mDir.length()<=0) {
mCache.mDir = Environment.getExternalStorageDirectory() + "/image_cache";
}
Log.d(TAG, "mDir="+mCache.mDir);
//若目錄不存在,則先建立新目錄
File dir = new File(mCache.mDir);
if (dir.exists() != true) {
dir.mkdirs();
}
mCache.mPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(mCache.mConfig.mThreadCount);
mCache.mMyHandler = new MyHandler((Activity)mCache.mContext);
return mCache;
}
public void show(String uri, ImageView iv) {
if (mConfig.mBeginImage != 0) {
iv.setImageResource(mConfig.mBeginImage);
}
mViewMap.put(uri, iv);
if (mImageMap.containsKey(uri) == true) {
mCache.render(uri, mImageMap.get(uri));
} else {
String path = getFilePath(uri);
if ((new File(path)).exists() == true) {
Bitmap bitmap = ImageUtil.openBitmap(path);
if (bitmap != null) {
mCache.render(uri, bitmap);
} else {
mPool.execute(new MyRunnable(uri));
}
} else {
mPool.execute(new MyRunnable(uri));
}
}
}
private String getFilePath(String uri) {
String file_path = String.format("%s/%d.jpg", mDir, uri.hashCode());
return file_path;
}
private static class MyHandler extends Handler {
public static WeakReference<Activity> mActivity;
public MyHandler(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity act = mActivity.get();
if (act != null) {
ImageData data = (ImageData) (msg.obj);
if (data!=null && data.bitmap!=null) {
mCache.render(data.uri, data.bitmap);
} else {
mCache.showError(data.uri);
}
}
}
}
private class MyRunnable implements Runnable {
private String mUri;
public MyRunnable(String uri) {
mUri = uri;
}
@Override
public void run() {
Activity act = MyHandler.mActivity.get();
if (act != null) {
Bitmap bitmap = ImageHttp.getImage(mUri);
if (bitmap != null) {
if (mConfig.mSize != null) {
bitmap = Bitmap.createScaledBitmap(bitmap, mConfig.mSize.x, mConfig.mSize.y, false);
}
ImageUtil.saveBitmap(getFilePath(mUri), bitmap);
}
ImageData data = new ImageData(mUri, bitmap);
Message msg = mMyHandler.obtainMessage();
msg.obj = data;
mMyHandler.sendMessage(msg);
}
}
};
private void render(String uri, Bitmap bitmap) {
ImageView iv = mViewMap.get(uri);
if (mConfig.mFadeInterval <= 0) {
iv.setImageBitmap(bitmap);
} else {
//記憶體中已有圖片的就直接顯示,不再展示淡入淡出動畫
if (mImageMap.containsKey(uri) == true) {
iv.setImageBitmap(bitmap);
} else {
iv.setAlpha(0.0f);
AlphaAnimation alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
alphaAnimation.setDuration(mConfig.mFadeInterval);
alphaAnimation.setFillAfter(true);
iv.setImageBitmap(bitmap);
iv.setAlpha(1.0f);
iv.setAnimation(alphaAnimation);
alphaAnimation.start();
mCache.refreshList(uri, bitmap);
}
}
}
private synchronized void refreshList(String uri, Bitmap bitmap) {
if (mUriList.size() >= mConfig.mMemoryFileCount) {
String out_uri = mUriList.pollFirst();
mImageMap.remove(out_uri);
}
mImageMap.put(uri, bitmap);
mUriList.addLast(uri);
}
private void showError(String uri) {
ImageView iv = mViewMap.get(uri);
if (mConfig.mErrorImage != 0) {
iv.setImageResource(mConfig.mErrorImage);
}
}
public void clear() {
for (Map.Entry<String, Bitmap> item_map : mImageMap.entrySet()) {
Bitmap bitmap = item_map.getValue();
bitmap.recycle();
}
mCache = null;
}
}
下面是該快取的呼叫程式碼示例:
ImageCacheConfig config = new ImageCacheConfig.Builder()
.setBeginImage(R.drawable.bliss)
.setErrorImage(R.drawable.error)
.setFadeInterval(2000)
.build();
ImageCache.getInstance(this).initConfig(config).show(file, iv_hello);
picasso
picasso是Square公司開源的一個Android圖片快取庫,使用相對簡單,一般只需一句程式碼即可下載圖片並顯示到檢視。Picasso
Picasso是主要的處理類,常用方法如下:with : 靜態方法。初始化一個預設例項。
setSingletonInstance : 靜態方法。指定已設定好的例項。
setIndicatorsEnabled : 設定標誌是否可用。其實就是開發模式,會在圖片左上角顯示三角標誌,綠色表示圖片取自記憶體,藍色表示取自磁碟,紅色表示取自網路。
setLoggingEnabled : 設定日誌是否可用。
load : 從指定位置載入圖片。該方法返回一個RequestCreator物件,供後續處理使用。
cancelRequest : 取消指定控制元件的圖片載入請求。
shutdown : 關閉Picasso。
RequestCreator
RequestCreator物件來源於Picasso的load方法,主要處理圖片的展示操作,常用方法如下:placeholder : 指定圖片載入前的佔位圖片。
error : 指定圖片載入失敗的佔位圖片。
resize : 指定圖片縮放的尺寸。
centerCrop : 指定圖片居中時裁剪。
centerInside : 指定圖片在內部居中。
rotate : 指定圖片的旋轉角度。
config : 指定圖片的色彩模式。
noFade : 指定不顯示淡入淡出動畫。預設有顯示動畫。
into : 指定圖片顯示的控制元件。
設定快取目錄
picasso除了能載入網路圖片,還能載入資源圖片(包括assets和drawable)。另外,若想自定義picasso的圖片快取目錄,可按如下方式進行設定: private void setImageCacheDir() {
String imageCacheDir = Environment.getExternalStorageDirectory() + "/picasso_image";
tv_hello.setText(imageCacheDir);
Picasso picasso = new Picasso.Builder(this).downloader(
new OkHttpDownloader(new File(imageCacheDir))).build();
Picasso.setSingletonInstance(picasso);
}
需要注意的是,picasso依賴於okhttp,而okhttp又依賴於okio,所以若想使用picasso的全部功能(比如自定義快取目錄時用到OkHttpDownloader),需要同時匯入picasso、okhttp、okio三個jar包。
程式碼示例
下面是picasso幾個常用場景下的程式碼例子://簡單載入
Picasso.with(this).load(url).into(iv_hello);
//縮放載入
Picasso.with(this).load(url).resize(512, 384).centerCrop().into(iv_hello);
//佔位載入
Picasso.with(this).load(url).placeholder(R.drawable.bliss).error(R.drawable.error).into(iv_hello);
Universal-Image-Loader
Universal-Image-Loader是個廣泛應用的圖片載入框架,它的功能比Picasso更豐富,當然用起來也會複雜一些。ImageLoader
Universal把快取圖片分為兩個過程:Load載入、Display顯示。載入資訊由ImageLoaderConfiguration類處理,顯示資訊由DisplayImageOptions類處理,最後再由ImageLoader統一設定和顯示。ImageLoader的常用方法如下:getInstance : 靜態方法。獲取ImageLoader的例項。
init : 初始化載入資訊。
displayImage : 在指定控制元件ImageView上顯示圖片,同時指定顯示資訊。
cancelDisplayTask : 取消指定控制元件上的圖片顯示任務。
loadImage : 在指定控制元件ImageView上載入圖片,可設定圖片載入的監聽器(包括開始載入onLoadingStarted、取消載入onLoadingCancelled、載入完成onLoadingComplete、載入失敗onLoadingFailed四個方法)。
ImageLoaderConfiguration
載入資訊的設定採用了建造者模式,主要指定執行緒、記憶體、磁碟的相關處理,詳細的方法使用舉例如下: File imageCacheDir = new File(Environment.getExternalStorageDirectory() + "/universal_image");
ImageLoaderConfiguration config = new ImageLoaderConfiguration
.Builder(this)
.threadPoolSize(3) //執行緒池內載入的數量
.threadPriority(Thread.NORM_PRIORITY - 2) //設定當前執行緒的優先順序
.denyCacheImageMultipleSizesInMemory() //拒絕快取同一圖片的多個尺寸版本
// .taskExecutor(new Executor() {
// @Override
// public void execute(Runnable command) {
// }
// }) //設定圖片載入的任務,如無必要不必重寫
// .taskExecutorForCachedImages(new Executor() {
// @Override
// public void execute(Runnable command) {
// }
// }) //設定已快取的圖片的載入任務,如無必要不必重寫
.tasksProcessingOrder(QueueProcessingType.FIFO) //佇列的排隊演算法,預設FIFO。FIFO表示先進先出,LIFO表示後進先出
.memoryCache(new UsingFreqLimitedMemoryCache(2 * 1024 * 1024)) //你可以通過自己的記憶體快取實現
.memoryCacheSize(2 * 1024 * 1024) //使用的記憶體大小
.memoryCacheSizePercentage(13) //使用的記憶體百分比
.memoryCacheExtraOptions(480, 800) //設定記憶體中圖片的長寬
.diskCache(new UnlimitedDiskCache(imageCacheDir)) //自定義磁碟的路徑
.diskCacheSize(50 * 1024 * 1024) //使用的磁碟大小
.diskCacheFileCount(100) //磁碟的檔案數量上限
.diskCacheFileNameGenerator(new Md5FileNameGenerator())//設定磁碟檔名的命名模式,預設雜湊。HashCodeFileNameGenerator表示採用雜湊演算法,Md5FileNameGenerator表示採用MD5演算法
.diskCacheExtraOptions(480, 800, new BitmapProcessor() {
@Override
public Bitmap process(Bitmap bitmap) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return BitmapFactory.decodeStream(bais, null, null);
}} ) //設定磁碟中圖片的長寬,需自定義壓縮倍率
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) //顯示圖片的選項,預設createSimple
.imageDownloader(new BaseImageDownloader(this, 5 * 1000, 30 * 1000)) //下載圖片的超時設定,預設連線超時5秒,讀取超時20秒。第二個引數表示連線超時,第三個引數表示讀取超時
.imageDecoder(new BaseImageDecoder(false)) //對圖片解碼,如需縮放或旋轉可在此處理
.writeDebugLogs() //列印除錯日誌。上線時需要去掉該方法
.build(); //開始構建配置
DisplayImageOptions
顯示資訊主要指定顯示模式與佔位圖片,可用於ImageLoader的displayImage和loadImage方法,以及ImageLoaderConfiguration的defaultDisplayImageOptions方法。詳細的方法使用舉例如下: DisplayImageOptions options = new DisplayImageOptions.Builder()
.cacheInMemory(true) //設定是否在記憶體中快取,預設為false
.cacheOnDisk(true) //設定是否在磁碟中快取,預設為false
.resetViewBeforeLoading(false) //設定是否在載入前重置檢視,預設為false
.displayer(new FadeInBitmapDisplayer(3000)) //設定淡入淡出的時間間隔
.imageScaleType(ImageScaleType.EXACTLY) //設定縮放型別
.bitmapConfig(Bitmap.Config.ARGB_8888) //設定影象的色彩模式
.showImageOnLoading(R.drawable.bliss) //設定圖片在下載期間顯示的圖片
.showImageForEmptyUri(R.drawable.error)//設定圖片Uri為空或是錯誤的時候顯示的圖片
.showImageOnFail(R.drawable.error) //設定圖片載入/解碼過程中錯誤時候顯示的圖片
.build(); //開始構建配置
載入資源圖片
除了載入網路圖片,Universal也支援載入資源類圖片,包括ContentProvider、assets和drawable三種資源圖片。具體方法如下:1、載入ContentProvider圖片
String contentUrl = "content://media/external/audio/albumart/13";
2、載入assets圖片String assetsUrl = Scheme.ASSETS.wrap("image.png");
3、載入drawable圖片String drawableUrl = Scheme.DRAWABLE.wrap(""+R.drawable.image);
特別注意drawable的載入方式,網上很多人轉的都是Scheme.DRAWABLE.wrap("R.drawable.image"),但這種寫法是有問題的,執行的時候會報錯“java.lang.NumberFormatException: Invalid int: "R.drawable.image"”。看來學習光看是不行的,人云亦云最容易犯錯,還是自己動手跑跑看,才知道這樣做行不行。
程式碼示例
下面是Universal-Image-Loader幾個常用場景下的程式碼例子: //簡單載入
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
mLoader.init(config);
DisplayImageOptions options = new DisplayImageOptions.Builder()
.displayer(new FadeInBitmapDisplayer(3000)) //設定淡入淡出的時間間隔
.build();
mLoader.displayImage(url, iv_hello, options);
//帶監聽器的載入
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
mLoader.init(config);
ImageSize size = new ImageSize(512, 384);
mLoader.loadImage(url, size, new SimpleImageLoadingListener(){
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
iv_hello.setImageBitmap(loadedImage);
}
});
點此檢視Android開發筆記的完整目錄