【Google官方教程】第三课:缓存Bitmap
2019獨角獸企業重金招聘Python工程師標準>>>
轉載聲明:Ryan的博客文章歡迎您的轉載,但在轉載的同時,請注明文章的來源出處,不勝感激! :-)?
http://my.oschina.net/ryanhoo/blog/88443
譯者:Ryan Hoo
來源:https://developer.android.com/develop/index.html
譯者按:?在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異?;蛑涣粝潞苄〉目臻g給應用的其他程序運行。???
????????這里有一個設置Bitmap的LruCache示例:
private LruCache<String, Bitmap> 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<String, Bitmap>(cacheSize) {@Overrideprotected 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); }
??注意:在這個例子中,1/8的應用內存被分配給緩存。在一個普通的/hdpi設備上最低也在4M左右(32/8)。一個分辨率為800*480的設備上,全屏的填滿圖片的GridView占用的內存約1.5M(800*480*4字節),因此這個大小的內存可以緩存2.5頁左右的圖片。
????????當加載一個Bitmap到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;}... }使用硬盤緩存
????? ??一個內存緩存對加速訪問最近瀏覽過的Bitmap非常有幫助,但是你不能局限于內存中的可用圖片。GridView這樣有著更大的數據集的組件可以很輕易消耗掉內存緩存。你的應用有可能在執行其他任務(如打電話)的時候被打斷,并且在后臺的任務有可能被殺死或者緩存被釋放。一旦用戶重新聚焦(resume)到你的應用,你得再次處理每一張圖片。
????????在這種情況下,硬盤緩存可以用來存儲Bitmap并在圖片被內存緩存釋放后減小圖片加載的時間(次數)。當然,從硬盤加載圖片比內存要慢,并且應該在后臺線程進行,因為硬盤讀取的時間是不可預知的。
????????注意:如果訪問圖片的次數非常頻繁,那么ContentProvider可能更適合用來存儲緩存圖片,例如Image Gallery這樣的應用程序。
????????這個類中的示例代碼使用DiskLruCache(來自Android源碼)實現。在示例代碼中,除了已有的內存緩存,還添加了硬盤緩存。
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); }注意:即便是硬盤緩存初始化也需要硬盤操作,因此不應該在主線程執行。但是,這意味著硬盤緩存在初始化前就能被訪問到。為了解決這個問題,在上面的實現中添加了一個鎖對象(lock object),以確保在緩存被初始化之前應用無法訪問硬盤緩存。
????????在UI線程中檢查內存緩存,相應的硬盤緩存檢查應在后臺線程中進行。硬盤操作永遠不要在UI線程中發生。當圖片處理完成后,最終的Bitmap要被添加到內存緩存和硬盤緩存中,以便后續的使用。
?處理配置更改
????????運行時的配置會發生變化,例如屏幕方向的改變,會導致Android銷毀并以新的配置重新啟動Activity(關于此問題的更多信息,請參閱Handling Runtime Changes)。為了讓用戶有著流暢而快速的體驗,你需要在配置發生改變的時候避免再次處理所有的圖片。
????????幸運的是,你在“使用內存緩存”一節中為Bitmap構造了很好的內存緩存。這些內存可以通過使用Fragment傳遞到信的Activity(活動)實例,這個Fragment可以調用setRetainInstance(true)方法保留下來。在Activity(活動)被重新創建后,你可以在上面的Fragment中訪問到已經存在的緩存對象,使得圖片能快加載并重新填充到ImageView對象中。
????????下面是一個使用Fragment將LruCache對象保留在配置更改中的示例:
private LruCache<String, Bitmap> mMemoryCache;@Override protected void onCreate(Bundle savedInstanceState) {...RetainFragment mRetainFragment =RetainFragment.findOrCreateRetainFragment(getFragmentManager());mMemoryCache = RetainFragment.mRetainedCache;if (mMemoryCache == null) {mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {... // Initialize cache here as usual}mRetainFragment.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();}return fragment;}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setRetainInstance(true);} } ?為了測試這個,可以在不適用Fragment的情況下旋轉設備屏幕。在保留緩存的情況下,你應該能發現填充圖片到Activity中幾乎是瞬間從內存中取出而沒有任何延遲的感覺。任何圖片優先從內存緩存獲取,沒有的話再到硬盤緩存中找,如果都沒有,那就以普通方式加載圖片。 ?轉載于:https://my.oschina.net/u/559701/blog/88596
總結
以上是生活随笔為你收集整理的【Google官方教程】第三课:缓存Bitmap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 读“Agile Method – by
- 下一篇: 学用MVC4做网站二:2.2添加用户组