天天看點

19_有效的顯示位圖(LruCache、DiskCache)

Android基礎彙總​​​​​​​

谷歌文檔翻譯,配有Demo;

技術點:1.加載大圖檔,圖檔的壓縮,緩存;

2.軟引用

3. LruCahce

4.DiskCahe

5. 處理并發

6.ListView加載圖檔錯位,閃爍,上下滑動重複加載

7.橫屏時圖檔不清除

Demo位址:關于DiskCahe的使用代碼中要詳細些

 http://download.csdn.net/detail/baopengjian/9799317

在加載圖檔的過程中要注意記憶體的使用情況,如果不小心,會導緻應用崩潰的記憶體溢出異常:java.lang.OutofMemoryError: bitmap size exceeds VM budget.   加載圖檔有記憶體限制的原因: 1) 移動裝置的記憶體有限,有些手機對單個程式僅配置設定16M的記憶體; 2) 圖檔本身占用記憶體大 3) Android應用程式UI的經常需要幾z張位圖加載   #1 有效加載大圖檔 圖檔通常有不同的尺寸和分辨率;如相機的分辨率一般會比手機螢幕的分辨率大,一個圖像的高分辨率不提供任何可見的好處,但仍然占用寶貴的記憶,并且由于額外的動态擴充有額外的性能開銷,,為了在有限的記憶體空間限制下加載相應圖檔,可以加載對應圖檔的低分辨率版本。 1)讀取圖檔的尺寸( Dimensions )和類型( Type )   BitmapFactory 提供了根據不同資料源建立位圖對象的方法如: decodeByteArray(),  decodeFile(), decodeResource()等等;這些方法試圖配置設定記憶體的構造圖,是以可以很容易地導緻OutOfMemory異常。每種類型的解碼方法有額外的簽名,讓你通過BitmapFactory.Optionsclass指定解碼選項。将inJustDecodeBounds屬性設定為true,而解碼避免記憶體配置設定,傳回零位圖對象。這種技術允許您讀取圖像的尺寸和類型資料之前建設(和記憶體配置設定)的位圖。   BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;   除非你信任的資料源,否則可以通過上述API來檢查位圖的尺寸   2)縮小圖檔加載到記憶體中 是否加載原圖檔的因素: a. 估計加載整個圖檔需要的記憶體 b. 應用程式提供給加載圖檔的記憶體 c. 加載圖檔的UI控件尺寸 d. 目前裝置的螢幕大小和密度   例如,将1024 x768像素圖像加載到記憶體中,如果它最終将被顯示在128x96像素ImageView縮略圖上,這樣做事不值得的。這時需要通過 BitmapFactory.Options 對象的設定  inSampleSize來告訴解碼器進行縮放。那麼縮放的比例是多少?這裡有一種方法: public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; } (方法中使用2進行計算是跟因為解碼器使用的最終值有關)   3)當擷取到縮放比例後,因為前面 inJustDecodeBounds 設定為true傳回空的位圖對象,必須重新設定為false   public static Bitmap decodeSampledBitmapFromResource ( Resources res , int resId ,         int reqWidth , int reqHeight ) {       // First decode with inJustDecodeBounds=true to check dimensions     final BitmapFactory . Options options = new BitmapFactory . Options ();     options .inJustDecodeBounds = true ;     BitmapFactory .decodeResource (res , resId , options );       // Calculate inSampleSize     options .inSampleSize = calculateInSampleSize (options , reqWidth , reqHeight );       // Decode bitmap with inSampleSize set     options .inJustDecodeBounds = false ;     return BitmapFactory .decodeResource (res , resId , options ); }     4)使用:任意大大小的位圖加載到一個100 x100的ImageView中,如以下示例所示代碼: mImageView .setImageBitmap (     decodeSampledBitmapFromResource (getResources (), R .id .myimage , 100 , 100 ));       #2 異步加載圖檔 當從網絡、本地或記憶體以外的其他來源加載大圖檔時,不應放在主線程。 注意:網絡加載圖檔在縮放時,先将輸入流用位元組數組緩存起來 1)使用 AsyncTask   class BitmapWorkerTask extends AsyncTask < Integer , Void , Bitmap > {     private final WeakReference < ImageView > imageViewReference ;     private int data = 0 ;       public BitmapWorkerTask ( ImageView imageView ) {         // Use a WeakReference to ensure the ImageView can be garbage collected         imageViewReference = new WeakReference < ImageView >(imageView );     }       // Decode image in background.     @Override     protected Bitmap doInBackground ( Integer ... params ) {         data = params [ 0 ];         return decodeSampledBitmapFromResource (getResources (), data , 100 , 100 ));     }       // Once complete, see if ImageView is still around and set bitmap.     @Override     protected void onPostExecute ( Bitmap bitmap ) {         if (imageViewReference != null && bitmap != null ) {             final ImageView imageView = imageViewReference . get ();             if (imageView != null ) {                 imageView .setImageBitmap (bitmap );             }         }     } }   WeakReference的使用時確定imageView不被回收,使用:   public void loadBitmap ( int resId , ImageView imageView ) {     BitmapWorkerTask task = new BitmapWorkerTask (imageView );     task .execute (resId ); }   2) 處理并發( Concurrency Handling ) 清單和表格會複用View,如果每個View都去使用 AsyncTask加載圖檔, 不能保證異步任務的順序開始的順序完成。 a . 建立一個AsyncDrawable static class AsyncDrawable extends BitmapDrawable {     private final WeakReference < BitmapWorkerTask > bitmapWorkerTaskReference ;       public AsyncDrawable ( Resources res , Bitmap bitmap ,             BitmapWorkerTask bitmapWorkerTask ) {         super (res , bitmap );         bitmapWorkerTaskReference =             new WeakReference < BitmapWorkerTask >(bitmapWorkerTask );     }       public BitmapWorkerTask getBitmapWorkerTask () {         return bitmapWorkerTaskReference . get ();     } }   b. 在執行 BitmapWorkerTask之前,将建立的  AsyncDrawable綁定到目标ImageView上 public void loadBitmap ( int resId , ImageView imageView ) {     if (cancelPotentialWork (resId , imageView )) {         final BitmapWorkerTask task = new BitmapWorkerTask (imageView );         final AsyncDrawable asyncDrawable =                 new AsyncDrawable (getResources (), mPlaceHolderBitmap , task );         imageView .setImageDrawable (asyncDrawable );         task .execute (resId );     } }   c. cancelPotentialWork方法中引用上面的代碼示例檢查另一個運作的任務已經與ImageView相關聯.如果是這樣,它試圖取消先前的任務callingcancel()。在少數情況下,新任務資料比對現有的任務并沒有進一步需要發生。這是cancelPotentialWork的實作: public static boolean cancelPotentialWork ( int data , ImageView imageView ) {     final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask (imageView );       if (bitmapWorkerTask != null ) {         final int bitmapData = bitmapWorkerTask .data ;         if (bitmapData != data ) {             // Cancel previous task             bitmapWorkerTask .cancel ( true );         } else {             // The same work is already in progress             return false ;         }     }     // No task associated with the ImageView, or an existing task was cancelled     return true ; }   d. getBitmapWorkerTask擷取與imageView相關的BitmapWorkerTask private static BitmapWorkerTask getBitmapWorkerTask ( ImageView imageView ) {     if (imageView != null ) {         final Drawable drawable = imageView .getDrawable ();         if (drawable instanceof AsyncDrawable ) {             final AsyncDrawable asyncDrawable = ( AsyncDrawable ) drawable ;             return asyncDrawable .getBitmapWorkerTask ();         }     }     return null ; }   e. 在BitmapWorkerTask的onPostExecute方法中檢查任務是否被取消,   class BitmapWorkerTask extends AsyncTask < Integer , Void , Bitmap > {     ...       @Override     protected void onPostExecute ( Bitmap bitmap ) {         if ( isCancelled ()) {             bitmap = null ;         }           if (imageViewReference != null && bitmap != null ) {             final ImageView imageView = imageViewReference . get ();             final BitmapWorkerTask bitmapWorkerTask =                     getBitmapWorkerTask ( imageView );             if ( this == bitmapWorkerTask && imageView != null ) {                 imageView .setImageBitmap (bitmap );             }         }     } }       #3 緩存 圖檔 記憶體和磁盤緩存常常可以允許元件快速加載圖像處理,提高響應能力和流動性的UI加載多個位圖   1)記憶體緩存 通過占用一定的寶貴記憶體,記憶體緩存提供了快速通路位圖的方法。LruCache尤其适合緩存位圖,其保持最近引用的對象在一個強引用LinkedHashMap,當緩存超過其指定大小時,移除最近最少使用的對象。   注意:過去記憶體緩存通過軟引用或弱引用實作,但這種方法不推薦,因為從API9(2.3)以後,垃圾回收器開始更積極的回收軟引用或弱引用,使得這種實作變得相當無效。此外,Android 3.0(API級别11)之前,支援位圖的資料是存儲在本機記憶體中不是以一種可預見的方式釋放,可能導緻應用程式暫時超過其記憶體限制和崩潰   為了給LruCache設定一個合理的大小,應該考慮許多因素,例如: 1)除了Activity和Application之外的剩餘記憶體; 2)有多少圖檔在螢幕上呢?有多少需要螢幕可用準備? 3)裝置的螢幕大小和密度是多少?額外的高密度螢幕(xhdpi) 4)圖檔的尺寸和配置,占用多少記憶體 5)圖檔的通路頻率。一些圖檔是否比另一些圖檔更高頻率的被通路,如果是這樣,可以維護多個LruCache來緩存不同的對象。 6)能否有效平衡品質和數量。有時存儲大量低品質的位圖,而在背景去加載另一版本的高品質位圖是非常有用的。   沒有特定的大小或配方,适合所有應用程式,由你來分析您的使用和想出一個合适的解決方案。緩存太小會導緻額外的開銷而且達不到想要的效果,緩存太大可以再次引起OutOfMemory異常,讓其他應用程式記憶體變小。   LruCache設定的Demo:   (1)設定LruCache 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); }   注意: 在這個例子中,八分之一的緩存的應用程式記憶體配置設定。正常/ hdpi裝置這是一個最低約為4 mb(32/8)。全屏顯示資料表格填滿800 x480分辨率的圖像裝置上使用大約1.5 mb(800 * 480 * 4個位元組),是以這将緩存至少約2.5頁的圖檔在記憶體中   (2)從LruCache擷取圖檔 當ImageView加載圖檔時,先從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 );     } }   (3)請求成功之後,将圖檔緩存進LruCache中去 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 ;     }     ... }   2) 磁盤緩存 記憶體緩存雖然有用,但不能完全依賴,當顯示資料表格或更大的結果集的時候,很容易就填滿它。你的應用程式如果被切到背景,有可能緩存被回收,當重新傳回是,你必須的處理每個圖像。 但圖檔不在可用的記憶體緩存中,磁盤緩存可以解決這種情況。當然,從磁盤擷取比從記憶體加載速度慢,時間不确定,需要使用異步線程。 注意:如果圖檔被頻繁的通路,可以使用内容提供者來存儲緩存,例如一個圖檔庫應用程式。   DiskLruCache: android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java ( http://download.csdn.net/detail/sinyu890807/7709759 ) 下載下傳好了源碼之後,隻需要在項目中建立一個libcore.io包,然後将DiskLruCache.java檔案複制到這個包中即可。 部落格: http://www.tuicool.com/articles/JB7RNj   Demo:DiskLruCache添加了一個磁盤緩存 具體見003. DiskLruCache(demo中有代碼)     注意:緩存的初始化要在異步線程中,這意味着,有可能緩存尚未建立就被通路,為了避免這樣的問題,需要加線程鎖確定在緩存在建立之後才被通路。 在UI線程檢查記憶體緩存,在背景線程檢查磁盤緩存。磁盤操作不應該發生在UI線程上。當完成圖像處理,最後添加位圖記憶體和磁盤緩存以備将來使用   3) 處理配置更改 運作時配置更改,比如一個螢幕方向改變,導緻安卓銷毀并重新啟動運作活動的新配置(關于這種行為的更多資訊,請參閱處理運作時更改)。你想避免再次處理所有你的照片是以使用者平穩和快速體驗當配置發生變化。 但是你有一個通過記憶體緩存建構的緩存容器。通過Fragment調用setRetainInstance(true)) 儲存緩存進而在新的Activity執行個體使用。活動被重新建立後,擷取被儲存的緩存對象,快速擷取圖像到ImageView對象。】   Demo:Fragment通過配置來儲存緩存 private LruCache<String, Bitmap> mMemoryCache;   @Override protected void onCreate(Bundle savedInstanceState) {     ...     RetainFragment retainFragment =             RetainFragment.findOrCreateRetainFragment(getFragmentManager());     mMemoryCache = retainFragment.mRetainedCache;     if (mMemoryCache == null) {         mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {             ... // Initialize cache here as usual         }         retainFragment.mRetainedCache = mMemoryCache;     }     ... }   class RetainFragment extends Fragment {     private static final String TAG = "RetainFragment";     public LruCache<String, Bitmap> mRetainedCache;       public RetainFragment() {}       public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {         RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);         if (fragment == null) {             fragment = new RetainFragment();             fm.beginTransaction().add(fragment, TAG).commit();         }         return fragment;     }       @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setRetainInstance(true);     } } 試着旋轉的裝置(設定或不設定保留片段)來測試這一點。設定儲存記憶體緩存的圖檔會被立即加載出來,沒有儲存的則像往常一樣處理。

繼續閱讀