450
技術社區[雲棲]
高效地顯示Bitmap圖片 3 - 兩種緩存Bitmap的方式
https://blog.csdn.net/kesenhoo/article/details/7491588
Caching Bitmaps [緩存位圖]
- 加載單個Bitmap到UI是簡單直接的,但是如果你需要一次加載大量的圖片,事情則會變得複雜起來。在大多數情況下(例如在
ListView
,GridView
orViewPager
), 顯示圖片的數量通常是沒有限製的。 - 通過循環利用子視圖可以抑製內存的使用,GC(garbage collector)也會釋放那些不再需要使用的bitmap。這些機製都非常好,但是為了保持一個流暢的用戶體驗,你想要在屏幕滑回來時避免每次重複處理那些圖片。內存與磁盤緩存通常可以起到幫助的作用,允許組件快速的重新加載那些處理過的圖片。
- 這一課會介紹在加載多張位圖時使用內存Cache與磁盤Cache來提高反應速度與UI的流暢度。
Use a Memory Cache [使用內存緩存]
-
內存緩存以花費寶貴的程序內存為前提來快速訪問位圖。
LruCache
類(在 Support Library 中也可以找到) 特別合適用來caching bitmaps,用一個strong referenced的LinkedHashMap
來保存最近引用的對象,並且在Cache超出設置大小的時候踢出(evict)最近最少使用到的對象。- Note: 在過去, 一個比較流行的內存緩存實現方法是使用
SoftReference
orWeakReference
, 然而這是不推薦的。 - 從Android 2.3 (API Level 9) 開始,GC變得更加頻繁的去釋放soft/weak references,這使得他們就顯得效率低下[容易被GC掉又不斷的創建]。 而且在Android 3.0 (API Level 11)之前,備份的bitmap是存放在native memory 中,它不是以可預知的方式被釋放,這樣可能導致程序超出它的內存限製而崩潰。
- Note: 在過去, 一個比較流行的內存緩存實現方法是使用
-
為了給LruCache選擇一個合適的大小,有下麵一些因素需要考慮到:
- 你的程序剩下了多少可用的內存?
- 多少圖片會被一次呈現到屏幕上?有多少圖片需要準備好以便馬上顯示到屏幕?
- 設備的屏幕大小與密度是多少? 一個具有特別高密度屏幕(xhdpi)的設備,像 Galaxy Nexus 會比 Nexus S (hdpi)需要一個更大的Cache來hold住同樣數量的圖片.
- 位圖的尺寸與配置是多少,會花費多少內存?
- 圖片被訪問的頻率如何?是其中一些比另外的訪問更加頻繁嗎?如果是,也許你想要保存那些最常訪問的到內存中,或者為不同組別的位圖(按訪問頻率分組)設置多個
LruCache
對象。 - 你可以平衡質量與數量嗎? 某些時候保存大量低質量的位圖會非常有用,在另外一個後台任務中加載更高質量的圖片。
- 沒有指定的大小與公式能夠適用與所有的程序,那取決於分析你的使用情況後提出一個合適的解決方案。一個太小的Cache會導致額外的花銷卻沒有明顯的好處,一個太大的Cache同樣會導致
java.lang.OutOfMemory的異常[Cache占用太多內存,其他活動則會因為內存不夠而異常],並且使得你的程序隻留下小部分的內存用來工作。
-
下麵是一個為bitmap建立
LruCache 的示例:
- private LruCache 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(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);
- }
- Note: 在上麵的例子中, 有1/8的程序內存被作為Cache. 在一個常見的設備上(hdpi),最小大概有4MB (32/8). 一個全屏的
GridView
組件,如果被800x480像素的圖片填滿大概會花費1.5MB (800*480*4 bytes), 因此這大概最少可以緩存2.5張圖片到內存中.
- Note: 在上麵的例子中, 有1/8的程序內存被作為Cache. 在一個常見的設備上(hdpi),最小大概有4MB (32/8). 一個全屏的
- 當加載位圖到
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
也需要做添加到內存Cache中的動作:
- class BitmapWorkerTask extends AsyncTask {
- ...
- // 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 [使用磁盤緩存]
- 內存緩存能夠提高訪問最近查看過的位圖,但是你不能保證這個圖片會在Cache中。像類似
GridView
等帶有大量數據的組件很容易就填滿內存Cache。你的程序可能會被類似Phone call等任務而中斷,這樣後台程序可能會被殺死,那麼內存緩存就會被銷毀。一旦用戶恢複前麵的狀態,你的程序就又需要為每個圖片重新處理。 -
磁盤緩存磁盤緩存可以用來保存那些已經處理好的位圖,並且在那些圖片在內存緩存中不可用時減少加載的次數。當然從磁盤讀取圖片會比從內存要慢,而且讀取操作需要在後台線程中處理,因為磁盤讀取操作是不可預期的。
- Note: 如果圖片被更頻繁的訪問到,也許使用
ContentProvider
會更加的合適,比如在Gallery程序中。
- Note: 如果圖片被更頻繁的訪問到,也許使用
- 在下麵的sample code中實現了一個基本的
DiskLruCache
。然而,Android 4.0 的源代碼提供了一個更加robust並且推薦使用的DiskLruCache
方案。(libcore/luni/src/main/java/libcore/io/DiskLruCache.java
). 因為向後兼容,所以在前麵發布的Android版本中也可以直接使用。 (quick search 提供了一個實現這個解決方案的示例)。
- private DiskLruCache mDiskCache;
- 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
- ...
- File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
- mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
- ...
- }
- class BitmapWorkerTask extends AsyncTask {
- ...
- // 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(String.valueOf(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
- if (!mDiskCache.containsKey(key)) {
- mDiskCache.put(key, bitmap);
- }
- }
- public Bitmap getBitmapFromDiskCache(String key) {
- return mDiskCache.get(key);
- }
- // 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 getCacheDir(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.getExternalStorageState() == Environment.MEDIA_MOUNTED
- || !Environment.isExternalStorageRemovable() ?
- context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
- return new File(cachePath + File.separator + uniqueName);
- }
- 內存緩存的檢查是可以在UI線程中進行的,磁盤緩存的檢查需要在後台線程中處理。磁盤操作永遠都不應該在UI線程中發生。當圖片處理完成後,最後的位圖需要添加到內存緩存與磁盤緩存中,方便之後的使用。
Handle Configuration Changes [處理配置改變]
- 運行時配置改變,例如屏幕方向的改變會導致Android去destory並restart當前運行的Activity。(關於這一行為的更多信息,請參考 Handling Runtime Changes). 你想要在配置改變時避免重新處理所有的圖片,這樣才能提供給用戶一個良好的平滑過度的體驗。
- 幸運的是,在前麵介紹 Use a Memory Cache 的部分,你已經知道如何建立一個內存緩存。這個緩存可以通過使用一個Fragment去調用
setRetainInstance(true)
傳遞到新的Activity中。在這個activity被recreate之後, 這個保留的Fragment
會白重新附著上。這樣你就可以訪問Cache對象,從中獲取到圖片信息並快速的重新添加到ImageView對象中。 - 下麵配置改變時使用Fragment來重新獲取
LruCache
的示例:
- private LruCache mMemoryCache;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- ...
- RetainFragment mRetainFragment =
- RetainFragment.findOrCreateRetainFragment(getFragmentManager());
- mMemoryCache = RetainFragment.mRetainedCache;
- if (mMemoryCache == null) {
- mMemoryCache = new LruCache(cacheSize) {
- ... // Initialize cache here as usual
- }
- mRetainFragment.mRetainedCache = mMemoryCache;
- }
- ...
- }
- class RetainFragment extends Fragment {
- private static final String TAG = "RetainFragment";
- public LruCache 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);
- }
- }
- 為了測試上麵的效果,嚐試對比retaining 這個
Fragment
.與沒有這樣做的時候去旋轉屏幕。你會發現從內存緩存中重新繪製幾乎沒有卡的現象,而從磁盤緩存則顯得稍慢,如果兩個緩存中都沒有,則處理速度像平時一樣。
最後更新:2017-04-04 07:03:06