加載一張圖片到UI相對比較簡單,若是一次要加載一組圖片,就會變得麻煩不少。像ListView,GridView,ViewPager等控件,須要顯示的圖片和將要顯示的圖片數量可能會很大。html
爲了減小內存使用,這類控件都重複利用移出屏幕的子視圖,若是你沒有持用引用,垃圾回收器也會回收你加載過的圖片。這種作法很好,可是若是想要圖片加載快速流暢且不想當控件拖回來時從新運算獲取加載過的圖片,一般會使用內存和磁盤緩存。這節主要介紹當加載多張圖片時利用內存緩存和磁盤緩存使加載圖片時更快。java
1、使用內存緩存
內存緩存以犧牲有限的應用內存爲代價提供快速訪問緩存的圖片方法。LruCache類(有兼容包能夠支持到API Level 4)很適合緩存圖片的功能,它在LinkedHashMap中保存着最近使用圖片對象的引用,而且在內容超過它指定的容量前刪除近期最少使用的對象的引用。android
注意:這前,很流行的圖片緩存的方法是使用SoftReference和WeakReference,可是這種方法不提倡。由於從Android2.3(Level 9)開始,內存回收器會對軟引用和弱引用進行回收。另外,在Android3.0(Levle 11)以前,圖片的數據是存儲在系統native內存中的,它的內存釋放不可預料,這也是形成程序內存溢出的一個潛在緣由。web
爲了給LruCache選擇一個合適的大小,一些因素須要考慮:緩存
》應用其它的模塊對內存大小的要求app
》有多少張圖片會同時在屏幕上顯示,有多少張圖片須要提早加載ide
》屏幕的大小和密度oop
》圖片的尺寸和設置ui
》圖片被訪問的頻度this
》平衡數量和質量,有時候存儲大量的低質量的圖片會比少許的高質量圖片要有用
對於緩存,沒有大小或者規則適用於全部應用,它依賴於你分析本身應用的內存使用肯定本身的方案。緩存過小可能只會增長額外的內存使用,緩存太大可能會致使內存溢出或者應用其它模塊可以使用內存過小。
下面是爲圖片緩存設置LruCache的一個例子:
- private LruCache mMemoryCache;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- ...
-
-
- final int memClass = ((ActivityManager) context.getSystemService(
- Context.ACTIVITY_SERVICE)).getMemoryClass();
-
-
- final int cacheSize = 1024 * 1024 * memClass / 8;
-
- mMemoryCache = new LruCache(cacheSize) {
- @Override
- protected int sizeOf(String key, Bitmap bitmap) {
-
- return bitmap.getByteCount();
- }
- };
- ...
- }
-
- 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用來作緩存。在普通hdpi設備上這個值一般爲4M(32/8)。一個全屏的GridView,尺寸爲800x480大小一般爲1.5M左右(800*480*4sbytes),因此在內存中能夠緩存2.5張圖片。
當一個ImageView加載圖片時,先檢查LruCache。若是緩存中存在,會用它立刻更新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加載圖片後,也要把圖片緩存到內存中:
- class BitmapWorkerTask extends AsyncTask {
- ...
-
- @Override
- protected Bitmap doInBackground(Integer... params) {
- final Bitmap bitmap = decodeSampledBitmapFromResource(
- getResources(), params[0], 100, 100));
- addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
- return bitmap;
- }
- ...
- }
2、使用磁盤緩存
內存緩存最加載最近訪問過的圖片有很大幫助,但你並不能依賴於圖片會存在於緩存中。像GridView這樣的控件若是數據稍微多一點,就能夠輕易的把內存緩存用完。你的應用也有可能被其餘任務打斷,如電話呼入,應用在後臺有可能會被結束,這樣緩存的數據也會丟失。當用戶回到應用時,全部的圖片還須要從新獲取一遍。
磁盤緩存可應用到這種場景中,它能夠減小你獲取圖片的次數,固然,從磁盤獲取圖片比從內存中獲取要慢的多,因此它須要在非UI線程中完成。示例代碼中是磁盤緩存的一個實現,在Android4.0源碼中(
libcore/luni/src/main/java/libcore/io/DiskLruCache.java),有更增強大和推薦的一個實現,它的向後兼容使在已發佈過的庫中很方便使用它。下面是它的例子:
- private DiskLruCache mDiskCache;
- private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
- private static final String DISK_CACHE_SUBDIR = "thumbnails";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- ...
-
- ...
- File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
- mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
- ...
- }
-
- class BitmapWorkerTask extends AsyncTask {
- ...
-
- @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(String.valueOf(imageKey, bitmap);
-
- return bitmap;
- }
- ...
- }
-
- public void addBitmapToCache(String key, Bitmap bitmap) {
-
- if (getBitmapFromMemCache(key) == null) {
- mMemoryCache.put(key, bitmap);
- }
-
-
- if (!mDiskCache.containsKey(key)) {
- mDiskCache.put(key, bitmap);
- }
- }
-
- public Bitmap getBitmapFromDiskCache(String key) {
- return mDiskCache.get(key);
- }
-
- public static File getCacheDir(Context context, String uniqueName) {
-
-
- final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
- || !Environment.isExternalStorageRemovable() ?
- context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
-
- return new File(cachePath + File.separator + uniqueName);
- }
在UI線程中檢查內存緩存,而在後臺線程中檢查磁盤緩存。磁盤操做最好永遠不要在UI線程中進行。當圖片獲取完成,把它同時緩存到內存中和磁盤中。
3、處理配置改變
運行時的配置改變,例如屏幕橫豎屏切換,會致使系統結束當前正在運行的Activity並用新配置從新啓動(可參考:http://developer.android.com/guide/topics/resources/runtime-changes.html)。爲了有好的用戶體驗,你可能不想在這種狀況下,從新獲取一遍圖片。幸虧你可使用上面講的內存緩存。緩存能夠經過使用一個Fragment(調用setRetainInstance(true)被傳到新的Activity,當新的Activity被建立後,只須要從新附加Fragment,你就能夠獲得這個Fragment並訪問到存在的緩存,把裏面的圖片快速的顯示出來。下面是一個示例:
- private LruCache mMemoryCache;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- ...
- RetainFragment mRetainFragment =
- RetainFragment.findOrCreateRetainFragment(getFragmentManager());
- mMemoryCache = RetainFragment.mRetainedCache;
- if (mMemoryCache == null) {
- mMemoryCache = new LruCache(cacheSize) {
- ...
- }
- mRetainFragment.mRetainedCache = mMemoryCache;
- }
- ...
- }
-
- class RetainFragment extends Fragment {
- private static final String TAG = "RetainFragment";
- public LruCache mRetainedCache;
-
- public RetainFragment() {}
-
- public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
- RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
- if (fragment == null) {
- fragment = new RetainFragment();
- }
- return fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- }
- }