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

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

  • 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"更多辦法

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容