最近在練習中用GridView加入相冊中圖檔發現加入大量的相片之後,GirdView會變得很卡,想到或許可以用異步加載的方式來解決,但是能力有限,想得到卻無法實作。在讀了一些大牛的部落格和代碼之後,終于實作了。
1 在異步加載之前的代碼的和普通加載代碼一樣,隻需要在GirdView的Adapter的public View getView(int position, View convertView, ViewGroupparent)方法使用異步加載的方式傳回ImageView。
2 如果能把加載過的圖檔給緩存起來,而不用每次都從sd卡上讀取,這樣效率應該會提高不少。是以可以先建一個緩存類,MemoryCache,為了能盡可能緩存,又盡可能的不抛出OOM的異常,可以使用SoftReference<Bitmap>,軟引用來緩沖圖檔。MemoryCache類的代碼如下:
/**
* 緩存類(圖檔的路徑和圖檔)
*/
public class MemoryCache {
private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();
//擷取緩存圖檔
public Bitmap get(String path) {
if(cache.get(path)==null){
return null;
}else{
return cache.get(path).get();
}
}
//新增緩存
public void put(String url,Bitmap bitmap) {
cache.put(url, new SoftReference<Bitmap>(bitmap));
}
//清空緩存
public void clear() {
cache.clear();
}
}
3 弄完了緩存類,接下來就可以弄異步加載的類,這個類主要在維護一個Stack,每次向這個類請求一個ImageView時,就要傳入對應ImageView和圖檔的路徑。異步類首先會根據圖檔的路徑先去緩存中查找是否有緩存對應的BItMap,如果有就把他放到ImageView傳回,如果沒有就把這個ImageView和圖檔路徑放到Stack中,并喚醒加載圖檔的線程。而加載圖檔的線程(線程優先權低于UI線程),會無限循環檢視Stack大小,如果為0,就進入等待。如果不為0,就依次出棧Stack中的元素進行處理。(感覺像生産者-消費者模式)。
3.1 接下來,就寫這個異步類的變量和構造函數了:
private MemoryCache cache;
private PhotosStack photosStack;
private LoadImageThread loadImageThread;
//用來儲存每個月ImageView和對應應加載的圖檔
private Map<ImageView, String> viewPath=Collections.synchronizedMap(new HashMap<ImageView, String>());
public LoadImage(Activity activity) {
super();
cache=new MemoryCache();
photosStack=new PhotosStack();
loadImageThread=new LoadImageThread(activity);
//設定異步加載線程優先權低于UI線程
loadImageThread.setPriority(Thread.NORM_PRIORITY);
}
其中MemoryCache是緩存類,PhotosStack類中維護一個Stack,LoadImageThread類是負責異步加載圖檔的。而viewPath這個變量是為了要保證GridView對應Position放對應的ImageView,如果沒有這個變量,GridView中排列就會無序,在處理GridView的點選事件時候,就不好處理。而對viewPath的操作的異步,是以就需要線程安全咯。
PhotosStack代碼如下:
//維護需要被填充的Image和對應圖檔路徑的棧
class PhotosStack{
Stack<MyPhoto> stack=new Stack<LoadImage.MyPhoto>();
//移除指定ImageView
public void remove(ImageView iView) {
for(int i=0;i<stack.size();i++){
if(stack.get(i).imageView==iView){
stack.remove(i);
break;
}
}
}
}
其中MyPhoto是一個照片的實體類,有兩個屬性,圖檔的路徑和對應的ImageView。
/**
* 照片的實體類
*/
class MyPhoto{
public String path;
public ImageView imageView;
public MyPhoto(String path, ImageView imageView){
this.path=path;
this.imageView=imageView;
}
}
3.2 接下來就可以實作給Adapter的調用的方法loadImage(Stringpath,ImageView iv ),接收兩參數——圖檔路徑和對應的ImageView。這個方法,首先會去緩存中檢視是否有緩存對應的圖檔,如果有就把它設給ImageView。如果沒有,就先為ImageView設個預設圖檔,然後以同步塊(鎖為PhotosStack中的stack)的方式加入PhotosStack中的stack中,并喚醒加載圖檔的線程。最後還要判斷下加載圖檔的線程是否已經被啟動了,如果沒有,就啟動。
/**
* 填充ImageView
* @param activity
* @param path 圖檔的路徑
* @param iv 被填充的ImageView
*/
public void loadImage(String path,ImageView iv ) {
viewPath.put(iv, path);
Bitmap momeryBitmap=cache.get(path);
if(momeryBitmap!=null){//有緩存這張圖檔
iv.setImageBitmap(momeryBitmap);
}else{//沒有緩存
iv.setImageResource(R.drawable.ic_launcher);
//有可以這個ImageVIew還沒出棧,是以需要先出棧
photosStack.remove(iv);
//由于photosStack中的stack,出棧入棧操作時不會在異步的,是以需要在同步塊中完成出入棧,并以photosStack.stack作為鎖
synchronized (photosStack.stack) {
photosStack.stack.push(new MyPhoto(path, iv));
//喚醒持有鎖的線程
photosStack.stack.notifyAll();
}
}
if(loadImageThread.getState()==Thread.State.NEW){
loadImageThread.start();
}
}
3.3 最後可以實作異步加載圖檔的方法了,主要是循環判斷stack中元素的數量,如果為0 ,說明所有的圖檔已經被加載完畢了,可以進入等待狀态。如果不為0,說明還有圖檔等待加載,就依次出棧這些元素,依次加載圖檔,并放到緩存中。代碼如下:
//異步加載圖檔的線程
class LoadImageThread extends Thread{
Activity activity;
public LoadImageThread(Activity activity) {
this.activity = activity;
}
@Override
public void run() {
while(true){
try {
if(photosStack.stack.size()>0){
final MyPhoto photo;
//得到棧頂的MyPhoto
synchronized (photosStack.stack) {
photo= photosStack.stack.pop();
}
cache.put(photo.path,getSmallBitmap(photo.path) );
String ivPathString=viewPath.get(photo.imageView);
if(ivPathString!=null&& ivPathString.equalsIgnoreCase(photo.path)){
Runnable runableRunnable=new Runnable() {
@Override
public void run() {
photo.imageView.setImageBitmap(getSmallBitmap(photo.path));
}
};
activity.runOnUiThread(runableRunnable);
}
}
if(photosStack.stack.size()==0){
synchronized (photosStack.stack) {
photosStack.stack.wait();
}
}
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
}
}
}
其中縮小圖檔方法getSmallBitmap(String path),代碼如下:
//縮小圖檔
public Bitmap getSmallBitmap(String path) {
Options options=new Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFile(path,options);
int REQUIRE_SIZE=80;
int scare=1;
while(true){
if(options.outWidth<=REQUIRE_SIZE|| options.outHeight<=REQUIRE_SIZE){
break;
}else{
options.outWidth=options.outWidth/2;
options.outHeight=options.outHeight/2;
scare++;
}
}
Options newoptions=new Options();
newoptions.inSampleSize=scare;
return BitmapFactory.decodeFile(path, newoptions);
}
其實這個也适合ListView加載大量圖檔,可以下載下傳代碼。