【Google官方教程】第三課:快取Bitmap

yangxi_001發表於2013-11-26

在Google最新的文件中,提供了一系列含金量相當高的教程。因為種種原因而鮮為人知,真是可惜!Ryan將會細心整理,將之翻譯成中文,希望對開發者有所幫助。

        本系列是Google關於展示大Bitmap(點陣圖)的官方演示,可以有效的解決記憶體限制,更加有效的載入並顯示圖片,同時避免讓人頭疼的OOM(Out Of Memory)。

-------------------------------------------------------------------------------------

譯文:

        載入一個Bitmap(點陣圖)到你的UI介面是非常簡單的,但是如果你要一次載入一大批,事情就變得複雜多了。在大多數的情況下(如ListView、GridView或者ViewPager這樣的元件),螢幕上的圖片以及馬上要在滾動到螢幕上顯示的圖片的總量,在本質上是不受限制的。

        像這樣的元件在子檢視移出螢幕後會進行檢視回收,記憶體使用仍被保留。但假設你不保留任何長期存活的引用,垃圾回收器也會釋放你所載入的Bitmap。這自然再好不過了,但是為了保持流暢且快速載入的UI,你要避免繼續在圖片回到螢幕上的時候重新處理。使用記憶體和硬碟快取通常能解決這個問題,使用快取允許元件快速載入並處理圖片。

        這節課將帶你使用記憶體和硬碟快取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示例:

01 private LruCache<String, Bitmap> mMemoryCache;
02  
03 @Override
04 protected void onCreate(Bundle savedInstanceState) {
05     ...
06     // Get memory class of this device, exceeding this amount will throw an
07     // OutOfMemory exception.
08     final int memClass = ((ActivityManager) context.getSystemService(
09             Context.ACTIVITY_SERVICE)).getMemoryClass();
10  
11     // Use 1/8th of the available memory for this memory cache.
12     final int cacheSize = 1024 1024 * memClass / 8;
13  
14     mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
15         @Override
16         protected int sizeOf(String key, Bitmap bitmap) {
17             // The cache size will be measured in bytes rather than number of items.
18             return bitmap.getByteCount();
19         }
20     };
21     ...
22 }
23  
24 public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
25     if (getBitmapFromMemCache(key) == null) {
26         mMemoryCache.put(key, bitmap);
27     }
28 }
29  
30 public Bitmap getBitmapFromMemCache(String key) {
31     return mMemoryCache.get(key);
32 }

        注意:在這個例子中,1/8的應用記憶體被分配給快取。在一個普通的/hdpi裝置上最低也在4M左右(32/8)。一個解析度為800*480的裝置上,全屏的填滿圖片的GridView佔用的記憶體約1.5M(800*480*4位元組),因此這個大小的記憶體可以快取2.5頁左右的圖片。

        當載入一個Bitmap到ImageView中,先要檢查LruCache。如果有相應的資料,則立即用來更新ImageView,否則將啟動後臺執行緒來處理這個圖片。

01 public void loadBitmap(int resId, ImageView imageView) {
02     final String imageKey = String.valueOf(resId);
03  
04     final Bitmap bitmap = getBitmapFromMemCache(imageKey);
05     if (bitmap != null) {
06         mImageView.setImageBitmap(bitmap);
07     else {
08         mImageView.setImageResource(R.drawable.image_placeholder);
09         BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
10         task.execute(resId);
11     }
12 }

        BitmapWorkerTask也需要更新記憶體中的資料:

01 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
02     ...
03     // Decode image in background.
04     @Override
05     protected Bitmap doInBackground(Integer... params) {
06         final Bitmap bitmap = decodeSampledBitmapFromResource(
07                 getResources(), params[0], 100100));
08         addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
09         return bitmap;
10     }
11     ...
12 }

使用硬碟快取

        一個記憶體快取對加速訪問最近瀏覽過的Bitmap非常有幫助,但是你不能侷限於記憶體中的可用圖片。GridView這樣有著更大的資料集的元件可以很輕易消耗掉記憶體快取。你的應用有可能在執行其他任務(如打電話)的時候被打斷,並且在後臺的任務有可能被殺死或者快取被釋放。一旦使用者重新聚焦(resume)到你的應用,你得再次處理每一張圖片。

        在這種情況下,硬碟快取可以用來儲存Bitmap並在圖片被記憶體快取釋放後減小圖片載入的時間(次數)。當然,從硬碟載入圖片比記憶體要慢,並且應該在後臺執行緒進行,因為硬碟讀取的時間是不可預知的。

        注意:如果訪問圖片的次數非常頻繁,那麼ContentProvider可能更適合用來儲存快取圖片,例如Image Gallery這樣的應用程式。

        這個類中的示例程式碼使用DiskLruCache(來自Android原始碼)實現。在示例程式碼中,除了已有的記憶體快取,還新增了硬碟快取。

01 private DiskLruCache mDiskLruCache;
02 private final Object mDiskCacheLock = new Object();
03 private boolean mDiskCacheStarting = true;
04 private static final int DISK_CACHE_SIZE = 1024 1024 10// 10MB
05 private static final String DISK_CACHE_SUBDIR = "thumbnails";
06  
07 @Override
08 protected void onCreate(Bundle savedInstanceState) {
09     ...
10     // Initialize memory cache
11     ...
12     // Initialize disk cache on background thread
13     File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
14     new InitDiskCacheTask().execute(cacheDir);
15     ...
16 }
17  
18 class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
19     @Override
20     protected Void doInBackground(File... params) {
21         synchronized (mDiskCacheLock) {
22             File cacheDir = params[0];
23             mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
24             mDiskCacheStarting = false// Finished initialization
25             mDiskCacheLock.notifyAll(); // Wake any waiting threads
26         }
27         return null;
28     }
29 }
30  
31 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
32     ...
33     // Decode image in background.
34     @Override
35     protected Bitmap doInBackground(Integer... params) {
36         final String imageKey = String.valueOf(params[0]);
37  
38         // Check disk cache in background thread
39         Bitmap bitmap = getBitmapFromDiskCache(imageKey);
40  
41         if (bitmap == null) { // Not found in disk cache
42             // Process as normal
43             final Bitmap bitmap = decodeSampledBitmapFromResource(
44                     getResources(), params[0], 100100));
45         }
46  
47         // Add final bitmap to caches
48         addBitmapToCache(imageKey, bitmap);
49  
50         return bitmap;
51     }
52     ...
53 }
54  
55 public void addBitmapToCache(String key, Bitmap bitmap) {
56     // Add to memory cache as before
57     if (getBitmapFromMemCache(key) == null) {
58         mMemoryCache.put(key, bitmap);
59     }
60  
61     // Also add to disk cache
62     synchronized (mDiskCacheLock) {
63         if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
64             mDiskLruCache.put(key, bitmap);
65         }
66     }
67 }
68  
69 public Bitmap getBitmapFromDiskCache(String key) {
70     synchronized (mDiskCacheLock) {
71         // Wait while disk cache is started from background thread
72         while (mDiskCacheStarting) {
73             try {
74                 mDiskCacheLock.wait();
75             catch (InterruptedException e) {}
76         }
77         if (mDiskLruCache != null) {
78             return mDiskLruCache.get(key);
79         }
80     }
81     return null;
82 }
83  
84 // Creates a unique subdirectory of the designated app cache directory. Tries to use external
85 // but if not mounted, falls back on internal storage.
86 public static File getDiskCacheDir(Context context, String uniqueName) {
87     // Check if media is mounted or storage is built-in, if so, try and use external cache dir
88     // otherwise use internal cache dir
89     final String cachePath =
90             Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
91                     !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
92                             context.getCacheDir().getPath();
93  
94     return new File(cachePath + File.separator + uniqueName);
95 }

        注意:即便是硬碟快取初始化也需要硬碟操作,因此不應該在主執行緒執行。但是,這意味著硬碟快取在初始化前就能被訪問到。為了解決這個問題,在上面的實現中新增了一個鎖物件(lock object),以確保在快取被初始化之前應用無法訪問硬碟快取。

        在UI執行緒中檢查記憶體快取,相應的硬碟快取檢查應在後臺執行緒中進行。硬碟操作永遠不要在UI執行緒中發生。當圖片處理完成後,最終的Bitmap要被新增到記憶體快取和硬碟快取中,以便後續的使用。

 處理配置更改

        執行時的配置會發生變化,例如螢幕方向的改變,會導致Android銷燬並以新的配置重新啟動Activity(關於此問題的更多資訊,請參閱Handling Runtime Changes)。為了讓使用者有著流暢而快速的體驗,你需要在配置發生改變的時候避免再次處理所有的圖片。

        幸運的是,你在“使用記憶體快取”一節中為Bitmap構造了很好的記憶體快取。這些記憶體可以通過使用Fragment傳遞到信的Activity(活動)例項,這個Fragment可以呼叫setRetainInstance(true)方法保留下來。在Activity(活動)被重新建立後,你可以在上面的Fragment中訪問到已經存在的快取物件,使得圖片能快載入並重新填充到ImageView物件中。

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

01 private LruCache<String, Bitmap> mMemoryCache;
02  
03 @Override
04 protected void onCreate(Bundle savedInstanceState) {
05     ...
06     RetainFragment mRetainFragment =
07             RetainFragment.findOrCreateRetainFragment(getFragmentManager());
08     mMemoryCache = RetainFragment.mRetainedCache;
09     if (mMemoryCache == null) {
10         mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
11             ... // Initialize cache here as usual
12         }
13         mRetainFragment.mRetainedCache = mMemoryCache;
14     }
15     ...
16 }
17  
18 class RetainFragment extends Fragment {
19     private static final String TAG = "RetainFragment";
20     public LruCache<String, Bitmap> mRetainedCache;
21  
22     public RetainFragment() {}
23  
24     public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
25         RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
26         if (fragment == null) {
27             fragment = new RetainFragment();
28         }
29         return fragment;
30     }
31  
32     @Override
33     public void onCreate(Bundle savedInstanceState) {
34         super.onCreate(savedInstanceState);
35         setRetainInstance(true);
36     }
37 }
        為了測試這個,可以在不適用Fragment的情況下旋轉裝置螢幕。在保留快取的情況下,你應該能發現填充圖片到Activity中幾乎是瞬間從記憶體中取出而沒有任何延遲的感覺。任何圖片優先從記憶體快取獲取,沒有的話再到硬碟快取中找,如果都沒有,那就以普通方式載入圖片。 

相關文章