要點提煉|開發藝術之Bitmap&Cache

釐米姑娘發表於2018-01-02

本篇將總結有關圖片載入、快取策略以及優化列表卡頓的知識點:

  • Bitmap的高效載入
  • 快取策略
    • LruCache(記憶體快取)
    • DiskLruCache(磁碟快取)
  • ImageLoader

1.Bitmap的高效載入

a.Bitmap(點陣圖):指一張圖片,常見格式:.png.jpg

b.必要性:直接載入大容量的高清Bitmap很容易出現顯示不完整、記憶體溢位OOM的問題(如報錯:

java.lang.OutofMemoryError:bitmap size exceeds VM budget
複製程式碼

c.核心思想:按一定的取樣率將圖片縮小後再載入進來。

d.工具類:

  • BitmapFactory類提供的四種載入圖片的方法:
    • decodeFile():從檔案系統載入出一個Bitmap物件
    • decodeResource():從資原始檔載入出一個Bitmap物件
    • decodeStream():從輸入流載入出一個Bitmap物件
    • decodeByteArray():從位元組陣列載入出一個Bitmap物件

  • 對應著BitmapFactory類的幾個native方法;
  • decodeFile()decodeResource()又間接呼叫decodeStream()
  • BitmapFactory.Options的引數
    • inSampleSize引數:即取樣率,同時作用於寬/高
      • 取值規定:
        • 應為2的指數,如1、2、4...
        • 否則系統會向下取整並選擇一個最接近2的指數來替代,如3被2替代。
      • 變化規則:
        • 當inSampleSize=1,取樣後大小不變。
        • 當inSampleSize=k>1,取樣後圖片會縮小。具體規則:寬高變為原圖的1/k, 畫素變為原圖的1/k^2, 佔用記憶體大小變為原圖的1/k^2。
      • 注意:根據圖片寬高的 實際大小&需要大小,而計算出的縮放比儘可能取最小,避免由於縮小的過多,導致在控制元件中不能鋪滿而被拉伸至模糊。
    • inJustDecodeBounds引數:
      • 值為true:BitmapFactory只載入圖片的原始寬高資訊,而不真正載入圖片到記憶體;
      • 值為false:BitmapFactory真正載入圖片到記憶體。

注意:BitmapFactory獲取的圖片寬高資訊和圖片的位置以及程式執行的裝置有關,會導致獲取到不同的結果。

e.載入流程

  • BitmapFactory.Options.inJustDecodeBounds引數設為true並載入圖片。
  • BitmapFactory.Options中取出圖片的原始寬高資訊,對應outWidth和outHeight引數。
  • 根據取樣率的規則並結合目標View的所需大小計算出取樣率inSampleSize。
  • BitmapFactory.Options.inJustDecodeBounds引數設為false,然後重新載入圖片。

常用的獲取取樣率的程式碼片段:

  /**
     * 對一個Resources的資原始檔進行指定長寬來載入進記憶體, 並把這個bitmap物件返回
     *
     * @param res   資原始檔物件
     * @param resId 要操作的圖片id
     * @param reqWidth 最終想要得到bitmap的寬度
     * @param reqHeight 最終想要得到bitmap的高度
     * @return 返回取樣之後的bitmap物件
     */
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //1.設定inJustDecodeBounds=true獲取圖片尺寸
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);
        //3.計算縮放比
        options.inSampleSize = calculateInSampleSize(options,reqHeight,reqWidth);
        //4.再設為false,重新從資原始檔中載入圖片
        options.inJustDecodeBounds =false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

   /**
     *  一個計算工具類的方法, 傳入圖片的屬性物件和想要實現的目標寬高. 通過計算得到取樣值
     * @param options 要操作的原始圖片屬性
     * @param reqWidth 最終想要得到bitmap的寬度
     * @param reqHeight 最終想要得到bitmap的高度
     * @return 返回取樣率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) {
        //2.height、width為圖片的原始寬高
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if(height>reqHeight||width>reqWidth){
            int halfHeight = height/2;
            int halfWidth = width/2;
            //計算縮放比,是2的指數
            while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
                inSampleSize*=2;
            }
        }    
        return inSampleSize;
    }
複製程式碼

現在假設ImageView期望圖片大小是為100*100畫素:

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(),R.mipmap.ic_launcher,100,100);
複製程式碼

推薦閱讀Android開發之高效載入Bitmap


2.快取策略

為減少流量消耗,可採用快取策略。常用的快取演算法是LRU(Least Recently Used):

  • 核心思想:當快取滿時, 會優先淘汰那些近期最少使用的快取物件。
  • 兩種方式:LruCache(記憶體快取)、DiskLruCache(磁碟快取)。

a.LruCache(記憶體快取)

  • LruCache類是一個執行緒安全的泛型類:內部採用一個LinkedHashMap強引用的方式儲存外界的快取物件,並提供getput方法來完成快取的獲取和新增操作,當快取滿時會移除較早使用的快取物件,再新增新的快取物件。
public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;
...
複製程式碼

:幾種引用的含義

  • 強引用:直接的物件引用,不會被gc回收;
  • 軟引用:當系統記憶體不足時,物件會被gc回收;
  • 弱引用:隨時會被gc回收。
  • 實現原理:LinkedHashMap利用一個雙重連結連結串列來維護所有條目item。
    • 常用屬性accessOrder:決定LinkedHashMap的連結串列順序。
      • 值為true:以訪問順序維護連結串列。
      • 值為false:以插入的順序維護連結串列。

而LruCache利用是accessOrder=true時的LinkedHashMap實現LRU演算法,使得最近訪問的資料會在連結串列尾部,在容量溢位時,將連結串列頭部的資料移除。

  • 使用方法:
    • 計算當前可用的記憶體大小;
    • 分配LruCache快取容量;
    • 建立LruCache物件並傳入最大快取大小的引數、重寫sizeOf()用於計算每個快取物件的大小;
    • 通過put()、get()和remove()實現資料的新增、獲取和刪除。

例項:

  //初始化LruCache物件
public void initLruCache()
{
    //1.獲取當前程式的可用記憶體,轉換成KB單位
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    //2.分配快取的大小
    int maxSize = maxMemory / 8;
    //3.建立LruCache物件並重寫sizeOf方法
    lruCache = new LruCache<String, Bitmap>(maxSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                return value.getWidth() * value.getHeight() / 1024;
            }
        };
}
//4.LruCache對資料的操作
public void fun()
{
    //新增資料
    lruCache.put("lizhuo", bm1);
    lruCache.put("sushe", bm2);
    lruCache.put("jiqian", bm3);
    //獲取資料
    Bitmap b1 = (lruCache.get("lizhuo"));
    Bitmap b2 = (lruCache.get("sushe"));
    Bitmap b3 = (lruCache.get("jiqian"));
    //刪除資料
    lruCache.remove("sushe");
}
複製程式碼

推薦閱讀詳細解讀LruCache類LruCache 原始碼解析


b.DiskLruCache(磁碟快取)

  • 通過將快取物件寫入檔案系統從而實現快取效果,即磁碟快取。

與LruCache區別:DiskLruCache非泛型類,不能新增型別,而是採用檔案儲存,儲存和讀取通過I/O流處理。

  • 使用方法:
    • 計算分配DiskLruCache的容量;
    • 設定快取目錄;
    • 建立DiskLruCache物件,注意不能通過構造方法來建立, 而是提供open()方法;
    • 利用Editor、Snapshot和remove()實現資料的新增、獲取和刪除。
    • 呼叫flush()將資料寫入磁碟。

(1)先來介紹DiskLruCache的建立:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
複製程式碼

其中,引數含義:

directory:磁碟快取的儲存路徑。有兩種目錄:

  • SD 上的快取目錄:/sdcard/Android/data/package_name/cache 目錄,當應用被解除安裝後會被刪除。
  • 其他目錄:應用解除安裝後快取資料還在。

appVersion:當前應用的版本號,一般設為1。

valueCount:單個節點所對應的資料的個數,一般設為1。

maxSize:快取的總大小,超出這個設定值後DiskLruCache會清除一些快取

例如,典型的建立過程:

DiskLruCache mDiskLruCache = null;  
try {  
    File cacheDir = getDiskCacheDir(context, "bitmap");  
    if (!cacheDir.exists()) {  
    //若快取地址的路徑不存在就建立一個
        cacheDir.mkdirs();  
    }  
    mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);  
} catch (IOException e) {  
    e.printStackTrace();  
}  
//用於獲取到快取地址的路徑
public File getDiskCacheDir(Context context, String uniqueName) {  
    String cachePath;  
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())|| !Environment.isExternalStorageRemovable()) {  
    //當SD卡存在或者SD卡不可被移除,獲取路徑 /sdcard/Android/data/<application package>/cache
        cachePath = context.getExternalCacheDir().getPath();  
    } else { 
    //反之,獲取路徑/data/data/<application package>/cache 
        cachePath = context.getCacheDir().getPath();  
    }  
    return new File(cachePath + File.separator + uniqueName);  
}  
//用於獲取到當前應用程式的版本號
public int getAppVersion(Context context) {  
    try {  
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);  
        return info.versionCode;  
    } catch (NameNotFoundException e) {  
        e.printStackTrace();  
    }  
    return 1;  
}  
複製程式碼

(2)新增快取操作:通過Editor完成

  • 獲取資源的key值,採用url的md5值作為key;
  • 通過DiskLruCache.edit() 獲取對應key的Editor;
  • 通過Editor.newOutputStream(0)得到一個輸出流;
  • 通過OutputStream寫入資料;
  • Editor.commit()提交寫操作,若發生異常,則呼叫Editor.abort()進行回退。

核心程式碼:

//1.返回url的MD5演算法結果
String key = hashKeyFormUrl(url);
//2.獲取Editor物件
Editor editor = mDiskLruCache.edit(key);
//3.建立輸出流,其中常量DISK_CACHE_INDEX = 0
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
//4.寫入資料
outputStream.wirte(data);
//5.提交寫操作
editor.commit();
複製程式碼

(3)查詢快取操作:和快取新增的過程類似

  • 獲取資源的key值,採用url的md5值作為key;
  • 通過DiskLruCache.get()獲取對應key的Snapshot物件;
  • 通過Snapshot.getInputStream(0)得到一個輸入流(可向下轉型為FileInputStream);
  • 通過InputStream讀取資料。

核心程式碼:

//1.返回url的MD5演算法結果
String key = hashKeyFormUrl(url);
//2.獲取Snapshot物件
Snapshot snapshot = mDiskLruCache.get(key);
//3.建立輸入流,其中常量DISK_CACHE_INDEX = 0
InputStream inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
//4.讀出資料
int data = inputStream.read();
複製程式碼
  • 問題:FileInputStream是一種有序的檔案流,呼叫兩次 BitmapFactory.decodeStream()會影響檔案流的位置屬性,導致第二次解析結果為空。
  • 解決辦法:通過檔案流得到其對應的檔案描述符,再呼叫 BitmapFactory.decodeFileDescriptor()來載入一張縮放後的圖片。

推薦閱讀Android DiskLruCache完全解析原始碼解析


3.ImageLoader 的使用

a.ImageLoader內部封裝了Bitmap的高效載入、LruCache和DiskLruCache。

b.應具備功能:

  • 同步載入
  • 非同步載入
  • 圖片壓縮
  • 記憶體快取
  • 磁碟快取
  • 網路拉取

更多瞭解Android 開源框架Universal-Image-Loader完全解析開源框架ImageLoader的完美例子

c.使用場景:

  • 實現照片牆效果 ,此處例項
  • 優化 ListView/GridView卡頓現象,幾點辦法:
    • 不要在Adapter的getView()中執行耗時操作,比如直接載入圖片。
    • 控制非同步任務的執行頻率,在列表滑動時停止載入圖片,而列表停下時再載入圖片,此處例項
    • 開啟硬體加速,給Activity新增配置android:hardwareAccelerated="true"更多辦法

希望這篇文章對你有幫助~

相關文章