【Google官方教程】第三課:緩存Bitmap

聲明:Ryan的博客文章歡迎您的轉載,但在轉載的同時,請註明文章的來源出處,不勝感激! :-)  html

http://my.oschina.net/ryanhoo/blog/88443
java

譯者:Ryan Hoo android

來源:https://developer.android.com/develop/index.html
緩存

譯者按: 在Google最新的文檔中,提供了一系列含金量至關高的教程。由於種種緣由而不爲人知,真是惋惜!Ryan將會細心整理,將之翻譯成中文,但願對開發者有所幫助。 app

        本系列是Google關於展現大Bitmap(位圖)的官方演示,能夠有效的解決內存限制,更加有效的加載並顯示圖片,同時避免讓人頭疼的OOM(Out Of Memory)。
ide

------------------------------------------------------------------------------------- 測試

譯文: ui

        加載一個Bitmap(位圖)到你的UI界面是很是簡單的,可是若是你要一次加載一大批,事情就變得複雜多了。在大多數的狀況下(如ListView、GridView或者ViewPager這樣的組件),屏幕上的圖片以及立刻要在滾動到屏幕上顯示的圖片的總量,在本質上是不受限制的。 this

        像這樣的組件在子視圖移出屏幕後會進行視圖回收,內存使用仍被保留。但假設你不保留任何長期存活的引用,垃圾回收器也會釋放你所加載的Bitmap。這天然再好不過了,可是爲了保持流暢且快速加載的UI,你要避免繼續在圖片回到屏幕上的時候從新處理。使用內存和硬盤緩存一般能解決這個問題,使用緩存容許組件快速加載並處理圖片。
spa

        這節課將帶你使用內存和硬盤緩存Bitmap,以在加載多個Bitmap的時候提高UI的響應性和流暢性。

使用內存緩存

        以犧牲寶貴的應用內存爲代價,內存緩存提供了快速的Bitmap訪問方式。LruCache類(能夠在Support Library中獲取並支持到API  Level 4以上,即1.6版本以上)是很是適合用做緩存Bitmap任務的,它將最近被引用到的對象存儲在一個強引用的LinkedHashMap中,而且在緩存超過了指定大小以後將最近不常使用的對象釋放掉。

        注意:之前有一個很是流行的內存緩存實現是SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap緩存方案,然而如今已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更着重於對軟/弱引用的回收,這使得上述的方案至關無效。此外,Android 3.0(API Level 11)以前的版本中,Bitmap的備份數據直接存儲在本地內存中並以一種不可預測的方式從內存中釋放,極可能短暫性的引發程序超出內存限制而崩潰。

        爲了給LruCache選擇一個合適的大小,要考慮到不少緣由,例如:

  • 其餘的Activity(活動)和(或)程序都是很耗費內存的嗎?
  • 屏幕上一次會顯示多少圖片?有多少圖片將在屏幕上顯示?
  • 設備的屏幕大小和密度是多少?一個超高清屏幕(xhdpi)的設備如Galaxy Nexus,相比Nexus S(hdpi)來講,緩存一樣數量的圖片須要更大的緩存空間。
  • Bitmap的尺寸、配置以及每張圖片須要佔用多少內存?
  • 圖片的訪問是否頻繁?有些會比其餘的更加被頻繁的訪問到嗎?若是是這樣,也許你須要將某些圖片一直保留在內存中,甚至須要多個LruCache對象分配給不一樣組的Bitmap。
  • 你能平衡圖片的質量和數量麼?有的時候存儲大量低質量的圖片更加有用,而後能夠在後臺任務中加載另外一個高質量版本的圖片。

        對於設置緩存大小,並無適用於全部應用的規範,它取決於你在內存使用分析後給出的合適的解決方案。緩存空間過小並沒有益處,反而會引發額外的開銷,而太大了又可能再次引發java.lang.OutOfMemory異常或只留下很小的空間給應用的其餘程序運行。   

        這裏有一個設置Bitmap的LruCache示例:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get memory class of this device, exceeding this amount will throw an
    // OutOfMemory exception.
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = 1024 * 1024 * memClass / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

        注意:在這個例子中,1/8的應用內存被分配給緩存。在一個普通的/hdpi設備上最低也在4M左右(32/8)。一個分辨率爲800*480的設備上,全屏的填滿圖片的GridView佔用的內存約1.5M(800*480*4字節),所以這個大小的內存能夠緩存2.5頁左右的圖片。

        當加載一個Bitmap到ImageView中,先要檢查LruCache。若是有相應的數據,則當即用來更新ImageView,不然將啓動後臺線程來處理這個圖片。

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也須要更新內存中的數據:

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

使用硬盤緩存

        一個內存緩存對加速訪問最近瀏覽過的Bitmap很是有幫助,可是你不能侷限於內存中的可用圖片。GridView這樣有着更大的數據集的組件能夠很輕易消耗掉內存緩存。你的應用有可能在執行其餘任務(如打電話)的時候被打斷,而且在後臺的任務有可能被殺死或者緩存被釋放。一旦用戶從新聚焦(resume)到你的應用,你得再次處理每一張圖片。

        在這種狀況下,硬盤緩存能夠用來存儲Bitmap並在圖片被內存緩存釋放後減少圖片加載的時間(次數)。固然,從硬盤加載圖片比內存要慢,而且應該在後臺線程進行,由於硬盤讀取的時間是不可預知的。

        注意:若是訪問圖片的次數很是頻繁,那麼ContentProvider可能更適合用來存儲緩存圖片,例如Image Gallery這樣的應用程序。

        這個類中的示例代碼使用DiskLruCache(來自Android源碼)實現。在示例代碼中,除了已有的內存緩存,還添加了硬盤緩存。

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

        注意:即使是硬盤緩存初始化也須要硬盤操做,所以不該該在主線程執行。可是,這意味着硬盤緩存在初始化前就能被訪問到。爲了解決這個問題,在上面的實現中添加了一個鎖對象(lock object),以確保在緩存被初始化以前應用沒法訪問硬盤緩存。

        在UI線程中檢查內存緩存,相應的硬盤緩存檢查應在後臺線程中進行。硬盤操做永遠不要在UI線程中發生。當圖片處理完成後,最終的Bitmap要被添加到內存緩存和硬盤緩存中,以便後續的使用。

 處理配置更改

        運行時的配置會發生變化,例如屏幕方向的改變,會致使Android銷燬並以新的配置從新啓動Activity(關於此問題的更多信息,請參閱Handling Runtime Changes)。爲了讓用戶有着流暢而快速的體驗,你須要在配置發生改變的時候避免再次處理全部的圖片。

        幸運的是,你在「使用內存緩存」一節中爲Bitmap構造了很好的內存緩存。這些內存能夠經過使用Fragment傳遞到信的Activity(活動)實例,這個Fragment能夠調用setRetainInstance(true)方法保留下來。在Activity(活動)被從新建立後,你能夠在上面的Fragment中訪問到已經存在的緩存對象,使得圖片能快加載並從新填充到ImageView對象中。

        下面是一個使用FragmentLruCache對象保留在配置更改中的示例:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment mRetainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        mRetainFragment.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();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}
        爲了測試這個,能夠在不適用Fragment的狀況下旋轉設備屏幕。在保留緩存的狀況下,你應該能發現填充圖片到Activity中幾乎是瞬間從內存中取出而沒有任何延遲的感受。任何圖片優先從內存緩存獲取,沒有的話再到硬盤緩存中找,若是都沒有,那就以普通方式加載圖片。
相關文章
相關標籤/搜索