Android讀書筆記(12)—— Bitmap的加載和Cache

一、Bitmap的高效加載

1、加載圖片

BitmapFactory類提供了四種方法:

  • decodeFile() 從文件系統加載出一個Bitmap對象,間接調用了decodeStream()方法
  • decodeResource() 從資源文件加載,間接調用了decodeStream()方法
  • decodeStream() 從輸入流加載
  • decodeByteArray() 從字節數組加載

2、高效加載的Bitmap

核心思想:采用BitmapFactory.Options來加載所需尺寸的圖片。

2.1、Options.inSampleSize(采樣率)

如果是inSampleSize=1那么和原圖大小一樣
如果是inSampleSize=2那么寬高都為原圖1/2, 而像素為原圖的1/4, 占用的內存大小也為原圖的1/4。

若取值小于1,則無效。建議取2的n次方。

現在有一張圖片像素為:1024*1024, 如果采用ARGB8888(四個顏色通道每個占有一個字節,相當于1點像素占用4個字節的空間)的格式來存儲,那么圖片的占有大小就是1024*1024*4那現在這張圖片在內存中占用4MB。
如果針對剛才的圖片進行inSampleSize=2, 那么最后占用內存大小為512*512*4, 也就是1MB

public class MyBitmapLoadUtil {
    /**
     * 對一個Resources的資源文件進行指定長寬來加載進內存, 并把這個bitmap對象返回
     *
     * @param res   資源文件對象
     * @param resId 要操作的圖片id
     * @param reqWidth 最終想要得到bitmap的寬度
     * @param reqHeight 最終想要得到bitmap的高度
     * @return 返回采樣之后的bitmap對象
     */
    public static Bitmap decodeFixedSizeForResource(Resources res, int resId, int reqWidth, int reqHeight){
        // 首先先指定加載的模式 為只是獲取資源文件的大小
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        //Calculate Size  計算要設置的采樣率 并把值設置到option上
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 關閉只加載屬性模式, 并重新加載的時候傳入自定義的options對象
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
    /**
     *  一個計算工具類的方法, 傳入圖片的屬性對象和 想要實現的目標大小. 通過計算得到采樣值
     */
    private 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 (reqWidth < height || reqHeight < width){
            int halfWidth = width/2;
            int halfHeight = height/2;
            // 判斷原始長寬的一半是否比目標大小小, 如果小那么增大采樣率2倍, 直到出現修改后原始值會比目標值大的時候
            while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

二、Android中的緩存策略

當程序第一次從網絡上加載圖片后,將其緩存在存儲設備中,下次使用這張圖片的時候就不用再從網絡從獲取了。很多時候為了提高應用的用戶體驗,往往還會把圖片在內存中再緩存一份,因為從內存中加載圖片比存儲設備中快。一般情況會把圖片存一份到內存中,一份到存儲設備中,如果內存中沒找到就去存儲設備中找,還沒有找到就從網絡上下載。

緩存策略包含緩存的添加、獲取和刪除操作。不管是內存還是存儲設備,緩存大小都是有限制的。如何刪除舊的緩存并添加新的緩存,就對應緩存算法。

目前常用的一種緩存算法是LRU(Least Recently Used),最近最少使用算法。核心思想: 當緩存存滿時, 會優先淘汰那些近期最少使用的緩存對象。采用LRU算法的緩存有兩種: LruCache和DiskLruCache。LruCache用于實現內存緩存, DiskLruCache則充當了存儲設備緩存

1、LruCache

LruCache是一個泛型類, 它內部采用了一個LinkedHashMap以強引用的方式存儲外界的緩存對象, 其提供了get和put方法來完成緩存的獲取和添加的操作。當緩存滿了時,LruCache會移除較早使用的緩存對象, 然后在添加新的緩存對象。LruCache是線程安全的。

強引用: 直接的對象引用
軟引用: 當一個對象只有軟引用存在時, 系統內存不足時此對象會被gc回收
弱引用: 當一個對象只有弱引用存在時, 對象會隨下一次gc時被回收

LruCache 典型初始化過程:

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }
};

只需要提供緩存的總容量大小(一般為進程可用內存的1/8)并重寫 sizeOf 方法即可。sizeOf方法作用是計算緩存對象的大小。這里大小的單位需要和總容量的單位(這里是kb)一致,因此除以1024。一些特殊情況下,需要重寫LruCache的entryRemoved方法,LruCache移除舊緩存時會調用entryRemoved方法,因此可以在entryRemoved中完成一些資源回收工作(如果需要的話)。

還有獲取和添加方法,都比較簡單:

  • get(K key) V
  • remove(K key) V

從Android 3.1開始,LruCache成為Android源碼的一部分。

2、DiskLruCache

DiskLruCache用于實現磁盤緩存,DiskLruCache得到了Android官方文檔推薦,但它不屬于Android SDK的一部分。

2.1、DiskLruCache的創建

DiskLruCache并不能通過構造方法來創建, 他提供了open()方法用于創建自身, 如下所示:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

  • File directory:表示磁盤緩存在文件系統中的存儲路徑。可以選擇SD卡上的緩存目錄, 具體是指/sdcard/Andriod/data/package_name/cache目錄,也可以選擇data目錄下. 或者其他地方。 這里給出的建議:如果應用卸載后就希望刪除緩存文件的話,那么就選擇SD卡上的緩存目錄, 如果希望保留緩存數據那就應該選擇SD卡上的其他目錄。
  • appVersion: 表示應用的版本號,一般設為1即可。當版本號發生改變的時候DiskLruCache會清空之前所有的緩存文件, 在實際開發中這個實用性不大。
  • valueCount: 一般設為1。
  • maxSize: 表示緩存的總大小。當緩存大小超出這個設定值后,會清除一些緩存而保證總大小不大于這個設定值。
    //初始化DiskLruCache,包括一些參數的設置
    public void initDiskLruCache() {
        //配置固定參數
        // 緩存空間大小
        private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
        //下載圖片時的緩存大小
        private static final long IO_BUFFER_SIZE = 1024 * 8;
        // 緩存空間索引,用于Editor和Snapshot,設置成0表示Entry下面的第一個文件
        private static final int DISK_CACHE_INDEX = 0;

        //設置緩存目錄
        File diskLruCache = getDiskCacheDir(mContext, "bitmap");
        if (!diskLruCache.exists())
            diskLruCache.mkdirs();
        //創建DiskLruCache對象,當然是在空間足夠的情況下
        if (getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskLruCache,
                        getAppVersion(mContext), 1, DISK_CACHE_SIZE);
                mIsDiskLruCache = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //上面的初始化過程總共用了3個方法
    //設置緩存目錄
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment
                .getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    // 獲取可用的存儲大小
    @TargetApi(VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD)
            return path.getUsableSpace();
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    //獲取應用版本號,注意不同的版本號會清空緩存
    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.2、DiskLruCache的緩存添加

DiskLruCache的緩存添加的操作是通過Editor完成的, Editor表示一個緩存對象的編輯對象.

如果還是緩存圖片為例子, 每一張圖片都通過圖片的url為key, 這里由于url可能會有特殊字符所以采用url的md5值作為key. 根據這個key就可以通過edit()來獲取Editor對象, 如果這個緩存對象正在被編輯, 那么edit()就會返回null. 即DiskLruCache不允許同時編輯一個緩存對象.

當用.edit(key)獲得了Editor對象之后. 通過editor.newOutputStream(0)就可以得到一個文件輸出流. 由于之前open()方法設置了一個節點只能有一個數據. 所以在獲得輸出流的時候傳入常量0即可.

有了文件輸出流, 可以當網絡下載圖片時, 圖片就可以通過這個文件輸出流寫入到文件系統上.最后,要通過Editor中commit()來提交寫操作, 如果下載中發生異常, 那么使用Editor中abort()來回退整個操作.

2.3、DiskLruCache的緩存查找

和緩存的添加過程類似, 緩存查找過程也需要將url轉換成key, 然后通過DiskLruCache#get()方法可以得到一個Snapshot對象, 接著在通過Snapshot對象即可得到緩存的文件輸入流, 有了文件輸入流, 自然就可以得到Bitmap對象. 為了避免加載圖片出現OOM所以采用壓縮的方式. 在前面對BitmapFactory.Options的使用說明了. 但是這中方法對FileInputStream的縮放存在問題. 原因是FileInputStream是一種有序的文件流, 而兩次decodeStream調用會影響文件的位置屬性, 這樣在第二次decodeStream的時候得到的會是null. 針對這一個問題, 可以通過文件流來得到它所對應的文件描述符, 然后通過BitmapFactory.decodeFileDescription()來加載一張縮放后的圖片.

/**
     * 磁盤緩存的讀取
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
 */
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
    if(Looper.myLooper() == Looper.getMainLooper())
        Log.w(TAG, "it's not recommented load bitmap from UI Thread");
    if(mDiskLruCache == null)
        return null;

    Bitmap bitmap = null;
    String key = hashKeyForDisk(url);
    Snapshot snapshot = mDiskLruCache.get(key);
    if(snapshot != null)
    {
        FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
        FileDescriptor fd = fileInputStream.getFD();
        bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);

        if(bitmap != null)
            addBitmapToMemoryCache(key, bitmap);

    }
    return bitmap;      
}

3、ImageLoader的實現

一個好的ImageLoader應該具備以下幾點:

  • 圖片的壓縮
  • 網絡拉取
  • 內存緩存
  • 磁盤緩存
  • 圖片的同步加載
  • 圖片的異步加載

異步加載過程:

  • bindBitmap先嘗試從內存緩存讀取圖片,如果沒有會在線程池中調用loadBitmap方法。獲取成功將圖片封裝為LoadResult對象通過mMainHandler向UI線程發送消息。選擇線程池和Handler來提供并發能力和異步能力。
  • 為了解決View復用導致的列表錯位問題,在給ImageView設置圖片之前都會檢查它的url有沒有發生改變,如果改變就不再給它設置圖片。

三、ImageLoader的使用

1、照片墻效果

實現照片墻效果,如果圖片都需要是正方形;這樣做很快,自定義一個ImageView,重寫onMeasure方法。

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
    super.onMeasure(widthMeasureSpec,widthMeasureSpec);//將原來的參數heightMeasureSpec換成widthMeasureSpec
}

2、優化列表的卡頓現象

  • 不要在getView中執行耗時操作,不要在getView中直接加載圖片。
  • 控制異步任務的執行頻率:如果用戶刻意頻繁上下滑動,getView方法會不停調用,從而產生大量的異步任務。可以考慮在列表滑動停止加載圖片;給ListView或者GridView設置 setOnScrollListener 并在 OnScrollListener 的 onScrollStateChanged 方法中判斷列表是否處于滑動狀態,如果是的話就停止加載圖片。
  • 大部分情況下,可以使用硬件加速解決莫名卡頓問題,通過設置 android:hardwareAccelerated=”true” 即可為Activity開啟硬件加速。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容