天天看點

Android開發筆記(七十七)圖檔緩存算法ImageCachepicassoUniversal-Image-Loader

ImageCache

由于手機流量有限,又要加快app的運作效率,是以好的app都有做圖檔緩存。圖檔緩存說起來簡單,做起來就用到很多知識點,可算是集Android技術之大全了。隻要了解圖檔緩存的算法,并加以實踐把它做好,我覺得差不多可以懂半個Android的開發。

緩存政策

圖檔緩存一般分為三級,分别是記憶體、磁盤檔案與網絡圖檔。正常情況下,app會先到記憶體尋找圖檔,如果有找到,則直接顯示記憶體中的圖檔。如果記憶體沒找到,再到磁盤尋找,如果有找到,則讀取磁盤圖檔并顯示。如果磁盤也沒找到,就得根據uri去網絡下載下傳圖檔,下載下傳成功後顯示圖檔。經過三級的緩存,即使網速很慢或者斷網,app也能迅速加載部分圖檔,進而提高了使用者體驗。

記憶體緩存的資料結構可使用映射表HashMap,通過唯一的uri來定位圖像的Bitmap對象;排隊算法一般采用先進先出FIFO政策,考慮到FIFO需要對隊列兩端做操作,從隊列頂端移除溢出的圖像,把新增的圖像加到隊列末端,是以排隊的緩存采用雙端隊列LinkedList。映射表和雙端隊列的介紹參見《Android開發筆記(二十六)Java的容器類》,另外,為防止并發操作雙端隊列,引起不必要的資源沖突,在聲明相關方法時要加上synchronized關鍵字。

磁盤操作分兩塊,一塊是建立圖檔檔案的緩存目錄,首先檢查緩存目錄是否存在,不存在則先建立目錄;其次根據哈希值檢查圖檔檔案是否存在,存在則讀取圖像,不存在則跳到網絡處理;目錄與檔案的介紹參見《Android開發筆記(三十二)檔案基礎操作》。另一塊是從檔案中讀寫Bitmap對象,圖檔檔案的讀寫操作參見《Android開發筆記(三十三)文本檔案和圖檔檔案的讀寫》。

下載下傳政策

圖檔在記憶體和磁盤都找不到,那隻好到網絡上擷取圖檔了。根據http位址擷取圖檔,采用的是GET方式,具體編碼參見《Android開發筆記(六十三)HTTP通路的通信方式》。

由于通路網絡屬于異步操作,不能在主線程中直接處理,是以必須另外開線程,溝通異步方式的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開發筆記的完整目錄