Android緩存Bitmap

2018-08-02 17:35 更新

編寫:kesenhoo - 原文:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

將單個Bitmap加載到UI是簡單直接的,但是如果我們需要一次性加載大量的圖片,事情則會變得復(fù)雜起來。在大多數(shù)情況下(例如在使用ListView,GridView或ViewPager時),屏幕上的圖片和因滑動將要顯示的圖片的數(shù)量通常是沒有限制的。

通過循環(huán)利用子視圖可以緩解內(nèi)存的使用,垃圾回收器也會釋放那些不再需要使用的Bitmap。這些機(jī)制都非常好,但是為了保證一個流暢的用戶體驗,我們希望避免在每次屏幕滑動回來時,都要重復(fù)處理那些圖片。內(nèi)存與磁盤緩存通??梢云鸬捷o助作用,允許控件可以快速地重新加載那些處理過的圖片。

這一課會介紹在加載多張Bitmap時使用內(nèi)存緩存與磁盤緩存來提高響應(yīng)速度與UI流暢度。

使用內(nèi)存緩存(Use a Memory Cache)

內(nèi)存緩存以花費(fèi)寶貴的程序內(nèi)存為前提來快速訪問位圖。LruCache類(在API Level 4的Support Library中也可以找到)特別適合用來緩存Bitmaps,它使用一個強(qiáng)引用(strong referenced)的LinkedHashMap保存最近引用的對象,并且在緩存超出設(shè)置大小的時候剔除(evict)最近最少使用到的對象。

Note: 在過去,一種比較流行的內(nèi)存緩存實現(xiàn)方法是使用軟引用(SoftReference)或弱引用(WeakReference)對Bitmap進(jìn)行緩存,然而我們并不推薦這樣的做法。從Android 2.3 (API Level 9)開始,垃圾回收機(jī)制變得更加頻繁,這使得釋放軟(弱)引用的頻率也隨之增高,導(dǎo)致使用引用的效率降低很多。而且在Android 3.0 (API Level 11)之前,備份的Bitmap會存放在Native Memory中,它不是以可預(yù)知的方式被釋放的,這樣可能導(dǎo)致程序超出它的內(nèi)存限制而崩潰。

為了給LruCache選擇一個合適的大小,需要考慮到下面一些因素:

  • 應(yīng)用剩下了多少可用的內(nèi)存?
  • 多少張圖片會同時呈現(xiàn)到屏幕上?有多少圖片需要準(zhǔn)備好以便馬上顯示到屏幕?
  • 設(shè)備的屏幕大小與密度是多少?一個具有特別高密度屏幕(xhdpi)的設(shè)備,像Galaxy Nexus會比Nexus S(hdpi)需要一個更大的緩存空間來緩存同樣數(shù)量的圖片。
  • Bitmap的尺寸與配置是多少,會花費(fèi)多少內(nèi)存?
  • 圖片被訪問的頻率如何?是其中一些比另外的訪問更加頻繁嗎?如果是,那么我們可能希望在內(nèi)存中保存那些最常訪問的圖片,或者根據(jù)訪問頻率給Bitmap分組,為不同的Bitmap組設(shè)置多個LruCache對象。
  • 是否可以在緩存圖片的質(zhì)量與數(shù)量之間尋找平衡點?某些時候保存大量低質(zhì)量的Bitmap會非常有用,加載更高質(zhì)量圖片的任務(wù)可以交給另外一個后臺線程。

通常沒有指定的大小或者公式能夠適用于所有的情形,我們需要分析實際的使用情況后,提出一個合適的解決方案。緩存太小會導(dǎo)致額外的花銷卻沒有明顯的好處,緩存太大同樣會導(dǎo)致java.lang.OutOfMemory的異常,并且使得你的程序只留下小部分的內(nèi)存用來工作(緩存占用太多內(nèi)存,導(dǎo)致其他操作會因為內(nèi)存不夠而拋出異常)。

下面是一個為Bitmap建立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);
}

Note:在上面的例子中, 有1/8的內(nèi)存空間被用作緩存。 這意味著在常見的設(shè)備上(hdpi),最少大概有4MB的緩存空間(32/8)。如果一個填滿圖片的GridView控件放置在800x480像素的手機(jī)屏幕上,大概會花費(fèi)1.5MB的緩存空間(800x480x4 bytes),因此緩存的容量大概可以緩存2.5頁的圖片內(nèi)容。

當(dāng)加載Bitmap顯示到ImageView 之前,會先從LruCache 中檢查是否存在這個Bitmap。如果確實存在,它會立即被用來顯示到ImageView上,如果沒有找到,會觸發(fā)一個后臺線程去處理顯示該Bitmap任務(wù)。

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 需要把解析好的Bitmap添加到內(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;
    }
    ...
}

使用磁盤緩存(Use a Disk Cache)

內(nèi)存緩存能夠提高訪問最近用過的Bitmap的速度,但是我們無法保證最近訪問過的Bitmap都能夠保存在緩存中。像類似GridView等需要大量數(shù)據(jù)填充的控件很容易就會用盡整個內(nèi)存緩存。另外,我們的應(yīng)用可能會被類似打電話等行為而暫停并退到后臺,因為后臺應(yīng)用可能會被殺死,那么內(nèi)存緩存就會被銷毀,里面的Bitmap也就不存在了。一旦用戶恢復(fù)應(yīng)用的狀態(tài),那么應(yīng)用就需要重新處理那些圖片。

磁盤緩存可以用來保存那些已經(jīng)處理過的Bitmap,它還可以減少那些不再內(nèi)存緩存中的Bitmap的加載次數(shù)。當(dāng)然從磁盤讀取圖片會比從內(nèi)存要慢,而且由于磁盤讀取操作時間是不可預(yù)期的,讀取操作需要在后臺線程中處理。

Note:如果圖片會被更頻繁的訪問,使用ContentProvider或許會更加合適,比如在圖庫應(yīng)用中。

這一節(jié)的范例代碼中使用了一個從Android源碼中剝離出來的DiskLruCache。改進(jìn)過的范例代碼在已有內(nèi)存緩存的基礎(chǔ)上增加磁盤緩存的功能。

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

Note:因為初始化磁盤緩存涉及到I/O操作,所以它不應(yīng)該在主線程中進(jìn)行。但是這也意味著在初始化完成之前緩存可以被訪問。為了解決這個問題,在上面的實現(xiàn)中,有一個鎖對象(lock object)來確保在磁盤緩存完成初始化之前,應(yīng)用無法對它進(jìn)行讀取。

內(nèi)存緩存的檢查是可以在UI線程中進(jìn)行的,磁盤緩存的檢查需要在后臺線程中處理。磁盤操作永遠(yuǎn)都不應(yīng)該在UI線程中發(fā)生。當(dāng)圖片處理完成后,Bitmap需要添加到內(nèi)存緩存與磁盤緩存中,方便之后的使用。

處理配置改變(Handle Configuration Changes)

如果運(yùn)行時設(shè)備配置信息發(fā)生改變,例如屏幕方向的改變會導(dǎo)致Android中當(dāng)前顯示的Activity先被銷毀然后重啟。(關(guān)于這一方面的更多信息,請參考Handling Runtime Changes)。我們需要在配置改變時避免重新處理所有的圖片,這樣才能提供給用戶一個良好的平滑過度的體驗。

幸運(yùn)的是,在前面介紹使用內(nèi)存緩存的部分,我們已經(jīng)知道了如何建立內(nèi)存緩存。這個緩存可以通過調(diào)用setRetainInstance(true))保留一個Fragment實例的方法把緩存?zhèn)鬟f給新的Activity。在這個Activity被重新創(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);
    }
}

為了測試上面的效果,可以嘗試在保留Fragment與沒有這樣做的情況下旋轉(zhuǎn)屏幕。我們會發(fā)現(xiàn)當(dāng)保留緩存時,從內(nèi)存緩存中重新繪制幾乎沒有延遲的現(xiàn)象。 內(nèi)存緩存中沒有的圖片可能存儲在磁盤緩存中。如果兩個緩存中都沒有,則圖像會像平時正常流程一樣被處理。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號