高效的顯示圖片(Displaying Bitmaps Efficiently)
了解如何使用通用的技術(shù)來處理和讀取位圖對象歇终,讓您的用戶界面(UI)組件是可響應(yīng)的,并避免超過你的應(yīng)用程序內(nèi)存限制的方式拨扶。如果你不小心,位圖可以快速消耗可用的內(nèi)存預(yù)算而導(dǎo)致應(yīng)用程序崩潰,引發(fā)可怕的異常:
java.lang.OutofMemoryError: bitmap size exceeds VM budget.
下面是一些 為什an么在你的Android應(yīng)用程序加載位圖是棘手的原因 :
- 移動設(shè)備通常擁有受限的系統(tǒng)資源媒惕。Android設(shè)備分配給每個應(yīng)用的可用內(nèi)存空間只不過16MB帝簇。在 Android兼容性定義文檔[ Android Compatibility Definition Document (CDD)], 3.7章節(jié). 虛擬設(shè)備的兼容性一文 為了適應(yīng)多屏幕尺寸和密度指定了最小應(yīng)用內(nèi)存需求徘郭。應(yīng)用程序需要優(yōu)化去處理最小的內(nèi)存限制。然而丧肴,要記住很多設(shè)備被設(shè)置成更高的限制残揉。
- 位圖占據(jù)大量的內(nèi)存,特別是那些豐富的圖像芋浮,比如照片抱环。例如,硬件類型為 Galaxy Nexus 的設(shè)備 的相機應(yīng)用拍的照片達到了 2592x1936 像素 (5百萬像素).如果位圖被配置為使用 ARGB_8888 (在 Android 2.3 以前是默認(rèn)的)纸巷,加載這個圖像到內(nèi)存里需要19MB的內(nèi)存(259219364 字節(jié))镇草,馬上就會耗盡一些設(shè)備匯總的單個應(yīng)用的內(nèi)存限制。
- Android應(yīng)用的UI 需要即時地加載多個位圖瘤旨。像ListView,GridView 和 ViewPager 組件 通常包含多個位圖在屏幕上陶夜,更多可能性在關(guān)閉屏幕時,使用手指撥動裆站,立即準(zhǔn)備去顯示条辟。
課程
- 高效的加載大尺寸位圖 (Loading Large Bitmaps Efficiently)
本課將引導(dǎo)您在不超過每個應(yīng)用程序的內(nèi)存限制下,解碼大位圖宏胯。 - 在UI線程外處理位圖(Processing Bitmaps Off the UI Thread)
位圖處理(調(diào)整大小羽嫡,從遠(yuǎn)程資源下載等)不應(yīng)該占用主UI線程。這節(jié)課將引導(dǎo)你通過使用AsyncTask在后臺線程中處理圖像肩袍,和解釋如何處理并發(fā)問題杭棵。 - 位圖緩存 (Caching Bitmaps)
這節(jié)課將引導(dǎo)你 在讀取多個位圖時,使用內(nèi)存和硬盤緩存來提高你的UI的 響應(yīng)性 和流暢性氛赐。 - 管理位圖內(nèi)存 (Managing Bitmap Memory)
這節(jié)課將引導(dǎo)你 如何管理位圖的內(nèi)存以最大化你的應(yīng)用的性能魂爪。 - 在UI上顯示位圖 (Displaying Bitmaps in Your UI)
這節(jié)課將所有的綜合在一起,向你展示如何加載多個圖片到你的組件中(比如ViewPager and GridView)艰管,并使用一個后臺線程和位圖緩存滓侍。
高效的加載大尺寸位圖
圖片有各種形狀和大小. 在很多情況下,它們有更大的需要超過一個典型的應(yīng)用程序的界面牲芋。例如撩笆,Gallery(畫廊)系統(tǒng)應(yīng)用在顯示圖片時捺球,使用了設(shè)備的攝像頭,它(攝像頭)通常的分辨率要高于你的設(shè)備的屏幕密度夕冲。
既然你正在使用有限的內(nèi)存氮兵,理想情況下,你只應(yīng)該在內(nèi)存中加載一個低分辨率的版本的圖片歹鱼。低分辨率版本的圖片應(yīng)該匹配你要顯示的UI組件的尺寸泣栈。一個更高分辨率的圖片不能提供更多可見的好處,但是仍然占據(jù)珍貴的內(nèi)存空間弥姻,和由于額外的縮放而導(dǎo)致額外的性能開銷南片。
這節(jié)課教你 解碼大尺寸的圖片而不越過每個應(yīng)用的內(nèi)存限制,以在內(nèi)存中加載一個更小的 樣本版本(縮略圖)的方式蚁阳。
讀取位圖的尺寸大小和類型
BitmapFactory類提供了多個對圖片解碼的方法 (decodeByteArray(), decodeFile(),decodeResource(), 等.) 铃绒,以從不同的數(shù)據(jù)源創(chuàng)建位圖對象÷菥瑁基于你的圖像數(shù)據(jù)源來選擇合適的解碼方法颠悬。這些方法的作用是為結(jié)構(gòu)化的位圖分配內(nèi)存,因此很容易的返回OutOfMemory 異常定血。每種類型的解碼方法都有擴展的方法簽名參數(shù)赔癌,可以通過BitmapFactory.Options類來幫助你指定解碼選項(參數(shù))。設(shè)置 inJustDecodeBounds 屬性為 true可以忽略內(nèi)存分配的步驟澜沟,它會返回 null 的位圖對象灾票,但是為選項outWidth, outHeight 和 outMimeType 賦值了。這個技術(shù)允許你讀取位圖數(shù)據(jù)的尺寸和類型而不構(gòu)造位圖對象(分配內(nèi)存)茫虽。
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;
為了不出現(xiàn) java.lang.OutOfMemory 異常,在解碼前先要檢查位圖的尺寸刊苍,除非你絕對的信任你的數(shù)據(jù)源以一種可預(yù)見的圖片尺寸大小與最小的可用內(nèi)存是合適的。
讀取縮放后的圖像到內(nèi)存
現(xiàn)在我們知道了圖像的尺寸濒析,他們可被用于決定是否使用完整的圖像加載到內(nèi)存或者采用縮略圖加載到內(nèi)存正什。下面是一些考慮的因素:
估計記載整個圖片到內(nèi)存后的內(nèi)存占用(使用)量
基于你的應(yīng)用的其他內(nèi)存需要, 你愿意的分配給的 加載圖片的內(nèi)存占用量
目標(biāo) ImageView 的尺寸 或者 你要加載到顯示用的UI組件的 尺寸号杏。
當(dāng)前設(shè)備的屏幕尺寸和密度
例如婴氮,加載分辨率為 1024x768 像素的圖像到內(nèi)存,最后卻只顯示在一個 ImageView上的 128x96的縮放后圖像盾致,是非常不值得的主经。
要告訴解碼器來抽樣(縮放)一個圖像,設(shè)置BitmapFactory.Options 對象的 inSampleSize 為 true庭惜。例如罩驻,一個分辨率為2048x1536 的圖像在使用 inSampleSize 等于4 時,產(chǎn)生一個 大約512x384 的位圖蜈块。加載這個倒內(nèi)存需要0.75MB鉴腻,全部加載整圖則需要12MB(假設(shè)使用 ARGB_8888配置加載).下面是一個計算 抽樣尺寸值的方法迷扇,它基于兩個屬性目標(biāo)寬度百揭,目標(biāo)高度爽哎。
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
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;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
注意: 一個比率 是被計算出來的,因為解碼器使用了一個固定值器一。通過舍入到最接近的 比率课锌。按照 inSampleSize 文檔。
要使用這個方法, 第一次解碼使用 inJustDecodeBounds設(shè)置為 true, 傳入設(shè)置的參數(shù)祈秕。然后再次解碼使用 inSampleSize value 和 inJustDecodeBounds 為false:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
這個方法很容易的加載一個 未知的大尺寸圖像 到 一個 "需要顯示100x100像素的縮略圖的 imageView"上:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
你可以使用類似的方法去處理其他數(shù)據(jù)源的位圖渺贤,有需要時用來替換 BitmapFactory.decode*方法。
在非UI線程上處理圖像
BitmapFactory.decode*系列方法请毛,在 Load Large Bitmaps Efficiently 這節(jié)課里就討論過志鞍,如果源數(shù)據(jù)時需要從硬盤或者網(wǎng)絡(luò)位置讀取時(或者其他真實的不是內(nèi)存的數(shù)據(jù)源),不應(yīng)該在主UI線程執(zhí)行方仿。加載圖片所用的時長是不可預(yù)測的固棚,和依賴多個因素(從硬盤或者網(wǎng)絡(luò)的讀取速度,圖像尺寸仙蚜,CPU的能力等等)此洲。如果一個任務(wù)阻塞的UI線程,那么系統(tǒng)就會標(biāo)記你的應(yīng)用為 未響應(yīng)的委粉,用戶就會收到一個關(guān)閉選項的對話框(更多請閱讀 Designing for Responsiveness )呜师。
這節(jié)課引導(dǎo) 使用AsyncTask 在后臺線程中處理圖片的處理方式和,展示如果處理并發(fā)問題贾节。
使用一個異步任務(wù) AsyncTask
AsyncTask 提供了一個簡單的方式來在后臺線程中執(zhí)行工作汁汗,和發(fā)布處理結(jié)果回調(diào)到UI線程中。要使用它栗涂,只需創(chuàng)建一個子類和重載提供的方法知牌。下面是一個家在大圖像到ImageView 中的示例,它使用了AsyncTask 和 上一節(jié)課中提到的decodeSampledBitmapFromResource():
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set 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 不會妨礙 ImageView 和 引用的對象能夠被垃圾回收器回收戴差。當(dāng)任務(wù)完成后送爸,這樣的方式不會保證ImageView會繼續(xù)存在,這樣你必須在onPostExecute(). 方法中檢查這個引用是否可用暖释。ImageView 可能不會存在很長時間袭厂,例如,用戶可能會離開這個activity球匕,或者在任務(wù)結(jié)束前發(fā)生了配置變化(譯者注:比如翻轉(zhuǎn)屏幕)纹磺。
要異步的加載圖片,簡單的只需創(chuàng)建一個任務(wù)和執(zhí)行它:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
處理并發(fā)
一般的視圖組件亮曹,比如ListView and GridView 當(dāng)結(jié)合AsyncTask 使用時會有一些其他問題橄杨。為了有效的利用內(nèi)存秘症,這些組件在滾動時會回收重用它們的子視圖控件。如果每個子控件都在AsyncTask中引發(fā)式矫,那么當(dāng)任務(wù)完成時就無法得到保證乡摹,導(dǎo)致被關(guān)聯(lián)到的視圖還沒有被回收,就使用在其他子視圖中了采转。此外聪廉,這也無法保證異步任務(wù)開始的順序和它結(jié)束的順序是一致的。
在Multithreading for Performance(多線程任務(wù)性能)這篇博客中討論了并發(fā)的處理故慈,和提供了一個解決方案板熊,在ImageView上存儲一個 指向最近的一次的異步任務(wù)AsyncTask的 引用,而當(dāng)任務(wù)完成后再次檢測該引用察绷。使用類似的方法干签,我們隨著這樣的模式,擴展我們上一章節(jié)中提到的AsyncTask方法拆撼。
創(chuàng)建一個專用的Drawable子類容劳,以存儲一個 工作任務(wù)(AsyncTask)對象的引用。在這種方式中情萤,一個 BitmapDrawable 被用于作為一個圖象占位符鸭蛙,在任務(wù)完成后,它能夠被顯示在 ImageView中:
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
在執(zhí)行 BitmapWorkerTask 之前筋岛,你要創(chuàng)建一個 AsyncDrawable 并綁定到 目標(biāo)ImageVIew上娶视。
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);
}
}
上面示例的 cancelPotentialWork 方法檢查了 是否有其他任務(wù)管理到這個ImageView。如果是睁宰,它嘗試調(diào)用 cancel()方法去終止上一次的任務(wù)肪获。在很少的情況下,新任務(wù)的數(shù)據(jù)匹配已經(jīng)存在的任務(wù)柒傻,并且不在需要觸發(fā)孝赫。下面是一個cancelPotentialWork的實現(xiàn):
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// If bitmapData is not yet set or it differs from the new data
if (bitmapData == 0 || bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
輔助方法 getBitmapWorkerTask(), 在上面被用于 獲得指定ImageView 關(guān)聯(lián)到的 任務(wù)對象:
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;
}
最后一步是在 BitmapWorkerTask 的 onPostExecute()方法中的更新操作,它檢查了 任務(wù)是否被終止過了和 當(dāng)前的任務(wù)是否是 ImageView關(guān)聯(lián)的任務(wù)红符。
The last step is updating onPostExecute() in BitmapWorkerTask so that it checks if the task is cancelled and if the current task matches the one associated with the ImageView:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
@Override
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);
}
}
}
}
這樣的實現(xiàn)適用于 ListView 和 GridView 組件及其他需要回收他們子視圖的組件青柄。在你平時設(shè)置圖像到ImageView的地方簡單的調(diào) loadBitmap 方法。比如预侯,在一個 GridView 中實現(xiàn)方式就是 在 adapter中的 getView()方法中調(diào)用致开。
緩存圖像
加載一張圖像到你的UI很簡單,然而如果你需要一次性加載一批圖片就會很復(fù)雜萎馅。在很多情形下(比如ListView, GridView 或 ViewPager)双戳,屏幕上的圖像總數(shù),結(jié)合那些不久后滾動后顯示再屏幕的圖片糜芳,根本就是無限的飒货。
有些組件 通過回收移除屏幕的子視圖的方式 可以保持較少的內(nèi)存使用 魄衅。加入你沒有或者更長久的活動引用,垃圾回收器將會釋放你加載的圖片塘辅。這是好的情況晃虫,但是為了保持流暢性和 快速加載UI,你不需要再處理 那些 “再次回到屏幕上的圖像 ”莫辨。在這里一個內(nèi)存和磁盤緩存常常是有幫助的傲茄,允許組件哭訴的重新加載處理過的圖像毅访。
這節(jié)課將引導(dǎo)你沮榜,當(dāng)加載多個圖像時,使用一個內(nèi)存和磁盤圖像緩存來提高UI的響應(yīng)性和流暢性喻粹。
使用一個內(nèi)存緩存
一個內(nèi)存緩存提供了快速訪問位圖的方式蟆融,更好的占用珍貴的應(yīng)用程序內(nèi)存。LruCache 類(在Support Library 安卓支持可 API 4 中)很適合 緩存圖像的任務(wù)守呜,它以LinkedHashMap 中的強引用方式 保持最近被引用的對象和 在緩存數(shù)量超過指定的數(shù)量時移除最近最少使用的成員型酥。
注意: 在過去,流行的內(nèi)存緩存的實現(xiàn)是使用SoftReference 或 WeakReference 位圖緩存查乒,然而現(xiàn)在已經(jīng)不再推薦使用弥喉。從Android 2.3(API 級別 9)開始,垃圾回收器更激進的回收 軟引用/弱引用玛迄,使得相當(dāng)于無效由境。另外 在 Android 3.0 (API 級別 11)之前,一個位圖的后臺數(shù)據(jù)被存放在原始內(nèi)存中蓖议,它不能以可預(yù)見的方式被釋放虏杰,它潛在性的導(dǎo)致一個應(yīng)用臨時的超出它的內(nèi)存限制而崩潰。
為了選擇一個合適的LruCache 的尺寸勒虾,一些因素必須要考慮到纺阔,比如:
- 你的剩余的activity或者應(yīng)用程序 是如何 集中 你的內(nèi)存的?How memory intensive is the rest of your activity and/or application?
- 一次加載多少圖像到屏幕上顯示修然? 有多少圖片即將準(zhǔn)備顯示到屏幕上笛钝?
- 設(shè)備的屏幕尺寸和密度是多少?在內(nèi)存中顯示相同數(shù)量的圖片愕宋,一個更高級的高密度屏幕 (xhdpi)設(shè)備比如 Galaxy Nexus 比起 Nexus S (hdpi)設(shè)備 需要更多的緩存玻靡。
- 這些圖片的尺寸規(guī)格和配置是什么,每個將占據(jù)多大的內(nèi)存掏婶?
- 圖像被訪問的頻率啃奴?是否有些圖像被訪問的頻率比其他的高?如果這樣雄妥,或許你需要一直在內(nèi)存中保持某些圖像最蕾,后者使用多個LruCache 對象對應(yīng)多個圖像的分組依溯。
- 你能在質(zhì)量和數(shù)量之間保持平衡么? 有時 存儲大量的低質(zhì)量圖像更有用瘟则,潛在的在其他后臺線程中加載高質(zhì)量的圖像版本黎炉。
沒有適用于所有應(yīng)用程序的絕對的指定尺寸和準(zhǔn)則,由你分析你的使用情況來決定醋拧,并上升到一個合適的方案慷嗜。一個緩存如果太小,則導(dǎo)致額外的無益的超過限額丹壕,如果過大而再次導(dǎo)致java.lang.OutOfMemory 異城煨担或者為你的app提供更少的剩余內(nèi)存可工作。
下面的示例演示了 如何為圖像配置 LruCache:
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
注意: 在示例中 菌赖,為我們的內(nèi)存分配了整個應(yīng)用內(nèi)存的八分之一缭乘。在一個一般 hdpi 設(shè)備中,這最小接近 4MB(32/8). 在一個 800x480分辨率 的設(shè)備中琉用,一個全屏的 GridView被填滿圖像的話堕绩,大約需要1.5MB (8004804 bytes), 這樣將可以在內(nèi)存中緩存大約 2.5頁的圖像。
當(dāng)加載一個圖像到 ImageView 中, LruCache 緩存 將會先進行檢查邑时。如果找到的結(jié)果不為空奴紧,它將被直接更新到 ImageView上,否則將會產(chǎn)生一個后臺線程來處理這個圖像:
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 也需要被更新晶丘,添加 緩存記錄 到內(nèi)存緩存中:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
使用一個磁盤緩存
對于快速的訪問最近呈現(xiàn)過的圖像黍氮,一個內(nèi)存緩存是非常用于的,然而你不能完全保證 圖片在這個緩存中铣口。像GridView這樣的組件有大量的數(shù)據(jù)集合滤钱,可以很容易的填滿整個內(nèi)存緩存。你的應(yīng)用可能被其他任務(wù)(比如電話來了)所打斷脑题,和在后臺時會背傻吊和內(nèi)存緩存被銷毀件缸。一旦用戶恢復(fù)了應(yīng)用,你的應(yīng)用需要再次處理每一個圖像叔遂。
一個磁盤緩存可以被應(yīng)用到這些場景他炊,當(dāng)圖像無法在內(nèi)存緩存中可用時,可以持續(xù)訪問圖像和幫助減少加載圖像的次數(shù)已艰。當(dāng)然痊末,從磁盤緩存中提取圖像相比較于從內(nèi)存中來說是較慢的,并且最好在后臺任務(wù)中處理哩掺,磁盤讀取次數(shù)可能不可預(yù)知凿叠。
注意: 如果訪問很頻繁,一個 ContentProvider 可能更適合用于緩存圖像, 在gallery 應(yīng)用示例中 有更多演示盒件。
下面的演示代碼使用了一個 DiskLruCache 的磁盤緩存實現(xiàn)蹬碧,它來自于 安卓源代碼 Android source. 下面是一個更新后的示例,它在內(nèi)存緩存之外炒刁,又增加了一個磁盤緩存:
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { // Not found in disk cache
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
注意: 由于初始化磁盤緩存需要磁盤操作恩沽,因此不應(yīng)該在主線程中進行。這意味著翔始,在初始化之前有機會訪問該緩存罗心。為了解決這個問題,在上面的實現(xiàn)中城瞎,使用了一個鎖對象渤闷,以確保在初始化完成之前不能從緩存中讀取。
這時全谤,在主UI線程中檢查內(nèi)存緩存肤晓,在后臺線程中檢查磁盤緩存。硬盤操作應(yīng)該絕對不要再UI線程中使用认然。當(dāng)圖像處理完成后,最后的圖片被添加到內(nèi)存緩存和磁盤緩存中漫萄。
處理配置的變化
運行時配置變化卷员,比如屏幕方向改變,導(dǎo)致Android銷毀和 使用新的配置 重新啟動運行中的activity(更多信息參考Handling Runtime Changes)腾务。當(dāng)一個配置改變發(fā)生時毕骡,你可能想不再重新處理你所有的圖片,以獲得平滑快速的用戶體驗岩瘦。
幸運的是未巫,在 使用內(nèi)存緩存(Use a Memory Cache ) 一節(jié)中你擁有了一個很好的圖片內(nèi)存緩存。通過一個 調(diào)用了setRetainInstance(true)的Fragment保存緩存對象启昧,再將Fragment 傳到新的Activity示例中叙凡。在activity被重新創(chuàng)建后,這個重新創(chuàng)建的(保留的)的 Fragment 被重新附加密末,這樣你重新通過它獲得到緩存對象握爷,允許圖像被快速提取和重新填充到 ImageView 對象。
下面的示例是 在配置變化時严里,使用一個Fragment 保留一個LruCache 對象:
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache<String, Bitmap> mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
為了驗證這一點新啼,使用 有或者沒有保留的Fragmen 方式來嘗試旋轉(zhuǎn)一個設(shè)備。你可以注意到刹碾,在圖像填充到activity上時幾乎沒有滯后燥撞,在你獲得緩存時是即刻從內(nèi)存中的。一些圖像沒有從內(nèi)存中被找到,也是有希望在磁盤緩存中找到物舒,如果沒有找到辆布,就會像平常那樣處理。
管理圖片內(nèi)存
除了在 緩存圖像(Caching Bitmaps) 章節(jié)描述的步驟茶鉴,這里有些明確的事情可以做锋玲,以幫助垃圾回收和重用圖像。根據(jù)不同的Android版本不同有不同的推薦策略涵叮。BitmapFun 示例包含了一些類惭蹂,展示了如何設(shè)計你的程序以在不同的Android版本中更有效率的工作。
為了對這節(jié)課劃分段落, 先了解Android如何管理圖片內(nèi)存的演變過程:
- 在 Android 2.2 (API 級別 8) 及以下割粮,當(dāng)垃圾回收發(fā)生時盾碗,你的應(yīng)用的線程會暫停。這導(dǎo)致了延遲舀瓢,降低了性能廷雅。Android 2.3添加了并發(fā)的垃圾回收,這意味著京髓,失去引用的圖像的內(nèi)存很快被回收航缀。
- 在 Android 2.3.3 (API 級別 10) 及以下,位圖的后備的像素數(shù)據(jù)被存儲在原生內(nèi)存中堰怨。它被和位圖本身分開芥玉,它被存儲在Dalvik 的堆中。 在原生內(nèi)存中的像素數(shù)據(jù)部能以可預(yù)知的方式被釋放备图,可能導(dǎo)致一個應(yīng)用臨時的越過內(nèi)存限制而崩潰灿巧。 Android 3.0 (API 級別 11)中,像素數(shù)據(jù)也被存儲在Dalvik 的堆中揽涮,和它關(guān)聯(lián)到的位圖一起了抠藕。
下面的章節(jié)描述了 在不同的Android版本中如何優(yōu)化內(nèi)存的管理。
Android 2.3.3 及以下 的內(nèi)存管理
在 Android 2.3.3 (API 級別 10)及以下蒋困,推薦使用 recycle() 方法盾似。如果你在你的應(yīng)用中顯示大量的圖像數(shù)據(jù),或許你遇到過 OutOfMemoryError 錯誤家破。recycle() 方法允許你盡快的回收內(nèi)存颜说。
警告: 當(dāng)你確定你的位圖對象不再使用的時候,你可以調(diào)用 recycle() 如果你調(diào)用了 lrecycle() 汰聋,而又試圖繪制這個位圖门粪,你將會受到一個錯誤: "Canvas: trying to use a recycled bitmap".
下面的代碼片段提供了一個 調(diào)用 recycle(). 的演示。它使用了引用計數(shù)(通過變量 mDisplayRefCount 和 mCacheRefCount )來追蹤 一個位圖當(dāng)前被顯示或者在緩存中烹困。當(dāng)下列條件成立時回收圖像:
引用計數(shù) mDisplayRefCount 和 mCacheRefCount 都為 0.
-
位圖不是 null, 并且還未被回收
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle.
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
&& hasValidBitmap()) {
getBitmap().recycle();
}
}private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
Android 3.0 及 更高版本上 管理內(nèi)存
Android 3.0 (API 級別 11) 提供了 BitmapFactory.Options.inBitmap 字段玄妈。如果這個選項被設(shè)置了,在加載內(nèi)容時,使用了這個選項的解碼方法將會試圖去重用已經(jīng)存在的位圖拟蜻。這意味著绎签,位圖內(nèi)存被重用了,而提升了性能酝锅,它移除了內(nèi)存分配和回收的步驟诡必。然而,inBitmap 也是有些已知的局限搔扁,具體的說爸舒,在Android 4.4 (API 級別 19)之前,只能尺寸大小相同的圖片才被支持重用稿蹲。更多信息請閱讀 inBitmap 文檔扭勉。
保存位圖以備后用
下面的代碼片段演示了 如何保持一個位圖以備將來使用。在運行在Android 3.0或者更高版本上的一個應(yīng)用中苛聘,一個圖片被從 LruCache中移除時涂炎,再在一個HashSet 中放置一個位圖的軟引用,使用inBitmap標(biāo)記它以盡可能被重用设哗。
Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
mReusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable.
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later.
mReusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}
使用一個已經(jīng)存在的位圖
在運行的應(yīng)用唱捣,解碼方法要去檢查 是否已經(jīng)有可重用的位圖,比如:
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// If we're running on Honeycomb or newer, try to use inBitmap.
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
下面的代碼片段展示了 上面代碼中調(diào)用的addInBitmapOptions()方法熬拒,它看起來 對一個已經(jīng)存在的位圖設(shè)置了inBitmap. 的值爷光。注意,如果它找到了一個合適的匹配時澎粟,這個方法也僅僅設(shè)置了inBitmap. 的值。你的代碼永遠(yuǎn)不要假設(shè)可以找到匹配欢瞪。
private static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
// inBitmap only works with mutable bitmaps, so force the decoder to
// return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap;
}
}
}
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
synchronized (mReusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator
= mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again.
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
}
return bitmap;
}
最后活烙,這個方法決定了 一個候選的位圖對象是否 滿足了 被用于inBitmap的尺寸的標(biāo)準(zhǔn):
static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
// the new bitmap is smaller than the reusable bitmap candidate
// allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
/**
* A helper function to return the byte usage per pixel of a bitmap based on its configuration.
*/
static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}
在 UI 上顯示位圖
這節(jié)課總結(jié)了上面課程的內(nèi)容,向你展示了如何加載多個圖像到 ViewPager 和 GridView 組件中遣鼓,使用了后臺線程啸盏,圖片緩存,處理并發(fā)和配置的改變骑祟。
加載圖像到 ViewPager 的實現(xiàn)
滑動屏幕模式 ( swipe view pattern ) 是一個極好的方式來導(dǎo)航圖像畫廊的詳細(xì)視圖頁回懦。你可以使用 一個PagerAdapter支持的ViewPager 組件來 實現(xiàn)這個模式。 然而次企,可能的更適合的支持適配器是 FragmentStatePagerAdapter 的子類怯晕,在從屏幕上不可見,內(nèi)存較低時缸棵,它自動的銷毀和保存 ViewPager 中的 Fragments 的狀態(tài)舟茶。
注意: 如果你只有很少的數(shù)量的圖像和確信 它們適用于應(yīng)用的內(nèi)存限制內(nèi),那么一個普通的 PagerAdapter 或 FragmentPagerAdapter 可能更合適。
下面是一個 擁有ImageView子元素的 ViewPager的實現(xiàn)吧凉,主Actvity 持有了 ViewPager和 adapter隧出。
public class ImageDetailActivity extends FragmentActivity {
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ViewPager mPager;
// A static dataset to back the ViewPager adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}
public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}
@Override
public int getCount() {
return mSize;
}
@Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
}
}
下面是一個詳細(xì)Fragment的實現(xiàn),它持有了ImageView子控件阀捅。這可能看起來是完全合理的方式胀瞪,然而你可以看到這個實現(xiàn)的缺點嗎?如何改善它呢饲鄙?
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;
static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}
// Empty constructor, required as per Fragment docs
public ImageDetailFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// image_detail_fragment.xml contains just an ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final int resId = ImageDetailActivity.imageResIds[mImageNum];
mImageView.setImageResource(resId); // Load image into ImageView
}
}
希望你注意到問題: 圖像從資源文件中讀取的過程 是在主UI線程的凄诞,它可能導(dǎo)致應(yīng)用掛起和被強行關(guān)閉。使用一個 AsyncTask 傍妒,像上面的課程 在UI線程外處理圖像 一課中描述的那樣幔摸,簡單的移動圖像加載和處理的過程到后臺線程中:
public class ImageDetailActivity extends FragmentActivity {
...
public void loadBitmap(int resId, ImageView imageView) {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
... // include BitmapWorkerTask class
}
public class ImageDetailFragment extends Fragment {
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (ImageDetailActivity.class.isInstance(getActivity())) {
final int resId = ImageDetailActivity.imageResIds[mImageNum];
// Call out to ImageDetailActivity to load the bitmap in a background thread
((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
}
}
}
一些額外的處理(比如改變大小或者從網(wǎng)絡(luò)中提出圖像)運行在 BitmapWorkerTask 中,不會影響主UI線程的響應(yīng)性颤练。如果后臺線程要很多次直接從磁盤中加載圖像既忆,那么添加一個內(nèi)存或者磁盤緩存是很有益的,像課程 緩存位圖 中描述的那樣嗦玖。下面是使用內(nèi)存緩存做了額外的修改:
public class ImageDetailActivity extends FragmentActivity {
...
private LruCache<String, Bitmap> mMemoryCache;
@Override
public void onCreate(Bundle savedInstanceState) {
...
// initialize LruCache as per Use a Memory Cache section
}
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = mMemoryCache.get(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
... // include updated BitmapWorkerTask from Use a Memory Cache section
}
將上面的片段整合在一起患雇,為你提供了一個 高響應(yīng)的 ViewPager的實現(xiàn),使用了更好圖像記載延遲宇挫,并且有能力的 盡可能多或者盡可能少的在后臺處理圖像苛吱。
加載圖像到 GridView 中的實現(xiàn)
網(wǎng)格列表構(gòu)造塊( grid list building block )對于展示圖像數(shù)據(jù)集合是十分有用的捧颅,它可以通過GridView組件方式的實現(xiàn)柠新。很多圖像需要一次性被加載到屏幕上婿失,當(dāng)上下滾動時很多圖像還需要準(zhǔn)備好被顯示坪圾。當(dāng)實現(xiàn)這樣的控件類型時笛粘,你一定要確保UI仍然流暢乔妈,內(nèi)存使用率在可控內(nèi)和正確的處理并發(fā)(由于 GridView 回收它們的子視圖 的方式導(dǎo)致)
要開始外莲,下面是一個標(biāo)準(zhǔn)的 GridView 的實現(xiàn)队贱,它擁有 ImageView 子控件欣除,并在 Fragment內(nèi)住拭。再一次,這看起來是完美的實現(xiàn)历帚,但是怎樣才能讓它更好滔岳?
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private ImageAdapter mAdapter;
// A static dataset to back the GridView adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
// Empty constructor as per Fragment docs
public ImageGridFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new ImageAdapter(getActivity());
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
return v;
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
startActivity(i);
}
private class ImageAdapter extends BaseAdapter {
private final Context mContext;
public ImageAdapter(Context context) {
super();
mContext = context;
}
@Override
public int getCount() {
return imageResIds.length;
}
@Override
public Object getItem(int position) {
return imageResIds[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup container) {
ImageView imageView;
if (convertView == null) { // if it's not recycled, initialize some attributes
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else {
imageView = (ImageView) convertView;
}
imageView.setImageResource(imageResIds[position]); // Load image into ImageView
return imageView;
}
}
}
再一次,這個實現(xiàn)的問題是圖像將在UI線程里被設(shè)置挽牢。當(dāng)工作在小的谱煤,簡單的圖像(由于系統(tǒng)資源加載和緩存),如果更多額外的處理需要完成卓研,你的UI就崩潰了趴俘。
在上面章節(jié)提到的睹簇,同樣的異步處理和緩存方法可以被用于這里的實現(xiàn)。然而寥闪,由于 GridView 回收它們的子視圖太惠,你仍然需要一個并發(fā)問題的方式。要處理它疲憋,使用 在UI線程外處理圖像 課程中討論過的技術(shù)凿渊,下面是個更新后的方案:
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
...
private class ImageAdapter extends BaseAdapter {
...
@Override
public View getView(int position, View convertView, ViewGroup container) {
...
loadBitmap(imageResIds[position], imageView)
return imageView;
}
}
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);
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
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;
}
... // include updated BitmapWorkerTask class
注意: 同樣的代碼可以輕易的適配到 ListView 中。
這個實現(xiàn)允許很靈活的處理 圖像的處理和加載缚柳,而不阻止UI的平滑埃脏。在后臺任務(wù)中,你可以從網(wǎng)絡(luò)加載圖像或者 改變大的相機照片的圖像尺寸秋忙,在任務(wù)完成后彩掐,圖像即呈現(xiàn)出來。
關(guān)于這的一個完整的示例灰追,和這節(jié)課其他概念的討論堵幽,請閱讀包含的示例應(yīng)用。
(本課程完弹澎,張云飛vir朴下,2015-08-25)