Android訓(xùn)練課程(Android Training) - 高效的顯示圖片

高效的顯示圖片(Displaying Bitmaps Efficiently)

了解如何使用通用的技術(shù)來處理和讀取位圖對(duì)象,讓您的用戶界面(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)用程序加載位圖是棘手的原因 :

  • 移動(dòng)設(shè)備通常擁有受限的系統(tǒng)資源。Android設(shè)備分配給每個(gè)應(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è)備 的相機(jī)應(yīng)用拍的照片達(dá)到了 2592x1936 像素 (5百萬像素).如果位圖被配置為使用 ARGB_8888 (在 Android 2.3 以前是默認(rèn)的),加載這個(gè)圖像到內(nèi)存里需要19MB的內(nèi)存(259219364 字節(jié)),馬上就會(huì)耗盡一些設(shè)備匯總的單個(gè)應(yīng)用的內(nèi)存限制。
  • Android應(yīng)用的UI 需要即時(shí)地加載多個(gè)位圖。像ListView,GridView 和 ViewPager 組件 通常包含多個(gè)位圖在屏幕上,更多可能性在關(guān)閉屏幕時(shí),使用手指撥動(dòng),立即準(zhǔn)備去顯示。

課程


  • 高效的加載大尺寸位圖 (Loading Large Bitmaps Efficiently)
    本課將引導(dǎo)您在不超過每個(gè)應(yīng)用程序的內(nèi)存限制下,解碼大位圖。
  • 在UI線程外處理位圖(Processing Bitmaps Off the UI Thread)
    位圖處理(調(diào)整大小,從遠(yuǎn)程資源下載等)不應(yīng)該占用主UI線程。這節(jié)課將引導(dǎo)你通過使用AsyncTask在后臺(tái)線程中處理圖像,和解釋如何處理并發(fā)問題。
  • 位圖緩存 (Caching Bitmaps)
    這節(jié)課將引導(dǎo)你 在讀取多個(gè)位圖時(shí),使用內(nèi)存和硬盤緩存來提高你的UI的 響應(yīng)性 和流暢性。
  • 管理位圖內(nèi)存 (Managing Bitmap Memory)
    這節(jié)課將引導(dǎo)你 如何管理位圖的內(nèi)存以最大化你的應(yīng)用的性能。
  • 在UI上顯示位圖 (Displaying Bitmaps in Your UI)
    這節(jié)課將所有的綜合在一起,向你展示如何加載多個(gè)圖片到你的組件中(比如ViewPager and GridView),并使用一個(gè)后臺(tái)線程和位圖緩存。

高效的加載大尺寸位圖

圖片有各種形狀和大小. 在很多情況下,它們有更大的需要超過一個(gè)典型的應(yīng)用程序的界面。例如,Gallery(畫廊)系統(tǒng)應(yīng)用在顯示圖片時(shí),使用了設(shè)備的攝像頭,它(攝像頭)通常的分辨率要高于你的設(shè)備的屏幕密度。

既然你正在使用有限的內(nèi)存,理想情況下,你只應(yīng)該在內(nèi)存中加載一個(gè)低分辨率的版本的圖片。低分辨率版本的圖片應(yīng)該匹配你要顯示的UI組件的尺寸。一個(gè)更高分辨率的圖片不能提供更多可見的好處,但是仍然占據(jù)珍貴的內(nèi)存空間,和由于額外的縮放而導(dǎo)致額外的性能開銷。

這節(jié)課教你 解碼大尺寸的圖片而不越過每個(gè)應(yīng)用的內(nèi)存限制,以在內(nèi)存中加載一個(gè)更小的 樣本版本(縮略圖)的方式。

讀取位圖的尺寸大小和類型

BitmapFactory類提供了多個(gè)對(duì)圖片解碼的方法 (decodeByteArray(), decodeFile(),decodeResource(), 等.) ,以從不同的數(shù)據(jù)源創(chuàng)建位圖對(duì)象。基于你的圖像數(shù)據(jù)源來選擇合適的解碼方法。這些方法的作用是為結(jié)構(gòu)化的位圖分配內(nèi)存,因此很容易的返回OutOfMemory 異常。每種類型的解碼方法都有擴(kuò)展的方法簽名參數(shù),可以通過BitmapFactory.Options類來幫助你指定解碼選項(xiàng)(參數(shù))。設(shè)置 inJustDecodeBounds 屬性為 true可以忽略內(nèi)存分配的步驟,它會(huì)返回 null 的位圖對(duì)象,但是為選項(xiàng)outWidth, outHeight 和 outMimeType 賦值了。這個(gè)技術(shù)允許你讀取位圖數(shù)據(jù)的尺寸和類型而不構(gòu)造位圖對(duì)象(分配內(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 異常,在解碼前先要檢查位圖的尺寸,除非你絕對(duì)的信任你的數(shù)據(jù)源以一種可預(yù)見的圖片尺寸大小與最小的可用內(nèi)存是合適的。

讀取縮放后的圖像到內(nèi)存

現(xiàn)在我們知道了圖像的尺寸,他們可被用于決定是否使用完整的圖像加載到內(nèi)存或者采用縮略圖加載到內(nèi)存。下面是一些考慮的因素:

估計(jì)記載整個(gè)圖片到內(nèi)存后的內(nèi)存占用(使用)量
基于你的應(yīng)用的其他內(nèi)存需要, 你愿意的分配給的 加載圖片的內(nèi)存占用量
目標(biāo) ImageView 的尺寸 或者 你要加載到顯示用的UI組件的 尺寸。
當(dāng)前設(shè)備的屏幕尺寸和密度
例如,加載分辨率為 1024x768 像素的圖像到內(nèi)存,最后卻只顯示在一個(gè) ImageView上的 128x96的縮放后圖像,是非常不值得的。

要告訴解碼器來抽樣(縮放)一個(gè)圖像,設(shè)置BitmapFactory.Options 對(duì)象的 inSampleSize 為 true。例如,一個(gè)分辨率為2048x1536 的圖像在使用 inSampleSize 等于4 時(shí),產(chǎn)生一個(gè) 大約512x384 的位圖。加載這個(gè)倒內(nèi)存需要0.75MB,全部加載整圖則需要12MB(假設(shè)使用 ARGB_8888配置加載).下面是一個(gè)計(jì)算 抽樣尺寸值的方法,它基于兩個(gè)屬性目標(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;
    }

注意: 一個(gè)比率 是被計(jì)算出來的,因?yàn)榻獯a器使用了一個(gè)固定值。通過舍入到最接近的 比率。按照 inSampleSize 文檔。

要使用這個(gè)方法, 第一次解碼使用 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);
}

這個(gè)方法很容易的加載一個(gè) 未知的大尺寸圖像 到 一個(gè) "需要顯示100x100像素的縮略圖的 imageView"上:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

你可以使用類似的方法去處理其他數(shù)據(jù)源的位圖,有需要時(shí)用來替換 BitmapFactory.decode*方法。

在非UI線程上處理圖像

BitmapFactory.decode*系列方法,在 Load Large Bitmaps Efficiently 這節(jié)課里就討論過,如果源數(shù)據(jù)時(shí)需要從硬盤或者網(wǎng)絡(luò)位置讀取時(shí)(或者其他真實(shí)的不是內(nèi)存的數(shù)據(jù)源),不應(yīng)該在主UI線程執(zhí)行。加載圖片所用的時(shí)長(zhǎng)是不可預(yù)測(cè)的,和依賴多個(gè)因素(從硬盤或者網(wǎng)絡(luò)的讀取速度,圖像尺寸,CPU的能力等等)。如果一個(gè)任務(wù)阻塞的UI線程,那么系統(tǒng)就會(huì)標(biāo)記你的應(yīng)用為 未響應(yīng)的,用戶就會(huì)收到一個(gè)關(guān)閉選項(xiàng)的對(duì)話框(更多請(qǐng)閱讀 Designing for Responsiveness )。

這節(jié)課引導(dǎo) 使用AsyncTask 在后臺(tái)線程中處理圖片的處理方式和,展示如果處理并發(fā)問題。

使用一個(gè)異步任務(wù) AsyncTask

AsyncTask 提供了一個(gè)簡(jiǎn)單的方式來在后臺(tái)線程中執(zhí)行工作,和發(fā)布處理結(jié)果回調(diào)到UI線程中。要使用它,只需創(chuàng)建一個(gè)子類和重載提供的方法。下面是一個(gè)家在大圖像到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 不會(huì)妨礙 ImageView 和 引用的對(duì)象能夠被垃圾回收器回收。當(dāng)任務(wù)完成后,這樣的方式不會(huì)保證ImageView會(huì)繼續(xù)存在,這樣你必須在onPostExecute(). 方法中檢查這個(gè)引用是否可用。ImageView 可能不會(huì)存在很長(zhǎng)時(shí)間,例如,用戶可能會(huì)離開這個(gè)activity,或者在任務(wù)結(jié)束前發(fā)生了配置變化(譯者注:比如翻轉(zhuǎn)屏幕)。

要異步的加載圖片,簡(jiǎn)單的只需創(chuàng)建一個(gè)任務(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 使用時(shí)會(huì)有一些其他問題。為了有效的利用內(nèi)存,這些組件在滾動(dòng)時(shí)會(huì)回收重用它們的子視圖控件。如果每個(gè)子控件都在AsyncTask中引發(fā),那么當(dāng)任務(wù)完成時(shí)就無法得到保證,導(dǎo)致被關(guān)聯(lián)到的視圖還沒有被回收,就使用在其他子視圖中了。此外,這也無法保證異步任務(wù)開始的順序和它結(jié)束的順序是一致的。

在Multithreading for Performance(多線程任務(wù)性能)這篇博客中討論了并發(fā)的處理,和提供了一個(gè)解決方案,在ImageView上存儲(chǔ)一個(gè) 指向最近的一次的異步任務(wù)AsyncTask的 引用,而當(dāng)任務(wù)完成后再次檢測(cè)該引用。使用類似的方法,我們隨著這樣的模式,擴(kuò)展我們上一章節(jié)中提到的AsyncTask方法。

創(chuàng)建一個(gè)專用的Drawable子類,以存儲(chǔ)一個(gè) 工作任務(wù)(AsyncTask)對(duì)象的引用。在這種方式中,一個(gè) BitmapDrawable 被用于作為一個(gè)圖象占位符,在任務(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 之前,你要?jiǎng)?chuàng)建一個(gè) 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ù)管理到這個(gè)ImageView。如果是,它嘗試調(diào)用 cancel()方法去終止上一次的任務(wù)。在很少的情況下,新任務(wù)的數(shù)據(jù)匹配已經(jīng)存在的任務(wù),并且不在需要觸發(fā)。下面是一個(gè)cancelPotentialWork的實(shí)現(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ù)對(duì)象:

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);
            }
        }
    }
}

這樣的實(shí)現(xiàn)適用于 ListView 和 GridView 組件及其他需要回收他們子視圖的組件。在你平時(shí)設(shè)置圖像到ImageView的地方簡(jiǎn)單的調(diào) loadBitmap 方法。比如,在一個(gè) GridView 中實(shí)現(xiàn)方式就是 在 adapter中的 getView()方法中調(diào)用。

緩存圖像

加載一張圖像到你的UI很簡(jiǎn)單,然而如果你需要一次性加載一批圖片就會(huì)很復(fù)雜。在很多情形下(比如ListView, GridView 或 ViewPager),屏幕上的圖像總數(shù),結(jié)合那些不久后滾動(dòng)后顯示再屏幕的圖片,根本就是無限的。

有些組件 通過回收移除屏幕的子視圖的方式 可以保持較少的內(nèi)存使用 。加入你沒有或者更長(zhǎng)久的活動(dòng)引用,垃圾回收器將會(huì)釋放你加載的圖片。這是好的情況,但是為了保持流暢性和 快速加載UI,你不需要再處理 那些 “再次回到屏幕上的圖像 ”。在這里一個(gè)內(nèi)存和磁盤緩存常常是有幫助的,允許組件哭訴的重新加載處理過的圖像。

這節(jié)課將引導(dǎo)你,當(dāng)加載多個(gè)圖像時(shí),使用一個(gè)內(nèi)存和磁盤圖像緩存來提高UI的響應(yīng)性和流暢性。

使用一個(gè)內(nèi)存緩存

一個(gè)內(nèi)存緩存提供了快速訪問位圖的方式,更好的占用珍貴的應(yīng)用程序內(nèi)存。LruCache 類(在Support Library 安卓支持可 API 4 中)很適合 緩存圖像的任務(wù),它以LinkedHashMap 中的強(qiáng)引用方式 保持最近被引用的對(duì)象和 在緩存數(shù)量超過指定的數(shù)量時(shí)移除最近最少使用的成員。

注意: 在過去,流行的內(nèi)存緩存的實(shí)現(xiàn)是使用SoftReference 或 WeakReference 位圖緩存,然而現(xiàn)在已經(jīng)不再推薦使用。從Android 2.3(API 級(jí)別 9)開始,垃圾回收器更激進(jìn)的回收 軟引用/弱引用,使得相當(dāng)于無效。另外 在 Android 3.0 (API 級(jí)別 11)之前,一個(gè)位圖的后臺(tái)數(shù)據(jù)被存放在原始內(nèi)存中,它不能以可預(yù)見的方式被釋放,它潛在性的導(dǎo)致一個(gè)應(yīng)用臨時(shí)的超出它的內(nèi)存限制而崩潰。

為了選擇一個(gè)合適的LruCache 的尺寸,一些因素必須要考慮到,比如:

  • 你的剩余的activity或者應(yīng)用程序 是如何 集中 你的內(nèi)存的?How memory intensive is the rest of your activity and/or application?
  • 一次加載多少圖像到屏幕上顯示? 有多少圖片即將準(zhǔn)備顯示到屏幕上?
  • 設(shè)備的屏幕尺寸和密度是多少?在內(nèi)存中顯示相同數(shù)量的圖片,一個(gè)更高級(jí)的高密度屏幕 (xhdpi)設(shè)備比如 Galaxy Nexus 比起 Nexus S (hdpi)設(shè)備 需要更多的緩存。
  • 這些圖片的尺寸規(guī)格和配置是什么,每個(gè)將占據(jù)多大的內(nèi)存?
  • 圖像被訪問的頻率?是否有些圖像被訪問的頻率比其他的高?如果這樣,或許你需要一直在內(nèi)存中保持某些圖像,后者使用多個(gè)LruCache 對(duì)象對(duì)應(yīng)多個(gè)圖像的分組。
  • 你能在質(zhì)量和數(shù)量之間保持平衡么? 有時(shí) 存儲(chǔ)大量的低質(zhì)量圖像更有用,潛在的在其他后臺(tái)線程中加載高質(zhì)量的圖像版本。

沒有適用于所有應(yīng)用程序的絕對(duì)的指定尺寸和準(zhǔn)則,由你分析你的使用情況來決定,并上升到一個(gè)合適的方案。一個(gè)緩存如果太小,則導(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)存分配了整個(gè)應(yīng)用內(nèi)存的八分之一。在一個(gè)一般 hdpi 設(shè)備中,這最小接近 4MB(32/8). 在一個(gè) 800x480分辨率 的設(shè)備中,一個(gè)全屏的 GridView被填滿圖像的話,大約需要1.5MB (8004804 bytes), 這樣將可以在內(nèi)存中緩存大約 2.5頁的圖像。

當(dāng)加載一個(gè)圖像到 ImageView 中, LruCache 緩存 將會(huì)先進(jìn)行檢查。如果找到的結(jié)果不為空,它將被直接更新到 ImageView上,否則將會(huì)產(chǎn)生一個(gè)后臺(tái)線程來處理這個(gè)圖像:

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;
    }
    ...
}

使用一個(gè)磁盤緩存

對(duì)于快速的訪問最近呈現(xiàn)過的圖像,一個(gè)內(nèi)存緩存是非常用于的,然而你不能完全保證 圖片在這個(gè)緩存中。像GridView這樣的組件有大量的數(shù)據(jù)集合,可以很容易的填滿整個(gè)內(nèi)存緩存。你的應(yīng)用可能被其他任務(wù)(比如電話來了)所打斷,和在后臺(tái)時(shí)會(huì)背傻吊和內(nèi)存緩存被銷毀。一旦用戶恢復(fù)了應(yīng)用,你的應(yīng)用需要再次處理每一個(gè)圖像。

一個(gè)磁盤緩存可以被應(yīng)用到這些場(chǎng)景,當(dāng)圖像無法在內(nèi)存緩存中可用時(shí),可以持續(xù)訪問圖像和幫助減少加載圖像的次數(shù)。當(dāng)然,從磁盤緩存中提取圖像相比較于從內(nèi)存中來說是較慢的,并且最好在后臺(tái)任務(wù)中處理,磁盤讀取次數(shù)可能不可預(yù)知。

注意: 如果訪問很頻繁,一個(gè) ContentProvider 可能更適合用于緩存圖像, 在gallery 應(yīng)用示例中 有更多演示。

下面的演示代碼使用了一個(gè) DiskLruCache 的磁盤緩存實(shí)現(xiàn),它來自于 安卓源代碼 Android source. 下面是一個(gè)更新后的示例,它在內(nèi)存緩存之外,又增加了一個(gè)磁盤緩存:

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)該在主線程中進(jìn)行。這意味著,在初始化之前有機(jī)會(huì)訪問該緩存。為了解決這個(gè)問題,在上面的實(shí)現(xiàn)中,使用了一個(gè)鎖對(duì)象,以確保在初始化完成之前不能從緩存中讀取。

這時(shí),在主UI線程中檢查內(nèi)存緩存,在后臺(tái)線程中檢查磁盤緩存。硬盤操作應(yīng)該絕對(duì)不要再UI線程中使用。當(dāng)圖像處理完成后,最后的圖片被添加到內(nèi)存緩存和磁盤緩存中。

處理配置的變化

運(yùn)行時(shí)配置變化,比如屏幕方向改變,導(dǎo)致Android銷毀和 使用新的配置 重新啟動(dòng)運(yùn)行中的activity(更多信息參考Handling Runtime Changes)。當(dāng)一個(gè)配置改變發(fā)生時(shí),你可能想不再重新處理你所有的圖片,以獲得平滑快速的用戶體驗(yàn)。

幸運(yùn)的是,在 使用內(nèi)存緩存(Use a Memory Cache ) 一節(jié)中你擁有了一個(gè)很好的圖片內(nèi)存緩存。通過一個(gè) 調(diào)用了setRetainInstance(true)的Fragment保存緩存對(duì)象,再將Fragment 傳到新的Activity示例中。在activity被重新創(chuàng)建后,這個(gè)重新創(chuàng)建的(保留的)的 Fragment 被重新附加,這樣你重新通過它獲得到緩存對(duì)象,允許圖像被快速提取和重新填充到 ImageView 對(duì)象。

下面的示例是 在配置變化時(shí),使用一個(gè)Fragment 保留一個(gè)LruCache 對(duì)象:

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);
    }
}

為了驗(yàn)證這一點(diǎn),使用 有或者沒有保留的Fragmen 方式來嘗試旋轉(zhuǎn)一個(gè)設(shè)備。你可以注意到,在圖像填充到activity上時(shí)幾乎沒有滯后,在你獲得緩存時(shí)是即刻從內(nèi)存中的。一些圖像沒有從內(nèi)存中被找到,也是有希望在磁盤緩存中找到,如果沒有找到,就會(huì)像平常那樣處理。

管理圖片內(nèi)存

除了在 緩存圖像(Caching Bitmaps) 章節(jié)描述的步驟,這里有些明確的事情可以做,以幫助垃圾回收和重用圖像。根據(jù)不同的Android版本不同有不同的推薦策略。BitmapFun 示例包含了一些類,展示了如何設(shè)計(jì)你的程序以在不同的Android版本中更有效率的工作。

為了對(duì)這節(jié)課劃分段落, 先了解Android如何管理圖片內(nèi)存的演變過程:

  • 在 Android 2.2 (API 級(jí)別 8) 及以下,當(dāng)垃圾回收發(fā)生時(shí),你的應(yīng)用的線程會(huì)暫停。這導(dǎo)致了延遲,降低了性能。Android 2.3添加了并發(fā)的垃圾回收,這意味著,失去引用的圖像的內(nèi)存很快被回收。
  • 在 Android 2.3.3 (API 級(jí)別 10) 及以下,位圖的后備的像素?cái)?shù)據(jù)被存儲(chǔ)在原生內(nèi)存中。它被和位圖本身分開,它被存儲(chǔ)在Dalvik 的堆中。 在原生內(nèi)存中的像素?cái)?shù)據(jù)部能以可預(yù)知的方式被釋放,可能導(dǎo)致一個(gè)應(yīng)用臨時(shí)的越過內(nèi)存限制而崩潰。 Android 3.0 (API 級(jí)別 11)中,像素?cái)?shù)據(jù)也被存儲(chǔ)在Dalvik 的堆中,和它關(guān)聯(lián)到的位圖一起了。
    下面的章節(jié)描述了 在不同的Android版本中如何優(yōu)化內(nèi)存的管理。

Android 2.3.3 及以下 的內(nèi)存管理

在 Android 2.3.3 (API 級(jí)別 10)及以下,推薦使用 recycle() 方法。如果你在你的應(yīng)用中顯示大量的圖像數(shù)據(jù),或許你遇到過 OutOfMemoryError 錯(cuò)誤。recycle() 方法允許你盡快的回收內(nèi)存。

警告: 當(dāng)你確定你的位圖對(duì)象不再使用的時(shí)候,你可以調(diào)用 recycle() 如果你調(diào)用了 lrecycle() ,而又試圖繪制這個(gè)位圖,你將會(huì)受到一個(gè)錯(cuò)誤: "Canvas: trying to use a recycled bitmap".

下面的代碼片段提供了一個(gè) 調(diào)用 recycle(). 的演示。它使用了引用計(jì)數(shù)(通過變量 mDisplayRefCount 和 mCacheRefCount )來追蹤 一個(gè)位圖當(dāng)前被顯示或者在緩存中。當(dāng)下列條件成立時(shí)回收?qǐng)D像:

  • 引用計(jì)數(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 級(jí)別 11) 提供了 BitmapFactory.Options.inBitmap 字段。如果這個(gè)選項(xiàng)被設(shè)置了,在加載內(nèi)容時(shí),使用了這個(gè)選項(xiàng)的解碼方法將會(huì)試圖去重用已經(jīng)存在的位圖。這意味著,位圖內(nèi)存被重用了,而提升了性能,它移除了內(nèi)存分配和回收的步驟。然而,inBitmap 也是有些已知的局限,具體的說,在Android 4.4 (API 級(jí)別 19)之前,只能尺寸大小相同的圖片才被支持重用。更多信息請(qǐng)閱讀 inBitmap 文檔。

保存位圖以備后用

下面的代碼片段演示了 如何保持一個(gè)位圖以備將來使用。在運(yùn)行在Android 3.0或者更高版本上的一個(gè)應(yīng)用中,一個(gè)圖片被從 LruCache中移除時(shí),再在一個(gè)HashSet 中放置一個(gè)位圖的軟引用,使用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()));
            }
        }
    }
....
}

使用一個(gè)已經(jīng)存在的位圖

在運(yùn)行的應(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()方法,它看起來 對(duì)一個(gè)已經(jīng)存在的位圖設(shè)置了inBitmap. 的值。注意,如果它找到了一個(gè)合適的匹配時(shí),這個(gè)方法也僅僅設(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;
}

最后,這個(gè)方法決定了 一個(gè)候選的位圖對(duì)象是否 滿足了 被用于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)容,向你展示了如何加載多個(gè)圖像到 ViewPager 和 GridView 組件中,使用了后臺(tái)線程,圖片緩存,處理并發(fā)和配置的改變。

加載圖像到 ViewPager 的實(shí)現(xiàn)

滑動(dòng)屏幕模式 ( swipe view pattern ) 是一個(gè)極好的方式來導(dǎo)航圖像畫廊的詳細(xì)視圖頁。你可以使用 一個(gè)PagerAdapter支持的ViewPager 組件來 實(shí)現(xiàn)這個(gè)模式。 然而,可能的更適合的支持適配器是 FragmentStatePagerAdapter 的子類,在從屏幕上不可見,內(nèi)存較低時(shí),它自動(dòng)的銷毀和保存 ViewPager 中的 Fragments 的狀態(tài)。

注意: 如果你只有很少的數(shù)量的圖像和確信 它們適用于應(yīng)用的內(nèi)存限制內(nèi),那么一個(gè)普通的 PagerAdapter 或 FragmentPagerAdapter 可能更合適。

下面是一個(gè) 擁有ImageView子元素的 ViewPager的實(shí)現(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);
        }
    }
}

下面是一個(gè)詳細(xì)Fragment的實(shí)現(xiàn),它持有了ImageView子控件。這可能看起來是完全合理的方式,然而你可以看到這個(gè)實(shí)現(xiàn)的缺點(diǎ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)用掛起和被強(qiáng)行關(guān)閉。使用一個(gè) AsyncTask ,像上面的課程 在UI線程外處理圖像 一課中描述的那樣,簡(jiǎn)單的移動(dòng)圖像加載和處理的過程到后臺(tái)線程中:

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ò)中提出圖像)運(yùn)行在 BitmapWorkerTask 中,不會(huì)影響主UI線程的響應(yīng)性。如果后臺(tái)線程要很多次直接從磁盤中加載圖像,那么添加一個(gè)內(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
}

將上面的片段整合在一起,為你提供了一個(gè) 高響應(yīng)的 ViewPager的實(shí)現(xiàn),使用了更好圖像記載延遲,并且有能力的 盡可能多或者盡可能少的在后臺(tái)處理圖像。

加載圖像到 GridView 中的實(shí)現(xiàn)

網(wǎng)格列表構(gòu)造塊( grid list building block )對(duì)于展示圖像數(shù)據(jù)集合是十分有用的,它可以通過GridView組件方式的實(shí)現(xiàn)。很多圖像需要一次性被加載到屏幕上,當(dāng)上下滾動(dòng)時(shí)很多圖像還需要準(zhǔn)備好被顯示。當(dāng)實(shí)現(xiàn)這樣的控件類型時(shí),你一定要確保UI仍然流暢,內(nèi)存使用率在可控內(nèi)和正確的處理并發(fā)(由于 GridView 回收它們的子視圖 的方式導(dǎo)致)

要開始,下面是一個(gè)標(biāo)準(zhǔn)的 GridView 的實(shí)現(xiàn),它擁有 ImageView 子控件,并在 Fragment內(nèi)。再一次,這看起來是完美的實(shí)現(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;
        }
    }
}

再一次,這個(gè)實(shí)現(xiàn)的問題是圖像將在UI線程里被設(shè)置。當(dāng)工作在小的,簡(jiǎn)單的圖像(由于系統(tǒng)資源加載和緩存),如果更多額外的處理需要完成,你的UI就崩潰了。

在上面章節(jié)提到的,同樣的異步處理和緩存方法可以被用于這里的實(shí)現(xiàn)。然而,由于 GridView 回收它們的子視圖,你仍然需要一個(gè)并發(fā)問題的方式。要處理它,使用 在UI線程外處理圖像 課程中討論過的技術(shù),下面是個(gè)更新后的方案:

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 中。

這個(gè)實(shí)現(xiàn)允許很靈活的處理 圖像的處理和加載,而不阻止UI的平滑。在后臺(tái)任務(wù)中,你可以從網(wǎng)絡(luò)加載圖像或者 改變大的相機(jī)照片的圖像尺寸,在任務(wù)完成后,圖像即呈現(xiàn)出來。

關(guān)于這的一個(gè)完整的示例,和這節(jié)課其他概念的討論,請(qǐng)閱讀包含的示例應(yīng)用。

(本課程完,張?jiān)骑wvir,2015-08-25)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評(píng)論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,855評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,833評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,016評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,273評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評(píng)論 1 288
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,730評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,006評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,666評(píng)論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,154評(píng)論 4 61
  • 三聯(lián)出的《上學(xué)記》,是何兆武口述的回憶錄,講了他年輕時(shí)求學(xué)時(shí)的生活經(jīng)歷。以前這類書讀的不多,對(duì)近代史相關(guān)的內(nèi)容了解...
    老杜還在閱讀 208評(píng)論 0 0
  • 還差最后一塊門板 扣下 今天就結(jié)束了 閉著眼 撫摸 親吻 這塊 歲月雕刻的木頭 多么溫暖 多么柔和 門外 人來人往...
    妤寶寶的吳越粑粑閱讀 391評(píng)論 0 0
  • 你是我最喜歡的貓咪 不管你怎么發(fā)脾氣 在我眼中都是那么的可愛 但在8月的不知是哪一天——你不見了 沒有你 就像大雄...
    萌樂寵閱讀 195評(píng)論 0 0