在app中通常最占內存、占流量的元素就是圖片了,圖片往往又無處不在,特別是伴隨着list,GridView或者ViewPager出現,這些圖片隨着你的滑動操作,時而出現在你的屏幕中,時而消失在屏幕之外。
對應滑出屏幕之外的圖片,你可以緩存在內存中以便下次加載快速渲染,但這回增加內存的開銷,你也可以立即釋放掉這部分內存,但下次加載會變的很慢,因為來講回收影響UI渲染,獲取圖片資源更加事一個耗時的過程。所以怎么樣才能做到節省內存的開銷又能提高加載速度?這是一個策略平衡問題,取決於你如何去使用 memory cache和disk cache來緩存Bitmap對象。
- 使用Memory Cache(軟引用、弱引用還在流行?)
memory cache 能使你從你的應用內存空間快速的訪問到你的Bitmap對象。對應Bitmap的緩存,LruCache(Least Recently Used)應運而生,關於LruCache的介紹請看官方文檔https://developer.android.com/reference/android/util/LruCache.html(翻牆),簡單的說
LruCache使用強引用方式把最近使用的內存對象使用LinkedHashMap存儲起來,在你使用LruCache時需要設置一個最大緩存值,當內存即將接近這個最大值的時候,它將幫你把那些 Least Recently Used 的內存對象釋放掉。在過去,一個通常的 memory cache 實現基本上是使用軟引用或弱引用來緩存bitmap,然而現在已經不推薦使用了,為什么呢?一、從 android 2.3 以后,垃圾回收器對應軟引用和弱引用的回收變動十分積極,這使得緩存的意義在極大程度上丟失;二, 在android 3.0 以前bitmpa的內存是存儲在native內存中的,使得垃圾回收器很難回收,對應內存的預算很難把握。
使用LruCache,那么對於最大緩存值設置是一門藝術,你需要考慮很多因素。例如:
- 你的 activity 使用了多少內存?
- 有多少張圖片會同時出現在你的屏幕中?
- 你的緩存的圖片被訪問的頻率是多少?
- 你對圖片顯示清新度的取舍?
總之,沒有一個固定的值適合所有的app,取決於你的app的具體身的很多因素,設置太小可能會降低你使用LruCache的福利,設置太大,在緩存峰值時候可能會引起OOM,這里有個例子參考:
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) { @Override protected 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); }
在這個例子中,使用了應用最大內存的1/8最為LruCache的最大值。
加載Bitmap對象到ImageView的經典模型
通常我們會先到 LruCache 中去檢測一下存不存在,如果存在直接更新ImageView;如果不存在則開啟一個線程去獲取Bitmap對象(通常是到網絡上獲取,也有可能從disk中讀取),然后再把這個Bitmap對象緩存到LruCache中。例如:
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); } }
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // 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; } ... }
- 使用disk緩存(硬盤緩存)
memory cache 在快速訪問Bitmap上十分有用,然而我們不能一直依賴它,為什么呢?對於像GridView這樣承載大量圖片的組件來說,memory cache 會很快就被使用殆盡。另外當我們的應用被切換到后台的時候或者像來電話等這樣的高優先級應用啟用的時候,我們的app內存很可能會被回收,甚至LruCache對象也可能會銷毀,一旦app再次切換到前台的話,所有的Bitmap對象都重新獲取(通常網絡請求),從而影響體驗而且耗費流量。於是DiskLruCache出場了,關於DiskLruCache實現源碼,有興趣深究的可以點擊這里查看。先來看一個在使用LruCache的基礎上使用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 thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> { @Override protected Void doInBackground(File... params) { synchronized (mDiskCacheLock) { File cacheDir = params[0]; mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); mDiskCacheStarting = false; // Finished initialization mDiskCacheLock.notifyAll(); // Wake any waiting threads } return null; } } class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // 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(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 synchronized (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 thread while (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 dir final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
注意:所有的disk讀取操作都不應該發生在UI線程中,當從網絡中獲取Bitmap對象后應該同時保存到LruCache中和LruDiskCache中以便后續使用。
推薦: