本章節討論的是安卓圖片加載的基礎部分,主要依據谷歌官方培訓教程中的代碼和方法,使用比較廣泛的圖片加載框架,很多都是要依據安卓的基礎方法,在此基礎上進行封裝,如果直接看封裝過得框架,以來難度會有點大,二來在基礎方法不是太了解的情況下看大的完整框架會有些耗費時間,所以還是讓我們來一起分析一下如何用安卓官方給出的基礎方法來封裝一個簡單圖像加載框架。
因為谷歌在中國建立了針對開發者的開發文檔服務器,各位安卓開發人員想要學習谷歌官方的安卓文檔已經不需要再翻墻了。雖然部分文檔還是英文版的,但是最起碼我們可以快速方便的打開了,地址:谷歌官方安卓開發文檔
以下相關源代碼和理論分析主要來自于:安卓圖像之高效顯示Bitmap
一、高效加載大圖:首先,在應用開發中在顯示圖片時,往往很多原圖是比較大的,而手機設備是不需要在內存中加載一個原圖大小的大圖的,這就需要加載一個符合手機需要大小的圖片,以此來減少內存加載圖片時過多的內存消耗。接下來用一段代碼進入分析:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
BitmapFactory提供了一些解碼(decode)的方法(decodeByteArray(), decodeFile(), decodeResource()等),用來從不同的資源中創建一個Bitmap。這些方法在構造位圖的時候會嘗試分配內存,因此OutOfMemory的異常往往在此時發生。
該段代碼使用了BitmapFactory.Options來獲取圖片的寬高和類型,而當options.inJustDecodeBounds設置為true時,內存是不會加載該圖片的,即返回一個為null的bitmap引用,但是會獲取到圖片的尺寸和類型,該數據可供后續的縮放使用。
接下來需要根據原圖的大小和控件需要顯示的大小來設置內存中應加載的圖片的尺寸大小,當計算完畢以后再將圖片加載進手機內存中,即可減少加載圖片時在內存中的資源占用,下面是計算控件所需圖片大小的代碼:
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;
}
該方法為設置BitmapFactory.Options 中inSampleSize 的值,如圖片的原大小為2048x1536,當inSampleSize 為4時,將會得到一個約512x384大小的Bitmap。加載這張縮小的圖片僅僅使用大概0.75MB的內存,如果是加載完整尺寸的圖片,那么大概需要花費12MB(前提都是Bitmap的配置是 ARGB_8888)。
設置inSampleSize為2的冪是因為解碼器最終還是會對非2的冪的數進行向下處理,獲取到最靠近2的冪的數。詳情參考inSampleSize的文檔。下面為加載任意大小圖片并設置為說需的大小的代碼:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
? ? ? ? ? ? ? ? ? final BitmapFactory.Options options = new BitmapFactory.Options();
? ? ? ? ? ? ? ? ? options.inJustDecodeBounds = true;
? ? ? ? ? ? ? ? ? BitmapFactory.decodeResource(res, resId, options);
? ? ? ? ? ? ? ? ? options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
? ? ? ? ? ? ? ? ? options.inJustDecodeBounds = false;
? ? ? ? ? ? ? ? ? return BitmapFactory.decodeResource(res, resId, options);
}
首先需要設置 inJustDecodeBounds 為 true, 把options的值傳遞過來,但不加載圖片,不消耗內存,然后設置 inSampleSize 的值。此時設置 inJustDecodeBounds 為 false,之后重新調用相關的解碼方法,指定大小的圖片便加載進入了內存當中。如:
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
到這里基本的加載相關已經完成了,但依然有很多問題,比如圖片加載是不應該在主線程完成的。
二、非UI線程處理Bitmap: 圖片的加載是耗時操作,尤其是網絡加載,需要在非UI線程中處理,此處使用AsyncTask演示在后臺線程中處理Bitmap以及處理并發(concurrency)的問題。官網介紹簡單情況比較青睞于用AsyncTask,復雜情況就不建議使用了。
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
imageViewReference = new WeakReference(imageView);}
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);} ?} ?} ?}
以上為對異步加載的代碼封裝,讓加載操作在后臺運行,對以上代碼的調用方法如下:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId); ?}
異步加載的簡單封裝與其他情況下使用AsyncTask并無太大差別,在這里不再進行過多的介紹。下面將分析圖像的并發處理。
此處的問題為在ListView、GridView、RecyclerView中我們為每一個item都添加了一個下載的task,在用戶滑動控件的時候,會有一些item被回收,然而,task并沒有消失也并不知道哪個Item被回收了哪個是要先加載的。接下來的這個類就是用來解決此問題的,AsyncDrawable類也是理解此小節的一個關鍵類。
static class AsyncDrawable extends BitmapDrawable { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ?private final WeakReference bitmapWorkerTaskReference; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? public AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask)
? ? ? {super(res, bitmap); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ?bitmapWorkerTaskReference =new WeakReference(bitmapWorkerTask);
? ? ? ?}
? ? ? ? public BitmapWorkerTask getBitmapWorkerTask() {
? ? ? ? ? ? ? ? ? ? ? return bitmapWorkerTaskReference.get();
? ? ? ? }
}
public void loadBitmap(int resId, ImageView imageView) {
? ? ? ? if (cancelPotentialWork(resId, imageView)) {
? ? ? ? ? ? ? ? ?final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
? ? ? ? ? ? ? ? ?final AsyncDrawable asyncDrawable =new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
? ? ? ? ? ? ? ? ?imageView.setImageDrawable(asyncDrawable);
? ? ? ? ? ? ? ? ?task.execute(resId);
? ? ? ? ? }
?}
結合loadBitmap來理解代碼意圖:在loadBitmap中創建了AsyncDrawable實例, AsyncDrawable(getResources(), mPlaceHolderBitmap, task);第一個參數忽略,第二個參數為默認bitmap圖片,第三個為需要異步加載的圖片如來自網絡,第一第二個參數是用來生成AsyncDrawable的默認圖片的,它將會和目標imageView(item中的圖片)進行綁定并顯示一個默認的圖片,同時執行task.execute(resId)代碼,后臺將加載需要加載的圖片,在圖片加載完成后將真正需要顯示的圖片顯示在imageView上。整個過程中AsyncDrawable起到了關鍵的綁定作用,在loadBitmap中使用到了cancelPotentialWork方法,該方法是檢查是否有另一個正在執行的任務與該ImageView關聯了起來,如果的確是這樣,它通過執行cancel()方法來取消另一個任務,以此來保證執行的是最新最近的task,而不是某一個已經過時的task。
public static boolean cancelPotentialWork(int data, ImageView imageView) {
? ? ?final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
? ? ?if (bitmapWorkerTask != null) {
? ? ? ? ? ? ? ? ?final int bitmapData = bitmapWorkerTask.data;
? ? ? ? ? ? ? ? ?if (bitmapData == 0 || bitmapData != data) {
? ? ? ? ? ? ? ? bitmapWorkerTask.cancel(true);
? ? ? ? ? ? ?} else {
? ? ? ? ? ? ? ? return false;
? ? ? ? ? ? }
? ? ?}
?return true;
?}
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
? ? ? ? ? ? ? ? if (imageView != null) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?final Drawable drawable = imageView.getDrawable();
? ? ? ? ? ? ? ? ? ? ? ? ? ? if (drawable instanceof AsyncDrawable) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?return asyncDrawable.getBitmapWorkerTask();
? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? return null;
}
getBitmapWorkerTask()被用作檢索AsyncTask是否已經被分配到指定的ImageView
最后是更新BitmapWorkerTask的onPostExecute() 方法
protected void onPostExecute(Bitmap bitmap) {
? ? ? ? ? ? ?if (isCancelled()) {bitmap = null;}
? ? ? ? ? ? ?if (imageViewReference != null && bitmap != null) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? final ImageView imageView = imageViewReference.get();
? ? ? ? ? ? ? ? ? ? ? ? ? ? final BitmapWorkerTask bitmapWorkerTask =getBitmapWorkerTask(imageView);
? ? ? ? ? ? ? ? ? ? ? ? ? ? if (this == bitmapWorkerTask && imageView != null) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?imageView.setImageBitmap(bitmap);
? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
}
在onPostExecute中再次檢查imageview是否被回收,task是否被取消,task是否一致如果一致且imageview沒有被回收,然后再顯示圖像。以上就是并發加載的解決方案了,主要解決listview等空間在執行循環加載和滾動時的并發圖像加載問題。
三、緩存Bitmap:解決完了加載問題,接下來要解決緩存問題,因為不可能所有的圖片無論是否可重復利用均每次顯示都重新加載。使用內存緩存與磁盤緩存可以提高響應速度與UI流暢度。
內存緩存以花費寶貴的程序內存為前提來快速訪問位圖。LruCache類(在API Level 4的Support Library中也可以找到)特別適合用來緩存Bitmaps,它使用一個強引用(strong referenced)的LinkedHashMap保存最近引用的對象,并且在緩存超出設置大小的時候剔除(evict)最近最少使用到的對象。當加載Bitmap顯示到ImageView 之前,會先從LruCache 中檢查是否存在這個Bitmap。如果確實存在,它會立即被用來顯示到ImageView上,如果沒有找到,會觸發一個后臺線程去處理顯示該Bitmap任務。
public void loadBitmap(int resId, ImageView imageView) {
? ? ? ? ? ? ? final String imageKey = String.valueOf(resId);
? ? ? ? ? ? ?final Bitmap bitmap = getBitmapFromMemCache(imageKey);
? ? ? ? ? ? if (bitmap != null) {
? ? ? ? ? ? ? ? ? ? ?mImageView.setImageBitmap(bitmap);
? ? ? ? ? ?} else {
? ? ? ? ? ? ? ? ? ? ?mImageView.setImageResource(R.drawable.image_placeholder);
? ? ? ? ? ? ? ? ? ? ?BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
? ? ? ? ? ? ? ? ? ? ?task.execute(resId);
? ? ? ? ? ? }
}
BitmapWorkerTask 需要在doInBackground中把解析好的Bitmap添加到內存緩存中
內存緩存能夠提高訪問最近用過的Bitmap的速度,但是我們無法保證最近訪問過的Bitmap都能夠保存在緩存中。像類似GridView等需要大量數據填充的控件很容易就會用盡整個內存緩存。另外,我們的應用可能會被類似打電話等行為而暫停并退到后臺,因為后臺應用可能會被殺死,那么內存緩存就會被銷毀,里面的Bitmap也就不存在了。一旦用戶恢復應用的狀態,那么應用就需要重新處理那些圖片。
磁盤緩存可以用來保存那些已經處理過的Bitmap,它還可以減少那些不再內存緩存中的Bitmap的加載次數。當然從磁盤讀取圖片會比從內存要慢,而且由于磁盤讀取操作時間是不可預期的,讀取操作需要在后臺線程中處理。內存緩存的檢查是可以在UI線程中進行的,磁盤緩存的檢查需要在后臺線程中處理。磁盤操作永遠都不應該在UI線程中發生。當圖片處理完成后,Bitmap需要添加到內存緩存與磁盤緩存中,方便之后的使用。該模塊可結合網上所介紹的Bitmap三級緩存的一些知識來具體的了解圖片的緩存機制。
以上只是在一些很基礎的android層面進行了圖片加載的分析,圖片加載框架很多都會在此基礎上進行封裝,理解基礎的圖片加載對理解封裝了更多方法的框架將會有不小的幫助。