注:本文翻譯自Google官方的Android Developers Training文檔,譯者技術一般,由于喜愛安卓而產生了翻譯的念頭,純屬個人興趣愛好。
原文鏈接: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
向你的應用中加載一個單一的位圖是很直接的行為,然而當你需要一次性加載一組圖像的大集合時,事情會變得更加復雜。在很多情況下(比如對于 ListView , GridView 或者 ViewPager ),屏幕上顯示的圖片以及會因加載動作而進入屏幕的圖片,這兩者的總數加起來是無法限制的。
通過對移除屏幕區域的子View進行回收,可以讓這類組件內存使用降低下來。垃圾回收器也會對那些假定你將不再需要的引用對象進行回收和釋放。這些措施都很好,但是為了保持流暢地和快速地加載UI,你會希望避免多次連續地處理這些圖片,當它們回到屏幕區域中來時。一個存儲或磁盤緩存可以在這方面提供幫助,它可以讓組件迅速的重新加載處理過的圖片。
這節課將會教你使用一個存儲和磁盤緩存,來提升你的UI加載多個圖片時的響應和流暢性。
一). 使用一個內存緩存
一個內存緩存提供了快速訪問位圖的方法,但它的代價是需要消耗掉珍貴的應用內存。 LruCache 類(在 Support Library 也有,可以支持到API Level 4及以上的平臺)對于緩存圖片來說尤其適合,它能將最近引用的對象存儲在一個基于強引用的 LinkedHashMap 中,并且在緩存超出它的特定大小后,將最近最遲被引用的對象去除。
Note:
在過去,一個流行的內存緩存實現是 SoftReference 或者 WeakReference 的位圖緩存,然而,這并不是推薦的實現方法。從Android 2.3(API Level 9)開始,垃圾回收器對于軟引用和弱引用的回收變得更加地激進,從而使得它們的效用正在下降。從Android 3.0(API Level 11)開始,存儲于本機內存的位圖數據并不是以一個可預測的形式釋放的,這就有潛在的可能性導致一個應用超出它的內存限制進而崩潰。
為了為一個 LruCache 選擇合適的大小,一些因素需要考量,例如:
- 你的activity或應用剩余的存儲壓力是如何的?
- 同一時間有多少應用顯示在屏幕上?有多少需要準備就緒顯示到屏幕上?
- 設備的屏幕的尺寸和密度的大小是多少?一個極高密度的屏幕(xhdpi)的設備(比如 Galaxy Nexus )可能相對于其他比如hdpi的設備(比如 Nexus S )需要更大的緩存來容納同樣數量的照片。
- 位圖文件的尺寸和屬性是怎樣的,需要消耗多少大的內存空間?
- 圖片被訪問的頻率高不高?有沒有一些圖片被訪問你的頻率比其它的要高?如果有,也許你會期望讓這些項目一直保留在內存或者為不同被訪問頻率的圖片設置多組 LruCache 對象。
- 能否做到數量和質量間的平衡?有些時候存儲大量低質量的圖片時很有用的,而將更高質量的圖片加載任務放在后臺執行。
沒有什么特定的大小或者公式能夠適合所有的應用,你應該自己分析并決定你的用法和解決方案。一個過小的緩存會導致大量無益處的執行操作,而太大的緩存會導致
java.lang.OutOfMemory
異常,或者讓你剩下的應用只有有限的存儲來工作。
下面是一個 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:
在這個例子中,八分之一的應用內存被分配給了我們的緩存。在一個標準或hdpi的設備上,這大約為4MB左右(32/8)。一個全屏的 GridView ,在一個分辨率為800x480的設備上,充滿圖片之后,會使用掉大約1.5MB( 800*480*4字節 ),所以這個緩存至少大約能放下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 這樣的組件其較大的數據集可以迅速填充內存緩存。同時,你的應用可能會被另一個事務打斷,如一個來電,此時在后臺中,它可能會被殺掉,這樣的話內存緩存就被銷毀了。一旦這個用戶恢復了,你的應用不得不重新處理這些圖片。
一個磁盤緩存可以在這種情況下發揮效用,它能保持處理過的位圖文件,并減少在內存緩存中不再可以獲得的加載時間。當然,從磁盤獲取圖片比從內存獲取圖片要慢,由于磁盤讀寫的速度有很多不確定性,故應該在后臺線程中執行。
Note:
一個 ContentProvider 是一個比較合適的存儲緩存圖片的地方,對于那些訪問頻率較高的圖片來說,例如在圖庫的應用中。
下面的代碼使用了 DiskLruCache 的實現,它來自于 Android source 。并且添加到內存緩存的代碼中,更新其功能:
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:
因為初始化磁盤緩存也需要磁盤操作所以它也不能再主線程中執行。然而,這其實意味著緩存有可能在還未初始化的時候就被訪問了。為了解決這個問題,在上面的代碼實現中,一個信號量(lock)保證了應用會在初始化完成之后才去讀取緩存。
雖然內存緩存在UI線程中檢查,磁盤緩存是在后臺線程中檢查。磁盤操作不應該發生在UI線程中執行。當圖片處理完成了,最后位圖將會同時添加到內存和磁盤緩存中,以備將來使用。
三). 處理配置變更
運行時的配置變更,如屏幕方向變化,會導致Android銷毀當前activity,并以新的配置重啟activity(可以閱讀: Handling Runtime Changes )。你一定希望避免重復處理圖像,這樣的話用戶就能在配置改變時,擁有平滑快速地使用體驗。
幸運的是,你在之前的章節中,已經擁有了一個很出色的圖片內存緩存了。這個緩存可以通過使用一個 Fragment (該 Fragment 通過調用 setRetainInstance(true) 將其自身保留),傳遞給新的activity實例。在activity重新創建之后,這個保留的 Fragment 就完成了重新依附( reattach ),同時你獲得了現有緩存對象的訪問,允許圖片快速提取并填充到 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 和不保留 Fragment 的情況下旋轉設備。你應該能注意到當保留了緩存時,圖片填充到activity時幾乎沒有延遲。那些在內存緩存中找不到的圖片一般都會在磁盤緩存中找到,如果找不到,這些圖片就會像平常一樣處理。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
