Android官方开发文档Training系列课程中文版:高效显示位图之位图缓存
原文地址:http://android.xsoftlab.net/training/displaying-bitmaps/cache-bitmap.html
往UI界面中加載單張圖片的過程是很簡單的,然而如果需要在某個時刻同時加載大量的圖片,那么這事情就有些復雜了。在很多情況下,比如使用了ListView、GridView或者是ViewPager來展示一定數量的圖片,在本質上這些情況下,屏幕的快速滑動會導致大量的圖片被集中展示在屏幕上。
類似這樣通過回收移除到屏幕之外的子View的組件會抑制內存的使用(也就是說它們本身不會濫用內存)。垃圾回收器還會釋放你所加載的位圖,假設你沒有使用任何持久化引用的話。這真是極好的,但是為了保持流暢的UI效果,你可能需要在它們每次重新返回到屏幕的時候,對它們按照常規的方式重新處理。內存緩存及磁盤緩存可以在這里提供幫助,可以使這些組件快速的重新加載已經處理過的圖片。
這節課將會討論在加載多張圖片的時候,如何通過使用內存緩存以及磁盤緩存來使UI界面更加流暢,響應速度更快。
使用內存緩存
內存緩存提供了一種快速訪問位圖的能力,不過這會花費寶貴的內存空間。類LruCache極其適合用來處理緩存圖片的任務,它會將最近使用到的位圖的引用存放在一個LinkedHashMap對象上,并會在超過內存設計大小之前將最后一個沒有用到的成員給驅除。
Note: 在過去,使用SoftReference或者是WeakReference來緩存圖片是最受歡迎的一種緩存方式,然而卻并不推薦這么用。在Android 2.3之后,垃圾回收器對soft/weak引用的回收更加強制,這會使得這些引用幾乎無效。此外,在Android 3.0之前,位圖的字節數據被存儲在本地內存中,可以預見這些數據是不會被釋放的,這會導致程序很容易超過自身的內存限制,然后崩潰。
為了給LruCache選擇合適的尺寸,有幾個因素應該被考慮在內:
- Activity或者程序在常規狀態下的內存使用量是多少?
- 在同一時間最多會有多少圖片集中顯示在屏幕上?有多少內存需要為準備顯示到屏幕上的圖片所用?
- 設備屏幕的大小和尺寸分別是多少?在加載相同圖片數量的情況下,像Galaxy Nexus這種超高的密度(xhdpi)的設備與Nexus S(hdpi)相比則需要更大的內存。
- 圖片的尺寸多大?配置是什么?加載這個位圖的時候需要花費的內存是多少?
- 圖片的訪問有多頻繁?會比其它位圖訪問更頻繁嗎?如果是這樣,可能你需要將它們永遠保持在內存中了,或者甚至是有多個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) {@Overrideprotected 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在全屏狀態下的800*480的設備上所占的內存大概是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.@Overrideprotected Bitmap doInBackground(Integer... params) {final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), params[0], 100, 100));addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);return bitmap;}... }使用磁盤緩存
內存緩存對于最近瀏覽過的圖像的快速加載非常有用,然而卻不能將所有的圖像都存放在內存緩存中。像GridView這樣的組件在加載大數據集的時候可以輕易的將內存緩存填滿。程序在運行的過程中可能會被其它任務打斷,比如一個來電,這時,在后臺的任務可能就會被殺死,內存緩存也會被銷毀。一旦用戶返回了界面,那么程序就需要再次重新處理每張圖片。
那么磁盤緩存在這些情況下就很有幫助了,它可以存儲處理過的圖片,并會輔助提升圖片的加載時間,在圖片不再在內存緩存中存在的時候。當然,在磁盤上獲取一張圖片要比內存中要慢,并且還需要開啟單獨的工作線程,這和從磁盤上讀取數據的時間一樣,都不可預估。
Note:ContentProvider可能更適合用來存放被緩存過的圖像,如果這些圖像的訪問更加頻繁的話,就像在相冊應用中的情況一樣。
從Android Source中更新的示例代碼使用了一個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 threadFile cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);new InitDiskCacheTask().execute(cacheDir);... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> {@Overrideprotected Void doInBackground(File... params) {synchronized (mDiskCacheLock) {File cacheDir = params[0];mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);mDiskCacheStarting = false; // Finished initializationmDiskCacheLock.notifyAll(); // Wake any waiting threads}return null;} } class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {...// Decode image in background.@Overrideprotected Bitmap doInBackground(Integer... params) {final String imageKey = String.valueOf(params[0]);// Check disk cache in background threadBitmap bitmap = getBitmapFromDiskCache(imageKey);if (bitmap == null) { // Not found in disk cache// Process as normalfinal Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), params[0], 100, 100));}// Add final bitmap to cachesaddBitmapToCache(imageKey, bitmap);return bitmap;}... } public void addBitmapToCache(String key, Bitmap bitmap) {// Add to memory cache as beforeif (getBitmapFromMemCache(key) == null) {mMemoryCache.put(key, bitmap);}// Also add to disk cachesynchronized (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 threadwhile (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 dirfinal String cachePath =Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :context.getCacheDir().getPath();return new File(cachePath + File.separator + uniqueName); }Note:因為磁盤緩存的初始化需要磁盤操作,所以這個過程不應該放在UI線程中執行。然而,這也意味著在緩存初始化之前這是個訪問的機會。為了做到這一點,需要有個lock對象來保證在緩存被初始化之前APP沒有從磁盤緩存中讀取數據。
內存緩存在UI線程中執行檢查,磁盤緩存在后臺線程中執行檢查。磁盤操作絕不應該放入UI線程。當圖像處理完畢后,最終被處理過的圖片應當被添加到內存緩存及磁盤緩存中以便備用。
處理配置變更
如果在運行時發生了變更,比如屏幕的方向發生了改變,會引起Android銷毀并重啟運行中的Activity,你可能想要避免再一次處理圖像,這樣一旦配置發生了改變,可以使用戶有一個流暢快速的用戶體驗。
幸運的是,你有一個非常贊的內存緩存方案:可以使用設置了setRetainInstance(true)的Fragment,它可以將緩存傳入新的Activity實例。在activity重新創建的時候,這個被保留存在的Fragment會被重新附加在Activity上,你可以獲得原先內存緩存的訪問能力,這使得圖像可以快速的被獲得并被重新填充在ImageView對象中。
下面這個例子使用了引用LruCache的Fragment,并通過了配置更改的問題:
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;}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setRetainInstance(true);} }為了測試這項輸出,試著在有和沒有Fragment的情況下旋轉設備。你應該會注意到這個過程幾乎沒有延遲。任何圖像如果沒有在內存緩存中找到,那么這就為磁盤緩存提供了用武之地,如果都沒有的話,那么常規的處理方法就會出場。
總結
以上是生活随笔為你收集整理的Android官方开发文档Training系列课程中文版:高效显示位图之位图缓存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手把手教你-如何查询中文期刊是否属于核心
- 下一篇: KnowledgeGraph Slide