若是隻須要加載一張圖片,那麼直接加載就能夠.可是,若是要在相似ListView,GridView或者ViewPager的控件中加載大量的圖片時,問題就會變得複雜.在使用這類控件時,在短期內可能會顯示在屏幕上的圖片數量是不固定的.html
這類控件會經過子View的複用來保持較低的內存佔用.而Garbage Collector也會在View被複用時釋放對應的Bitmap,保證這些沒用用到的Bitmap不會長期存在於內存中.可是爲了保證控件的流暢滑動,在一個View再次滑動出如今屏幕上時,咱們須要避免圖片的重複性加載.而此時,在內存和磁盤上開闢一塊緩存空間每每可以保證圖片的快速重複加載.java
使用內存緩存android
一塊內存緩存在耗費必定應用內存基礎上,可以讓快速加載圖片成爲可能.而LruCache正合適用來緩存圖片,對最近使用過的對象保存在
緩存LinkedHashMap中,而且將最近未使用過的對象釋放.
app爲了給LrcCache肯定一個合適的大小,有如下一些因素須要考慮:
異步1.應用中其餘組件佔用內存的狀況
ide2.有多少圖片可能會顯示在屏幕上?有多少圖片將要顯示在屏幕上?
函數3.屏幕的尺寸和屏幕密度是多少?與Nexus S這類高屏幕密度設備相比,Galaxy Nexs這類超高屏幕密度的設備,每每須要更大的緩存空間來存儲相同數量的圖片.
ui4.圖片的尺寸以及其餘的參數,還有每張圖片將會佔用多少內存.
this5.圖片被訪問的頻率有多高?是否有一些圖片的訪問頻率會比另一些更高?若是是這樣,咱們可能須要將一些圖片長存於內存中,或者使用多個LrcCache來對不一樣的Bitmap進行分組.
6.咱們還須要在圖片的數量和質量之間權衡.有些時候,在緩存中存放大量的縮略圖,而在後臺加載高清圖片會明顯提升效率.
對每一個應用來講,須要指定的緩存大小是不必定的,這取決於咱們對應用的分析並得出相應的解決方案.若是緩存空間太小,可能會形成額外的開銷,這對整個應用並沒有補益;而緩存空間過大,則可能會形成java.lang.OutOfMemory異常,而且留給其餘組件使用的內存空間也會相應減小.
如下爲初始化一個存放Bitmap的LrcCache的例子:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // 獲取最大的可用空間,若是須要的空間超出這個大小,則會拋出OutOfMemory異常 // LrcCache構造函數中的參數是以千字節爲單位的 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. // 緩存的大小會使用千字節來衡量 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中獲取對應的Bitmap public 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; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected 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; @Override protected 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後從新取出圖片幾乎沒有延時.在內存緩存中沒有的圖片極可能在磁盤緩存上會有,若是磁盤緩存中也沒有,則會正常加載須要的圖片.