1. 程式人生 > >Bitmap的高效載入和LruCache快取

Bitmap的高效載入和LruCache快取

Bitmap高效載入

Android應用程式都是有一定記憶體限制的,程式佔用了過高的記憶體就容易出現OOM(OutOfMemory)異常。

  • 檢視每個應用程式的最最高可用記憶體:

    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
    Log.d("TAG", "Max memory is " + maxMemory + "KB"); 
    

因此在展示高解析度圖片的時候,最好先將圖片進行壓縮。壓縮後的圖片大小應該和用來展示它的控制元件大小相近。

BitmapFactory

BitmapFactory類提供了四類方法: decodeFile、decodeResource、decodeStream和decodeByteArray,分別用於支援從檔案系統、資源、輸入流以及位元組陣列中載入Bitmap物件。

BitmapFactory.Options引數

為了避免OOM異常,最好在解析每張圖片的時候都先檢查一下圖片的大小,除非你非常信任圖片的來源,保證這些圖片都不會超出你程式的可用記憶體。

根據圖片大小決定把整張圖片載入到記憶體中還是載入一個壓縮版的圖片到記憶體中。以下幾個因素是我們需要考慮的:

  • 預估一下載入整張圖片所需佔用的記憶體。
  • 為了載入這一張圖片你所願意提供多少記憶體。
  • 用於展示這張圖片的控制元件的實際大小。
  • 當前裝置的螢幕尺寸和解析度。

比如,你的ImageView只有128* 96畫素的大小,只是為了顯示一張縮圖,這時候把一張1024 * 768畫素的圖片完全載入到記憶體中顯然是不值得的。

高效載入Bitmap的核心思想
  • 採用BitmapFactory.Options來載入所需尺寸的圖片。
獲取取樣率的流程

通過取樣率即可有效地載入圖片

  • 將BitmapFactory.Options的inJustDecodeBounds引數設定為true並載入圖片

  • 從BitmapFactory.Options中取出圖片的原始寬高資訊,它們對應於outWidth和outHeight引數

  • 根據取樣率的規則並結合目標View的所需大小計算出取樣率inSampleSize

  • 將BitmapFactory.Options的inJustDecodeBounds引數設定為false,然後重新載入圖片。
    當inJustDecodeBounds為true時BitmapFactory只會去解析圖片的原始寬高資訊,並不會真正地載入圖片,故這個操作是輕量級的。並且BitmapFactory獲取圖片的寬高資訊和圖片的位置以及程式執行的裝置有關。

具體程式碼
  • 獲取圖片原始大小

    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;
    
  • 計算inSampleSize

    通過設定BitmapFactory.Options中inSampleSize的值就可以實現。比如我們有一張2048*1536畫素的圖片,將inSampleSize的值設定為4,就可以把這張圖片壓縮成512*384畫素。原本載入這張圖片需要佔用13M的記憶體,壓縮後就只需要佔用0.75M了(假設圖片是ARGB_8888型別,即每個畫素點佔用4個位元組)。下面的方法可以根據傳入的寬和高,計算出合適的inSampleSize值:

    public static int calculateInSampleSize(BitmapFactory.Options options,  
            int reqWidth, int reqHeight) {  
        // 源圖片的高度和寬度  
        final int height = options.outHeight;  
        final int width = options.outWidth;  
        int inSampleSize = 1;  
        if (height > reqHeight || width > reqWidth) {  
            // 計算出實際寬高和目標寬高的比率  
            final int heightRatio = Math.round((float) height / (float) reqHeight);  
            final int widthRatio = Math.round((float) width / (float) reqWidth);  
            // 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高  
            // 一定都會大於等於目標的寬和高。  
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
        }  
        return inSampleSize;  
    }  
    
  • 根據inSampleSize壓縮圖片

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) {  
        // 第一次解析將inJustDecodeBounds設定為true,來獲取圖片大小  
        final BitmapFactory.Options options = new BitmapFactory.Options();  
        options.inJustDecodeBounds = true;  
        BitmapFactory.decodeResource(res, resId, options);  
        // 呼叫上面定義的方法計算inSampleSize值  
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
        // 使用獲取到的inSampleSize值再次解析圖片  
        options.inJustDecodeBounds = false;  
        return BitmapFactory.decodeResource(res, resId, options);  
    }  
    
  • 下面的程式碼非常簡單地將任意一張圖片壓縮成100*100的縮圖,並在ImageView上展示。

    mImageView.setImageBitmap(  
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
    

Android中的快取策略

目前比較常用的快取策略是LruCache和DiskLruCache,其中LruCache常被用作記憶體快取,DisKLruCache常被用作儲存快取。Lru是Least Recently Used的縮寫,即最近最少使用演算法,核心思想:當快取快滿時,會淘汰近期最少使用的快取目標。

LruCache是一個泛型類,它內部採用了一個LinkedHashMap以強引用的方式儲存外界的快取物件,其提供了get和put方法來提供完成快取的獲取和操作新增,當快取滿時,LruCache會移除較早使用的快取物件,然後再新增新的快取物件。

  • 強引用:直接的物件引用
  • 軟引用: 當一個物件只有軟引用存在時,系統記憶體不足時此物件會被GC回收

LruCache

  • LruCache 是執行緒安全的

    public class LruCache<K,V>{
        private final LinkedHashMap<K,V> map;
    }
    
  • LruCache典型初始過程:

    int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024); //單位kb
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize){
        protected int sizeOf(String key, Bitmap bitmap){
            return bitmap.gteRowBytes() * bitmap.getHeight() / 1024;
        }
    }
    

    在某些特殊情況下,還需要重寫LruCache的entryRemoved方法,LruCache移除舊快取時會呼叫,因此可以在entryRemoved中完成一些資源回收工作。

    private LruCache

DiskLruCache

DiskLruCache用於實現儲存裝置快取,即磁碟快取,通過將快取物件寫入檔案系統從而實現快取的效果。

使用方式:

  • DiskLruCache建立

    • DiskLruCache並不能通過構造方法來建立,它提供open方法建立自身

      public static DiskLruCache open(File directory,int appVersion, int valueCount, long maxSize)
      
      //directory表示磁碟快取在檔案系統中的儲存路徑
      //這個路徑可以選擇SD卡上的快取目錄,具體指/sdcard/Android/data/package_name/cache 當應用被解除安裝後,此目錄會一併刪除
      //建議:如果應用解除安裝後就希望刪除快取檔案,那麼就選擇卡上的快取目錄,如果希望保留快取資料那就應該選擇SD卡上的其他特定目錄
      
      //appVersion 一般設為1即可,當版本號發生改變時DiskLruCache會清空之前所有的快取檔案,而這個特性在實際開發中作用並不大
      
      //valueCount表示節點對應的資料的個數,一般設為1即可。
      
      //maxSize 表示快取的總大小,比如50MB,當快取大小超出這個設定值後,DiskLruCache會清除一些快取從而保證總大小不大於這個設定值
      
    • DiskLruCache的建立過程:

      private static final long DISK_CACHE_SIZE = 1024* 1024*50; //50MB
      File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
      if(!diskCacheDir.exists()){
          diskCacheDir.mkdirs();
      }
      mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
      
  • DiskLruCache的快取新增

    DiskLruCache的快取新增的操作是通過Editor完成的,Editor表示一個快取物件的編輯物件。這裡仍然以圖片快取舉例,首先需要獲取圖片url所對應的key,然後根據key就可以通過edit()來獲取Editor物件,如果這個快取正在被編輯,那麼返回edit()會返回null,即DiskLruCache不允許同時編輯一個快取物件。之所以要把url轉換成key,是因為圖片的url中很可能有特殊字元,這將影響url在android中直接使用,一般採用url的md5作為key。

    將圖片的url轉成key以後,就可以獲取Editor物件。對於這個key來說,如果當前不存在其他Editor物件,那麼edit()就會返回一個新的Editor物件,通過它就可以得到檔案輸出流。需要注意的是,由於前面在DiskLruCache的open方法中設定了一個節點只能有一個數據,因此下面的DISK_CACHE_INDEX常量直接設定為0即可。

    String key = hashKeyFromUrl(url);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if(editor != null){
        OutputStream out = editor.newOutputStream(DISK_CHANE_INDEX);
    }
    
    進行寫入操作後,還必須通過commit()來提交寫入操作。如果圖片下載過程發生了異常,可以通過Editor的abort()來退回整個操作。
    
  • DiskLruCache的快取查詢

    快取查詢過程也需要將url轉換為key,通過DiskLruCache的get方法得到一個Snapshot物件,接著再通過Snapshot物件即可得到快取的檔案輸入流,有了檔案輸出流,就可以得到Bitmap物件。為了避免載入圖片過程中導致的OOM問題,一般不建議直接載入原始圖片。通過BitmapFactory.Options物件來載入一張縮放後的圖片,但那種方法對FileInputStream的縮放存在問題,原因是FileInputStream是一種有序的檔案流,而兩次decodeStream 呼叫影響了檔案流的位置屬性,導致了第二次decodeStream時的得到的是null。為了解決這個問題,可以通過檔案流來得到它所對應的檔案描述符,然後再通過BitmapFactory.decodeFileDescriptor方法來載入一張縮放後的圖片

使用圖片快取技術

在很多情況下,(比如使用ListView, GridView 或者 ViewPager 這樣的元件),螢幕上顯示的圖片可以通過滑動螢幕等事件不斷地增加,最終導致OOM。

為了保證記憶體的使用始終維持在一個合理的範圍,通常會把被移除螢幕的圖片進行回收處理。此時垃圾回收器也會認為你不再持有這些圖片的引用,從而對這些圖片進行GC操作。用這種思路來解決問題是非常好的,可是為了能讓程式快速執行,在介面上迅速地載入圖片,你又必須要考慮到某些圖片被回收之後,使用者又將它重新滑入螢幕這種情況。這時重新去載入一遍剛剛載入過的圖片無疑是效能的瓶頸,你需要想辦法去避免這個情況的發生。

記憶體快取技術對那些大量佔用應用程式寶貴記憶體的圖片提供了快速訪問的方法。其中最核心的類是LruCache (此類在android-support-v4的包中提供) 。這個類非常適合用來快取圖片,它的主要演算法原理是把最近使用的物件用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的物件在快取值達到預設定值之前從記憶體中移除。

在過去,我們經常會使用一種非常流行的記憶體快取技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的物件,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的資料會儲存在本地的記憶體當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程式的記憶體溢位並崩潰。

為了能夠選擇一個合適的快取大小給LruCache, 有以下多個因素應該放入考慮範圍內,例如:

  • 你的裝置可以為每個應用程式分配多大的記憶體?
    裝置螢幕上一次最多能顯示多少張圖片?有多少圖片需要進行預載入,因為有可能很快也會顯示在螢幕上?

  • 你的裝置的螢幕大小和解析度分別是多少?一個超高解析度的裝置(例如 Galaxy Nexus) 比起一個較低解析度的裝置(例如 Nexus S),在持有相同數量圖片的時候,需要更大的快取空間。
    圖片的尺寸和大小,還有每張圖片會佔據多少記憶體空間。
    圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在記憶體當中,或者使用多個LruCache 物件來區分不同組的圖片。
    你能維持好數量和質量之間的平衡嗎?有些時候,儲存多個低畫素的圖片,而在後臺去開執行緒載入高畫素的圖片會更加的有效。

並沒有一個指定的快取大小可以滿足所有的應用程式,這是由你決定的。你應該去分析程式記憶體的使用情況,然後制定出一個合適的解決方案。一個太小的快取空間,有可能造成圖片頻繁地被釋放和重新載入,這並沒有好處。而一個太大的快取空間,則有可能還是會引起 Java.lang.OutOfMemory 的異常。

下面是一個使用 LruCache 來快取圖片的例子:

private LruCache<String, Bitmap> mMemoryCache;  

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    // 獲取到可用記憶體的最大值,使用記憶體超出這個值會引起OutOfMemory異常。  
    // LruCache通過建構函式傳入快取值,以KB為單位。  
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
    // 使用最大可用記憶體值的1/8作為快取的大小。  
    int cacheSize = maxMemory / 8;  
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
        @Override  
        protected int sizeOf(String key, Bitmap bitmap) {  
            // 重寫此方法來衡量每張圖片的大小,預設返回圖片數量。  
            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);  
}  

在這個例子當中,使用了系統分配給應用程式的八分之一記憶體來作為快取大小。在中高配置的手機當中,這大概會有4兆(32/8)的快取空間。一個全螢幕的 GridView 使用4張 800x480解析度的圖片來填充,則大概會佔用1.5兆的空間(800*480*4)。因此,這個快取大小可以儲存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) {  
        imageView.setImageBitmap(bitmap);  
    } else {  
        imageView.setImageResource(R.drawable.image_placeholder);  
        BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
        task.execute(resId);  
    }  
}  

BitmapWorkerTask 還要把新載入的圖片的鍵值對放到快取中。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {  
    // 在後臺載入圖片。  
    @Override  
    protected Bitmap doInBackground(Integer... params) {  
        final Bitmap bitmap = decodeSampledBitmapFromResource(  
                getResources(), params[0], 100, 100);  
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);  
        return bitmap;  
    }  
}