轉自:https://my.oschina.net/rengwuxian/blog/184650
摘要: 假設你開發了一個聊天程序,它的好友列表中顯示從網絡獲取的好友頭像。可是如果用戶發現每次進入好友列表的時候,程序都要重新下載頭像才能進行顯示,甚至當把列表滑動到底部再重新滑動回頂部的時候,剛才已經加載完成了的頭像竟然又變成了空白圖片開始重新加載,這將是一種糟糕的用戶體驗。為了解決這種問題,你需要使用高速緩存技術——Cache。
應用的場景
假設你開發了一個聊天程序,它的好友列表中顯示從網絡獲取的好友頭像。可是如果用戶發現每次進入好友列表的時候,程序都要重新下載頭像才能進行顯示,甚至當把列表滑動到底部再重新滑動回頂部的時候,剛才已經加載完成了的頭像竟然又變成了空白圖片開始重新加載,這將是一種糟糕的用戶體驗。為了解決這種問題,你需要使用高速緩存技術——Cache。
什么是Cache?
Cache,高速緩存,原意是指計算機中一塊比內存更高速容量更小的存儲器。更廣義地說,Cache指對於最近使用過的信息的可高速讀取的存儲塊。而本文要講的Cache技術,指的就是將最近使用過的Bitmap緩存在手機的內存與磁盤中,來實現再次使用Bitmap時的瞬時加載,以節省用戶的時間和手機流量。
下面將針對Android中的兩種Cache類型Memory Cache和Disk Cache分別進行介紹。樣例代碼取自Android開發者站。
1/2:Memory Cache(內存中的Cache)
Memory Cache使用內存來為應用程序提供Cache。由於內存的讀寫速度非常快,所以我們應該優先使用它(相對於下面將介紹的Disk Cache來說)。
Android中提供了LruCache類來進行Memory Cache的管理(該類是在Android 3.1時推出的,但我們可以使用android -support-v4.jar的兼容包來對低版本的手機提供支持)。
提示:有人習慣使用SoftReference和WeakReference來做Memory Cache,但谷歌官方不建議這么做。因為自從Android2.3之后,Android中的GC變得更加積極,導致這種做法中緩存的Bitmaps非常容易被回收掉;另外,在Android3.0之前,Bitmap的數據是直接分配在native memory中,它的釋放是不受dalvik控制的,因此更容易導致內存的溢出。如果你喜歡簡單粗暴的總結,那就是:反正不要用這種方法來管理Memory Cache。
下面我們看一段為Bitmap設置LruCache的代碼:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // 獲取虛擬機可用內存(內存占用超過該值的時候,將報OOM異常導致程序崩潰)。最后除以1024是為了以kb為單位 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用可用內存的1/8來作為Memory Cache final int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重寫sizeOf()方法,使用Bitmap占用內存的kb數作為LruCache的size 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來提供給Memory Cache,我們簡單分析一下這個值。一個普通屏幕尺寸、hdpi的手機的可用內存為32M,那么他的Memory Cache為32M/8=4M。通常hdpi的手機為480*800像素,它一個全屏Bitmap占用內存為480*800*4B=1536400B≈1.5M。那么4M的內存為大約2.5個屏幕大小的bitmap提供緩存。同理,一個普通尺寸、xhdpi大小的720*1280的手機可以為大約2.2個屏幕大小的bitmap提供緩存。
當一個ImageView需要設置一個bitmap的時候,LruCache會進行檢查,如果它已經緩存了相應的bitmap,它就直接取出來並設置給這個ImageView;否則,他將啟動一個后台線程加載這個Bitmap:
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在加載完成后,通過前面的addBitmapToMemoryCache()方法把這個bitmap進行緩存:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // 后台加載Bitmap @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } ... }
2/2:Disk Cache(磁盤中的Cache)
前面已經提到,Memory Cache的優點是讀寫非常快。但它的缺點就是容量太小了,而且不能持久化,所以在用戶在滑動GridView時它很快會被用完,而且切換多個界面時或者是關閉程序重新打開后,再次進入原來的界面,Memory Cache是無能為力的。這個時候,我們就要用到Disk Cache了。
Disk Cache將緩存的數據放在磁盤中,因此不論用戶是頻繁切換界面,還是關閉程序,Disk Cache是不會消失的。
實際上,Android SDK中並沒有一個類來實現Disk Cache這樣的功能。但google其實已經提供了實現代碼:DiskLruCache。我們只要把它搬到自己的項目中就可以了。
下面請看一段使用DiskLruCache來配合Memory Cache進行圖片緩存的代碼:
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) { ... // 初始化memory cache ... // 開啟后台線程初始化disk cache 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; // 初始化完成 mDiskCacheLock.notifyAll(); // 喚醒被hold住的線程 } return null; } } class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // 在后台加載圖片 @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // 通過后台線程檢查disk cache Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // 如果沒有在disk cache中發現這個bitmap // 加載這個bitmap final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // 把這個bitmap加入cache addBitmapToCache(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // 把bitmap加入memory cache if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } // 同樣,也加入disk cache synchronized (mDiskCacheLock) { if (mDiskLruCache != null && mDiskLruCache.get(key) == null) { mDiskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (mDiskCacheLock) { // 等待disk cache初始化完畢 while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); } catch (InterruptedException e) {} } if (mDiskLruCache != null) { return mDiskLruCache.get(key); } } return null; } // 在自帶的cache目錄下建立一個獨立的子目錄。優先使用外置存儲。但如果外置存儲不存在,使用內置存儲。 public static File getDiskCacheDir(Context context, String uniqueName) { // 如果MEDIA目錄已經掛載或者外置存儲是手機自帶的(Nexus設備都這么干),使用外置存儲;否則使用內置存儲 final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
提示:由於disk cache的初始化是耗時操作,所以這個過程被放在了后台進程。而由此導致的結果是,主線程有可能在它初始化完成之前就嘗試讀取disk cache,這會導致程序出錯。因此以上代碼中使用了synchronized關鍵字和一個lock對象來確保在初始化完成之前disk cache不會被訪問。(什么是synchronized?文章最后會有介紹)
上面這段代碼看起來比較多,但大致讀一下就會發現,它的思路非常簡單:1.讀取cache的時候,優先讀取memory cache,讀不到的時候再讀取disk cache;2.把bitmap保存到cache中的時候,memory cache和disk cache都要保存。
至此,使用Cache來緩存Bitmap的方法就介紹完了。把這套思路使用在你的項目中,用戶體驗會馬上大大增強的。