BitMap高效顯示策略(二):在ListView上異步加載網絡圖片

在ListView上的item顯示網絡圖片,首先考慮的是2個因素

1.防止內存溢出

2.異步加載網絡圖片,防止ANR

針對以上2個問題,第一個問題解決方案是1.使用inSampleSize縮小圖片,2.且及時回收使用的Bitmap。第二個問題的解決方案是使用AsyncTask,下面討論這2個方法的具體實現,大部分代碼來自於Google的ApiDemo:DisplayingBitmapsSample,本人只是做了少量修改。

使用inSampleSize,參考這篇:BitMap高效顯示策略(一):大圖的縮放和加載

回收Bitmap:Bitmap的回收使用recycle()方法。爲了讓代碼能更好的複用,不建議在代碼中手動調用recycle,因爲Imageview通過BitmapDrawable去顯示Bitmap,所以可以定義一個BitmapDrawable的子類去實現這個回收邏輯

public class RecyclingBitmapDrawable extends BitmapDrawable {

    private int mCacheRefCount = 0;
    private int mDisplayRefCount = 0;

    private boolean mHasBeenDisplayed;

    public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }


    public void setIsDisplayed(boolean isDisplayed) {
        synchronized (this) {
            if (isDisplayed) {
                mDisplayRefCount++;
                mHasBeenDisplayed = true;
            } else {
                mDisplayRefCount--;
            }
        }

        checkState();
    }

    public void setIsCached(boolean isCached) {
        synchronized (this) {
            if (isCached) {
                mCacheRefCount++;
            } else {
                mCacheRefCount--;
            }
        }

        checkState();
    }

    private synchronized void checkState() {
        if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
                && hasValidBitmap()) {

            getBitmap().recycle();
        }
    }

    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }

}
RecyclingBitmapDrawable有3個屬性:mCacheRefCount記錄被內存緩存引用的次數,mDisplayRefCount記錄被imageview引用次數,mHasBeenDisplayed記錄當前是否是顯示狀態。setIsDisplayed和setIsCached方法根據布爾值參數設置引用計數,改變計數後,調用checkState方法,在checkState中,如果該Drawable沒有被引用或緩存,並且其中的Bitmap對象沒被回收的話,則調用其recycle方法。

這個RecyclingBitmapDrawable的setIsDisplayed方法在有ImageView調用setImageDrawable的時被調用,這個過程也不需要手動調用,可以通過實現ImageView的子類去實現。

public class RecyclingImageView extends ImageView {

    public RecyclingImageView(Context context) {
        super(context);
    }

    public RecyclingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDetachedFromWindow() {
        setImageDrawable(null);

        super.onDetachedFromWindow();
    }
    
    @Override
    public void setImageDrawable(Drawable drawable) {
        final Drawable previousDrawable = getDrawable();

        super.setImageDrawable(drawable);

        notifyDrawable(drawable, true);

        notifyDrawable(previousDrawable, false);
    }
    
    private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
        if (drawable instanceof RecyclingBitmapDrawable) {
            ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
        } else if (drawable instanceof LayerDrawable) {
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
                notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
            }
        }
    }
}
注意在重寫的setImageDrawable方法中,調用 notifyDrawable, 將上一個被顯示的drawable的引用計數-1,新的drawable引用+1。在Imageview被移除,onDetachedFromWindow被回調時,也對計數進行更新。

以上通過實現2個類,可以做到bitmap的recycle方法隱式調用,不用手動管理,只要在佈局文件中使用RecyclingImageView顯示圖片即可。


使用AsyncTask:

爲了避免ANR,應該避免在主線程中進行網絡下載/讀取文件的操作,ListView上Item下載圖片應使用AsyncTask。

在Adapter的getView中,爲每個ImageView去啓動一個AsyncTask用於下載圖片。

	class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> {
	    private final WeakReference imageViewReference;
	    private int mUrl = 0;

	    public BitmapWorkerTask(ImageView imageView,String url) {
	        imageViewReference = new WeakReference(imageView);
	        mUrl = url;
	    }

	    @Override
	    protected Bitmap doInBackground(Void... params) {
	        bitmap = processBitmap(mUrl); //子類實現
	        return bitmap;
	    }

	    @Override
	    protected void onPostExecute(Bitmap bitmap) {
	        if (imageViewReference != null && bitmap != null) {
	            final ImageView imageView = imageViewReference.get();
	            if (imageView != null) {
	                imageView.setImageBitmap(bitmap);
	            }
	        }
	    }
	}

爲ImageView使用WeakReference 確保了 AsyncTask 所引用的資源可以被GC。因爲當任務結束時不能確保 ImageView 仍然存在,因此你必須在 onPostExecute() 裏面去檢查引用。

以上的代碼,在ListView不進行上下滑動的時候,是沒問題的,但是如果在ListView進行滑動時,會出現問題:

爲了內存的優化,ListView/GridView等組件,在Item被滑動到屏幕上看不到的位置時,會被回收,用作下個新的可視Item,如果每個Item上的ImageView都啓動一個AsyncTask,則有可能出現當ListView向下划動時,新出現的Item是之前被回收的,那麼這個Item加載圖片的Url是上一個Item的Url,這樣會造成圖片顯示順序被打亂的情況。

針對這種情況的解決方法是:

定義一個AsyncDrawable繼承BitmapDrawable,作爲ImageView的佔位Drawable,在加載圖片的AsyncTask執行完畢後,在ImageView上顯示圖片。它保存一個最近使用的AsyncTask引用,加載圖片的AsyncTask中也保存ImageVIew的引用。

在加載圖片到指定的ImageView的任務開始前,判斷和ImageView關聯的,當前執行的任務是否之前已經在被執行(下面的cancelPotentialWork方法)。

在加載圖片的任務的doInBackgound和onPostExecute中,再次通過AsyncDrawable中引用的 AsyncTask 判斷當前的加載任務是否和之前的匹配(下面的getAttachedImageView方法)

代碼:

ImageWorker.java:異步加載網絡圖片的核心類

/***
 * 
 * ListView上每個item啓動一個AsyncTask去下載圖片,AsyncTask在容量爲2的線程池上運行。
 * 每個AsyncTask和item上的下載url和imageview綁定
 * 
 */
public abstract class ImageWorker {

	private boolean mExitTasksEarly = false; // 是否提前退出的標誌
	protected boolean mPauseWork = false;
	private final Object mPauseWorkLock = new Object();

	protected Resources mResources;

	protected ImageWorker(Context context) {
		mResources = context.getResources();
	}

	public void loadImage(String url, ImageView imageView) {
		if (url == null) {
			return;
		}

		if (cancelPotentialWork(url, imageView)) {

			final BitmapWorkerTask task = new BitmapWorkerTask(url, imageView);
			final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources,
					task);
			imageView.setImageDrawable(asyncDrawable);

			task.executeOnExecutor(ImageAsyncTask.DUAL_THREAD_EXECUTOR);
		}
	}

	/**
	 * 
	 * 
	 * 當item進行圖片下載時,這個item有可能是以前回收的item,此時item上的imageView還和一個task關聯着,
	 * 檢查當前的task和imageview綁定的以前的task是不是在下載同一個圖片, 不匹配則終止以前運行的task,否則繼續之前的下載
	 * 
	 * @param data
	 * @param imageView
	 * @return true表示需要重新下載
	 */
	public static boolean cancelPotentialWork(String currentUrl,
			ImageView imageView) {
		BitmapWorkerTask oldBitmapWorkerTask = getBitmapWorkerTask(imageView);
		if (oldBitmapWorkerTask != null) {
			String oldUrl = oldBitmapWorkerTask.mUrl;
			if (oldUrl == null || !oldUrl.equals(currentUrl)) {
				oldBitmapWorkerTask.cancel(true);
			} else {
				return false;
			}
		}

		return true;
	}

	/***
	 * 下載圖片的task。 ListView上Item的ImageView被回收重用後,綁定的下載圖片task可能和之前的不一樣,
	 * Item回收可能發生在:task執行中,task執行後。這2種情況都需要校驗,通過{@link #getAttachedImageView()}
	 * 方法
	 * 
	 */
	private class BitmapWorkerTask extends
			ImageAsyncTask<Void, Void, BitmapDrawable> {
		private String mUrl;
		private final WeakReference<ImageView> imageViewReference;

		public BitmapWorkerTask(String url, ImageView imageView) {
			mUrl = url;
			imageViewReference = new WeakReference<ImageView>(imageView);
		}

		@Override
		protected BitmapDrawable doInBackground(Void... params) {

			Bitmap bitmap = null;
			BitmapDrawable drawable = null;

			synchronized (mPauseWorkLock) {
				while (mPauseWork && !isCancelled()) {
					try {
						mPauseWorkLock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}

			if (bitmap == null && !isCancelled()
					&& getAttachedImageView() != null && !mExitTasksEarly) {
				bitmap = processBitmap(mUrl);
			}

			if (bitmap != null) {
				drawable = new BitmapDrawable(mResources, bitmap);
			}

			return drawable;
		}

		@Override
		protected void onPostExecute(BitmapDrawable result) {
			if (isCancelled() || mExitTasksEarly) {
				result = null;
			}

			ImageView imageView = getAttachedImageView();
			if (result != null && imageView != null) {

				setImageDrawable(imageView, result);
			}
		}

		@Override
		protected void onCancelled(BitmapDrawable value) {
			super.onCancelled(value);
			synchronized (mPauseWorkLock) {
				mPauseWorkLock.notifyAll();
			}
		}

		/***
		 * 返回當前綁定task的ImageView,如果ImageView綁定的task不是自己,則返回null
		 * 
		 * @return
		 */
		private ImageView getAttachedImageView() {
			ImageView imageView = imageViewReference.get();
			BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

			if (this == bitmapWorkerTask) {
				return imageView;
			}

			return null;
		}
	}

	/***
	 * 圖片下載完畢後設置ImageView
	 * 
	 * @param imageView
	 * @param drawable
	 */
	private void setImageDrawable(ImageView imageView, Drawable drawable) {
		imageView.setImageDrawable(drawable);
	}

	/***
	 * 返回當前ImageView綁定的task
	 * 
	 * @param imageView
	 * @return
	 */
	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
		if (imageView != null) {
			Drawable drawable = imageView.getDrawable();
			if (drawable instanceof AsyncDrawable) {
				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
				return asyncDrawable.getBitmapWorkerTask();
			}
		}

		return null;
	}

	private static class AsyncDrawable extends BitmapDrawable {
		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

		public AsyncDrawable(Resources res, BitmapWorkerTask bitmapWorkerTask) {
			super(res);
			bitmapWorkerTaskReference = new WeakReference<ImageWorker.BitmapWorkerTask>(
					bitmapWorkerTask);

		}

		public BitmapWorkerTask getBitmapWorkerTask() {
			return bitmapWorkerTaskReference.get();
		}
	}

	public void setPauseWork(boolean pauseWork) {
		synchronized (mPauseWorkLock) {
			mPauseWork = pauseWork;
			if (!mPauseWork) {
				mPauseWorkLock.notifyAll();
			}
		}
	}

	public void setExitTasksEarly(boolean exitTasksEarly) {
		mExitTasksEarly = exitTasksEarly;
		setPauseWork(false);
	}

	/***
	 * 下載圖片的代碼由子類實現
	 * 
	 * @param url
	 * @return
	 */
	protected abstract Bitmap processBitmap(String url);

}

cancelPotentialWork這個方法的作用是:BitmapWorkerTask開始前,檢查當前要下載的圖片(Url)是否和之前的一樣,如果不一樣,取消之前的任務,否則,繼續執行之前的任務。

getAttachedImageView:返回當前綁定task的ImageView,如果ImageView中引用的task不是自己,則返回null

它們都調用了 getBitmapWorkerTask 方法。getBitmapWorkerTask獲得與這個ImageView綁定的BitmapWorkerTask,用於和當前的BitmapWorkerTask比對

ImageResizer:繼承ImageWorker,處理圖片縮放。

這樣相當於在  BitmapWorkerTask開始前,執行中,執行後都做了檢查,所以避免了在這3個階段的某個階段中,ListView回收Item導致圖片順序打亂的問題。

public class ImageResizer extends ImageWorker{
	
	private static final String TAG = "ImageResizer";
	private int mImageWidth;
	private int mImageHeight;

	public ImageResizer(Context context, int imageWidth, int imageHeight) {
        super(context);
        setImageSize(imageWidth, imageHeight);
    }
	
	public ImageResizer(Context context, int imageSize) {
        super(context);
        setImageSize(imageSize, imageSize);
    }
	
	protected ImageResizer(Context context) {
		super(context);
	}
	
	public void setImageSize(int width, int height) {
		mImageWidth = width;
		mImageHeight = height;
	}

	public int getmImageWidth() {
		return mImageWidth;
	}

	public int getmImageHeight() {
		return mImageHeight;
	}

	@Override
	protected Bitmap processBitmap(String url) {
		return null;
	}
	
	public static BitmapFactory.Options getSampledBitmapOptionsFromStream(
			InputStream is, int reqWidth, int reqHeight) {
		final BitmapFactory.Options options = new BitmapFactory.Options();
		options.inJustDecodeBounds = true;
		BitmapFactory.decodeStream(is, null, options);
		// 計算縮放比例
		options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
		options.inJustDecodeBounds = false;
		
		return options;
	}
	
	public static Bitmap decodeSampledBitmapFromStream(
			InputStream is, BitmapFactory.Options options) {
		
		return BitmapFactory.decodeStream(is, null, options);
	}
	
    /** 
    * @Description: 計算圖片縮放比例
    * @param @param options
    * @param @param reqWidth
    * @param @param reqHeight
    * @param @return    
    * @return int 縮小的比例
    * @throws 
    */
    public static int calculateInSampleSize(BitmapFactory.Options options,
            int reqWidth, int reqHeight) {
    	
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}
ImageDownloader:繼承ImageResizer,實現父類的processBitmap

public class ImageDownloader extends ImageResizer {

	public ImageDownloader(Context context) {
		super(context);
	}
	
	public ImageDownloader(Context context, int imageSize) {
		super(context, imageSize);
	}

	@Override
	protected Bitmap processBitmap(String url) {
		
		return downLoadByUrl(url);
	}
	
    public Bitmap downLoadByUrl(String urlString) {
        disableConnectionReuseIfNecessary();
        HttpURLConnection urlConnection = null;
        InputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = urlConnection.getInputStream();
            final BitmapFactory.Options options = 
            		getSampledBitmapOptionsFromStream(in, getmImageWidth(), getmImageHeight());
            in.close();
            urlConnection.disconnect();
            //重新獲得輸入流
            urlConnection = (HttpURLConnection) url.openConnection();
            in = urlConnection.getInputStream();
            
            Bitmap bitmap = decodeSampledBitmapFromStream(in, options);
            
            return bitmap;
            
        } catch (final IOException e) {
            Log.e("text", "Error in downloadBitmap - " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {}
        }
		return null;
    }
	
    public static void disableConnectionReuseIfNecessary() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
            System.setProperty("http.keepAlive", "false");
        }
    }
}

ImageGridFragment:

public class ImageGridFragment extends Fragment {
    private static final String TAG = "ImageGridFragment";
    private ImageWorker mImageWorker;
    private ImageAdapter mAdapter;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	
    	mImageWorker = new ImageFetcher(getActivity(), 50);
    	
    }
    
    @Override
    public View onCreateView(LayoutInflater inflater,
    		@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    	
    	View v = inflater.inflate(R.layout.list, container, false);
    	ListView listView = (ListView) v.findViewById(R.id.image_list);
    	mAdapter = new ImageAdapter(getActivity());
    	listView.setAdapter(mAdapter);
    	
    	listView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
            	
                if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
                    if (Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
                    	mImageWorker.setPauseWork(true);
                    }
                } else {
                	mImageWorker.setPauseWork(false);
                }
            }

            @Override
            public void onScroll(AbsListView absListView, int firstVisibleItem,
                    int visibleItemCount, int totalItemCount) {
            }
        });
    	
    	return v;
    }
    
    @Override
    public void onResume() {
        super.onResume();
        mImageWorker.setExitTasksEarly(false);
        mAdapter.notifyDataSetChanged();
    }

    @Override
    public void onPause() {
        super.onPause();
        mImageWorker.setPauseWork(false);
        mImageWorker.setExitTasksEarly(true);
    }
    
    
    private class ImageAdapter extends BaseAdapter {
    	private final Context mContext;
    	private LayoutInflater mInflater;
    	
    	public ImageAdapter(Context context) {
    		this.mContext = context;
    		this.mInflater = LayoutInflater.from(mContext);
    	}
    	
		@Override
		public int getCount() {
			return Images.imageThumbUrls.length;
		}

		@Override
		public Object getItem(int position) {
			return null;
		}

		@Override
		public long getItemId(int position) {
			return 0;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ImageView imageView;
			
			if (convertView == null) {
				convertView = mInflater.inflate(R.layout.list_item, parent, false);
			}
			
			imageView = (RecyclingImageView) convertView.findViewById(R.id.img);
			//參數:url,imageview
			mImageWorker.loadImage(Images.imageThumbUrls[position], imageView);
			
			return convertView;
		}
    }
}
MainActivity:

public class MainActivity extends ActionBarActivity {

	private static final String TAG = "ImageGridActivity";
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
    	
        if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
            final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            ft.add(android.R.id.content, new ImageGridFragment(), TAG);
            ft.commit();
        }
        
    }
}

佈局:

list.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    
    <ListView 
        android:id="@+id/image_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        ></ListView>

</LinearLayout>
list_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    
	<com.example.displayingbitamapdemov1.image.RecyclingImageView
	    android:id="@+id/img"
	    android:layout_width="60dp"
	    android:layout_height="60dp"
	    android:background="@drawable/ic_launcher"
	    android:scaleType="centerCrop"
	    />
</LinearLayout>


效果:


Demo下載地址:

http://download.csdn.net/detail/ohehehou/8110965


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章