Bitmap Cache
譯文出自谷歌安卓官網圖片緩存篇
- 緩存位圖
- 使用內存緩存Use a Memory Cache
- 使用磁盤緩存Use a Disk Cache
- 處理配置發生改變的情況Handle Configuration Changes
緩存位圖
加載一個簡單的位圖到UI中比較快,但是如果加載一些大的位圖的話,則會變得很復雜。在許多情況下(ListView,GridView,ViewPager),屏幕上的圖片與即將滾動到屏幕上的圖片個數一般是無限制的。通過回收移除屏幕外的子視圖可以讓內存使用率降下來。假設你不做任何長引用的話,垃圾收集器也會釋放你已加載的位圖。一切都很好,但是如果你想要保持一個流動的并快速加載的UI的話,你必須避免重復加載這些即將滾動回屏幕的圖片。內存緩存與磁盤緩存可以做到這些,允許組件快速加載已處理過的圖片。
使用內存緩存(Use a Memory Cache)
內存緩存以占用應用程序一些內存為條件提供了快速獲取位圖的方式。LruCache類被用來做緩存圖片任務的,讓最近引用的對象保存到強引用LinkedHashMap中,并在超出緩存大小之前刪除最近最少使用的成員。
注意:在過去,流行的內存緩存實現是SoftReference 或 WeakReference位圖緩存,但是現在不推薦使用了。從Android2.3開始,垃機回收器會更喜歡回收soft/weak references,這導致了它們變得無效了。除此之外,在Android3.0之前,備份的位圖數據被保存在本地內存中,釋放方式是不可預測的,可能導致應用程序快速的超過內存限制并導致崩潰。
為了選擇合適的LruCache大小,一些因素我們需要考慮:
- Activity或者Application剩余內存
- 多少張圖片會立即顯示到屏幕上,還有多少會滾動到屏幕中。
- 設備的屏幕大小與密度,像Galaxy Nexus這種高分辨率xhdpi比起Nexus S來說,需要更大的緩存。
- Bitmap的大小與類型決定單張圖片所需內存大小
- 圖片獲取的頻率,哪些圖片獲取的更頻繁,如果這樣,你可能需要將固定的某些圖片放入內存中,或者為多組Bitmap分配多個LruCache
- 質量與數量之間衡量,有時候后臺加載高質量的圖片的時候,我們存儲低質量的圖片更好一些。
Android沒有指定固定的大小與格式用于適合所有應用程序,它完全由你自己去分析使用并提出合適的解決方案。緩存太小毫無用處并造成負擔,緩存過大可能會引起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); } 注意:在這個例子中,八分之一應用程序內存分配給了我們的緩存。在一般或高分辨率設備上,至少為4M內存(32/8)。800*480分辨率的設備上的全屏GridView大概會使用1.5MB (800*480*4 bytes),因此會內存中會至少緩存2.5頁圖片。當將Bitmap加入ImageView的時候,LruCache會先被檢查。如果內存中存在此Bitmap,則會用它更新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用于獲取Bitmap并將獲取得Bitmap存入LruCache中:
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;}... }使用磁盤緩存(Use a Disk Cache)
內存緩存可以加速獲取最近瀏覽過的Bitmap,但是你不能完全依賴內存緩存。像GridView這樣攜帶大數據的組件很容易將內存緩存填滿。而且,你的應用程序可能被外來程序打斷如電話,此時背景線程可能被殺死,內存可能被銷毀。一旦用戶回過頭的時候,你的應用又得重新處理每張圖片了。這些情況發生的時候,我們可以使用磁盤緩存來保存已經加載過的圖片,在內存緩存不在的情況下,降低圖片加載時間。當然,從磁盤中提取圖片比從內存中加載圖片要慢一些并且需要為其開啟額外線程用于處理。因為磁盤的讀寫是不可預測的。
注意:如果頻繁的訪問圖片,ContentProvider可能是最好的地方來存儲緩存圖片。例如相冊程序例子:
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); } 注意:初始化磁盤緩存需要磁盤操作,因此這些操作不能在主線程中執行。但是這意味著在磁盤緩存初始化之前,可能會訪問緩存。為了處理這個問題,通過鎖對象確保應用程序在磁盤初始化之后,才可以從磁盤緩存中讀取。雖然內存緩存在UI線程中被檢查,但是磁盤緩存在背景線程中被檢查。磁盤操作永遠不能在主線程中發生。當圖像被處理完全之后,Bitmap被添加到內存緩存與磁盤緩存中。
處理配置發生改變的情況(Handle Configuration Changes)
運行配置發生改變,例如屏幕旋轉改變,會造成Android銷毀Activity并重啟帶有新配置的Activity。如果當配置發生改變的時候,你想要避免再次處理你的圖片以便用戶能夠有一個平滑和快速的體驗效果。你可以使用上述提到的內存緩存機制,借助于中間組件Fragment的方法setRetainInstance(true)來保存內存換,然后實現將其傳遞到新的Activity中。當Activity被重建之后,通過Fragment獲取內存緩存對象,并且從其中提取對象到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;}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setRetainInstance(true);} }總結
以上是生活随笔為你收集整理的Bitmap Cache的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android N新特性
- 下一篇: 在非UI线程中显示Toast