Android Training Caching Bitmaps 翻譯

原文:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.htmlhtml

圖片緩存android

在Android開發中,加載一個圖片到界面很容易,但若是一次加載大量圖片就複雜多了。在不少狀況下(好比:ListView,GridView或ViewPager),可以滾動的組件須要加載的圖片幾乎是無限多的。緩存

有些組件的child view在不顯示時會回收,並循環使用,若是沒有任何對bitmap的持久引用的話,垃圾回收器會釋放你加載的bitmap。這沒什麼問題,但當這些圖片再次顯示的時候,要想避免重複處理這些圖片,從而達到加載流暢的效果,就要使用內存緩存和本地緩存了,這些緩存可讓你快速加載處理過的圖片。app

這些緩存,就是本章要討論的內容。ide

使用內存緩存ui

內存緩存以犧牲內存的代價,帶來快速的圖片訪問。LruCache類(API Level 4以前可使用Support Library)很是適合圖片緩存任務,在一個LinkedHashMap中保存着對Bitmap的強引用,當緩存數量超過容器容量時,刪除最近最少使用的成員(LRU)。this

注意:在過去,很是流行用SoftReference或WeakReference來實現圖片的內存緩存,但如今再也不推薦使用這個方法了。由於從Android 2.3 (API Level 9)以後,垃圾回收器會更積極的回收soft/weak的引用,這將致使使用soft/weak引用的緩存幾乎沒有緩存效果。順帶一提,在Android3.0(API Level 11)之前,bitmap是儲存在native 內存中的,因此係統以不可預見的方式來釋放bitmap,這可能會致使短期超過內存限制從而形成崩潰。spa

爲了給LruCache一個合適的容量,須要考慮不少因素,好比:線程

  • 你其它的Activity 和/或 Application是怎樣使用內存的?
  • 屏幕一次顯示多少圖片?須要多少圖片爲顯示到屏幕作準備?
  • 屏幕的大小(size)和密度(density)是多少?像Galaxy Nexus這樣高密度(xhdpi)的屏幕在緩存相同數量的圖片時,就須要比低密度屏幕Nexus S(hdpi)更大的內存。
  • 每一個圖片的尺寸多大,相關配置怎樣的,佔用多大內存?
  • 圖片的訪問頻率高不高?不一樣圖片的訪問頻率是否不同?若是是,你可能會把某些圖片一直緩存在內存中,或須要多種不一樣緩存策略的LruCache。
  • 你能平衡圖片的質量和數量嗎?有時候,緩存多個質量低的圖片是頗有用的,而質量高的圖片應該(像下載文件同樣)在後臺任務中加載。

這裏沒有適應全部應用的特定大小或公式,只能經過分析具體的使用方法,來得出合適的解決方案。緩存過小的話沒有實際用處,還會增長額外開銷;緩存太大的話,會再一次形成OutOfMemory異常,並給應用的其餘部分留下不多的內存。code

下面是一個圖片的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);
}

注意:在這個例子中,將應用最大可以使用內存的八分之一分配給了緩存,在一個普通的或者hdpi設備上,這個值大約至少是4MB(32/8)。在一個800x480分辨率的設備上全屏顯示一個填滿圖片的GridView大約要1.5MB(800*480*4 bytes),所以,這個LruCache至少可以緩存2.5頁的圖片。

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

 

使用磁盤緩存

內存緩存可以加快對最近顯示過的圖片的訪問速度,然而你不能認爲緩存中的圖片全是有效的。像GridView這樣須要大量數據的組件是很容易填滿內存緩存的。你的應用可能會被別的任務打斷(好比一個來電),它可能會在後臺被殺掉,其內存緩存固然也被銷燬了。當用戶恢復你的應用時,應用將從新處理以前緩存的每一張圖片。

在這個情形中,使用磁盤緩存能夠持久的儲存處理過的圖片,而且縮短加載內存緩存中無效的圖片的時間。固然從磁盤加載圖片比從內存中加載圖片要慢的多,而且因爲磁盤讀取的時間是不肯定的,因此要在後臺線程進行磁盤加載。

注意:若是以更高的頻率訪問圖片,好比圖片牆應用,使用ContentProvider可能更適合儲存圖片緩存。

下面這個例子除了以前的內存緩存,還添加了一個磁盤緩存,這個磁盤緩存實現自Android源碼中的DiskLruCache:

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

注意:磁盤緩存的初始化會涉及磁盤操做,所以不該該在UI線程作初始化操做。然而,這樣作可能會出現初始化完成前,就訪問緩存的狀況。爲了解決這個問題,上面的例子使用了一個鎖對象,確保在磁盤緩存初始化完成前,不會有其餘對象使用它。

檢查內存緩存能夠在UI線程,檢查磁盤緩存最好在後臺線程。永遠不要在UI線程作磁盤操做。當圖片處理完成,應該將其添加進內存緩存和磁盤緩存中,以備未來不時之需。

相關文章
相關標籤/搜索