如果只需要載入一張圖片,那麼直接載入就可以.但是,如果要在類似ListView,GridView或者ViewPager的控制項中載入大量的圖片時,問題就會變得複雜.在使用這類控制項時,在短時間內可能會顯示在螢幕上的圖片數量是不固定的.
這類控制項會通過子View的複用來保持較低的記憶體佔用.而Garbage Collector也會在View被複用時釋放對應的Bitmap,保證這些沒用用到的Bitmap不會長期存在於記憶體中.但是為了保證控制項的流暢滑動,在一個View再次滑動出現在螢幕上時,我們需要避免圖片的重複性載入.而此時,在記憶體和磁碟上開闢一塊緩衝空間往往能夠保證圖片的快速重複載入.
使用記憶體緩衝
一塊記憶體緩衝在耗費一定應用記憶體基礎上,能夠讓快速載入圖片成為可能.而LruCache正合適用來緩衝圖片,對最近使用過的對象儲存在LinkedHashMap中,並且將最近未使用過的對象釋放.
為了給LrcCache確定一個合適的大小,有以下一些因素需要考慮:
1.應用中其他組件佔用記憶體的情況
2.有多少圖片可能會顯示在螢幕上?有多少圖片將要顯示在螢幕上?
3.螢幕的尺寸和螢幕密度是多少?與Nexus S這類高螢幕密度裝置相比,Galaxy Nexs這類超高螢幕密度的裝置,往往需要更大的緩衝空間來儲存相同數量的圖片.
4.圖片的尺寸以及其他的參數,還有每張圖片將會佔用多少記憶體.
5.圖片被訪問的頻率有多高?是否有一些圖片的訪問頻率會比另外一些更高?如果是這樣,我們可能需要將一些圖片長存於記憶體中,或者使用多個LrcCache來對不同的Bitmap進行分組.
6.我們還需要在圖片的數量和品質之間權衡.有些時候,在緩衝中存放大量的縮圖,而在後台載入高清圖片會明顯提高效率.
對每個應用來說,需要指定的緩衝大小是不一定的,這取決於我們對應用的分析並得出相應的解決方案.如果緩衝空間過小,可能會造成額外的開銷,這對整個應用並無補益;而緩衝空間過大,則可能會造成java.lang.OutOfMemory異常,並且留給其他組件使用的記憶體空間也會相應減少.
以下為初始化一個存放Bitmap的LrcCache的例子:
private LruCache<String, Bitmap> mMemoryCache;@Overrideprotected void onCreate(Bundle savedInstanceState) { ...// 擷取最大的可用空間,如果需要的空間超出這個大小,則會拋出OutOfMemory異常// LrcCache建構函式中的參數是以KB為單位的 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);// 此處緩衝大小取可用記憶體的1/8 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.// 緩衝的大小會使用KB來衡量 return bitmap.getByteCount() / 1024; } }; ...}// 將Bitmap存入緩衝public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) {// 當使用(getBitmapFromMemCache方法,根據傳入的key擷取Bitmap// 當擷取到的Bitmap為空白時,證明沒有儲存過該Bitmap// 此時將該Bitmap儲存到LrcCache中 mMemoryCache.put(key, bitmap); }}// 根據key從LrcCache中擷取對應的Bitmappublic Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key);}
注意:在這個例子中,應用記憶體的1/8被分配用作緩衝.在一台正常/高螢幕解析度的裝置上,這個緩衝的大小在4MB左右(32/8 MB).而使用800×480解析度的圖片填充一個全屏的GridView的話,大概需要1.5MB的記憶體空間(800*480*4 bytes),所以這個緩衝能夠儲存至少2.5頁的圖片.
在載入一張圖片到ImageView時,LrcCache會首先檢查這張圖片是否存在.如果圖片存在,則圖片會立即被更新到ImageView中,否則會開啟一個後台線程去載入這張圖片.
public void loadBitmap(int resId, ImageView imageView) {// 將圖片的資源id轉換為String型,作為key final String imageKey = String.valueOf(resId);// 根據key從LruCache中擷取Bitmap final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) {// 如果擷取到的Bitmap不為空白// 則直接將擷取到的Bitmap更新到ImageView中 mImageView.setImageBitmap(bitmap); } else {// 否則,則先在ImageView中設定一張佔位圖 mImageView.setImageResource(R.drawable.image_placeholder);// 再開啟一個新的非同步任務去載入圖片 BitmapWorkerTask task = new BitmapWorkerTask(mImageView); task.execute(resId); }}
BitmapWorkerTask也需要更新,將Bitmap以索引值對的形式儲存到LrcCache中.
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // 在後台載入圖片 @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100));// 將Bitmap對象以索引值對的形式儲存到LrcCache中 addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } ...}
使用磁碟緩衝
記憶體緩衝在訪問最近使用過的圖片方面能夠極大地提高效率,但是我們不能指望所有需要的圖片都能在記憶體緩衝中找到.向GridView這類資料來源中有大量資料的控制項,會輕易的就將記憶體緩衝佔用滿.而我們的應用也可能會被其他的任務打斷(切換到後台),例如電話中,而當我們的應用被切換到後台時,它極有可能會被關閉,此時記憶體緩衝也會被銷毀.當使用者返回我們的應用時,應用又需要重新載入需要的圖片.
而磁碟緩衝會在記憶體緩衝被銷毀時繼續載入圖片,這樣當記憶體緩衝不可用但是又需要載入圖片時就能夠減少載入的時間.當然,從磁碟上讀取圖片要比從記憶體中讀取圖片慢,而且需要在後台線程中執行,因為圖片的載入時間是不一定的.
注意:如果緩衝圖片需要經常訪問,則將這些緩衝圖片儲存到ContentProvider是一個更好的選擇,例庫應用就是這麼做的.
以下樣本是一個DiskLruCache的實現(Android source).這個樣本是在記憶體緩衝的基礎上又增加了磁碟緩衝.
private DiskLruCache mDiskLruCache;private final Object mDiskCacheLock = new Object();private boolean mDiskCacheStarting = true;private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MBprivate static final String DISK_CACHE_SUBDIR = "thumbnails";@Overrideprotected void onCreate(Bundle savedInstanceState) { ...// 初始化記憶體緩衝 ... // 在後台線程初始化磁碟緩衝 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(); // 喚醒等待中的線程 } return null; }}class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // 在後台解析圖片 @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // 在後台線程中判斷圖片是否已經存在於磁碟緩衝中 Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // 不存在於磁碟緩衝中 // 則正常載入圖片 final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // 將載入出的圖片添加到緩衝中 addBitmapToCache(imageKey, bitmap); return bitmap; } ...}public void addBitmapToCache(String key, Bitmap bitmap) {// 將圖片添加到記憶體緩衝中 if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } // 同時將圖片添加到磁碟緩衝中 synchronized (mDiskCacheLock) { if (mDiskLruCache != null && mDiskLruCache.get(key) == null) { mDiskLruCache.put(key, bitmap); } }}public Bitmap getBitmapFromDiskCache(String key) { synchronized (mDiskCacheLock) {// 當磁碟緩衝正在初始化時,則等待 while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); } catch (InterruptedException e) {} } if (mDiskLruCache != null) { return mDiskLruCache.get(key); } } return null;}// 當外部儲存空間可用時,則在應用指定檔案夾中建立一個唯一的子檔案夾作為緩衝目錄// 而當外部裝置不可用時,則使用內建儲存空間public static File getDiskCacheDir(Context context, String uniqueName) {// 檢查外部儲存空間是否可用,如果可用則使用外部儲存空間的緩衝目錄// 否則使用內部儲存空間的緩衝目錄 final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName);}
注意:既然磁碟緩衝的操作涉及到磁碟操作,則此處的所有過程不能在UI線程中執行.同時,這也意味著在磁碟緩衝初始化完畢以前是能夠被訪問的.為瞭解決這個問題,上述方法中添加了一個鎖,這個鎖保證了磁碟緩衝在初始化完畢之前不會被應用讀取.
儘管記憶體緩衝的檢查工作可以在UI線程中執行,磁碟緩衝的檢察工作則必須在後台線程中執行.設計磁碟的操作無論如何不應該在UI線程中執行.當圖片載入成功,得到的圖片會添加到這兩個緩衝中去以待使用.
處理配置的更改
當運行時,配置發生了改變,例如螢幕方向的變化.這種變化會使Android系統摧毀並且使用新的配置重建當前正在執行的Activity(有關此方面的更多介紹,請查看Handling Runtime Changes).為了使使用者有一個順暢的體驗,我們需要避免重新載入所有的圖片.
幸運的時,我們有一個不錯的記憶體緩衝,這個記憶體緩衝可以通過調用Fragment的setRetainInstance(true)方法儲存並且傳遞到新的Activity中.當Activity被重建後,這個Fragment可以重新依附到新的Activity上,這樣我們就可以使用已經存在的記憶體緩衝,快速擷取圖片並展示在ImageView中.
以下是通過Fragment實現保留LruCache的代碼:
private LruCache<String, Bitmap> mMemoryCache;@Overrideprotected void onCreate(Bundle savedInstanceState) { ...// 得到一個用於儲存LruCache的Fragment RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager());// 取出Fragment的LruCache mMemoryCache = retainFragment.mRetainedCache; if (mMemoryCache == null) {// 如果LruCache為空白,則原先沒有緩衝// 需要建立並初始化一個LruCache mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual }// 將建立的LruCache存放到Fragment中 retainFragment.mRetainedCache = mMemoryCache; } ...}class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache<String, Bitmap> mRetainedCache; public RetainFragment() {}// 建立或者從FragmentManager中得到儲存LruCache的Fragment public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {// 根據tag從FragmentManager中擷取對應的Fragment RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); if (fragment == null) {// 如果Fragment為空白,則原先沒有該Fragment// 即表明原先沒有LruCache// 此時需要建立一個Fragment用於存放LruCache fragment = new RetainFragment();// 並將Fragment添加到FragmentManager中 fm.beginTransaction().add(fragment, TAG).commit(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);// 設定當Activity被重建時,Fragment重新依附到Activity上 setRetainInstance(true); }}
為了驗證一下效果(是否重新將Fragment依附到Activity上),我們可以旋轉一下螢幕.你會發現當我們通過Fragment儲存了記憶體緩衝,重建了Activity後重新取出圖片幾乎沒有延時.在記憶體緩衝中沒有的圖片很可能在磁碟緩衝上會有,如果磁碟緩衝中也沒有,則會正常載入需要的圖片.