第12章 Bitmap的加載和Cache(筆記)


title: 第12章 Bitmap的加載和Cache
tags: []
notebook: Android開發(fā)藝術(shù)探索


第12章 Bitmap的加載和Cache

[TOC]

本章主要介紹了三個方面的知識:

  1. 圖片加載:如何有效的加載一個Bitmap
  2. 緩存策略:LruCache和DiskLruCache
  3. 列表的滑動流暢性:如何優(yōu)化列表的卡頓現(xiàn)象

12.1 Bitmap的高效加載

首先有4種Bitmap的加載方法,都是有BitmapFactory提供的

加載方法 加載來源
decodeFile 文件
decodeResource 資源
decodeStream 輸入流
decodeByteArray 字節(jié)數(shù)組

*其中decodeFile和decodeResource是間接調(diào)用了decodeStream方法

1.核心思想

加載所需尺寸的圖片。即原本圖片很大岸啡,但是實(shí)際上需要的尺寸很小淤翔,比如頭像只需要縮略圖线得,此時可以計(jì)算出采樣率爆价,然后根據(jù)采樣率來加載圖片

2.壓縮方法

通過設(shè)置BitmapFactory.Options對象的inSampleSize來進(jìn)行圖片的采樣計(jì)算和縮放奥喻,這里需要了解的是整個圖片縮放比例是1/(inSampleSize的2次方)光绕,而寬高為原來的1/inSampleSize铭段,即inSampleSize為1時不縮放,為2時縮放為原來的1/4(寬高都為原來的1/2)隅很,為4時縮放為原來的1/16(寬高都為原來的1/4)

采樣原則:縮放比例一定要大于等于需求寬高。比如ImageView的大小為100*100像素率碾,原始圖片為200*300叔营,那么inSampleSize應(yīng)該為2,此時壓縮后的圖片為100*150 >= 100*100

3.采樣縮放流程

總體流程應(yīng)該分為:獲取圖片的寬高->獲取所需的寬高->計(jì)算采樣率->壓縮圖片
具體流程如下:

  1. 將BitmapFactory.Options的inJustDecodeBounds參數(shù)設(shè)為true所宰,并通過BitmapFactory和Options加載圖片(此時加載的只是原始圖片的寬高)
  2. 從BitmapFactory.Options中獲取到原始圖片的寬高绒尊,對應(yīng)outWidth和outHeight
  3. 通過實(shí)際所需的寬高reqWidth和reqHeight以及outWidth和outHeight計(jì)算出采樣率,并設(shè)置到Options的inSampleSize中
  4. 將BitmapFactory.Options的inJustDecodeBounds參數(shù)設(shè)為false仔粥,然后重新通過BitmapFactory和Options加載圖片婴谱,此時圖片就是根據(jù)實(shí)際需要壓縮過的圖片

*其中inJustDecodeBounds參數(shù)的作用是使BitmapFactory只加載圖片的寬高,因?yàn)椴蓸勇实挠?jì)算并不需要加載整個圖片躯泰,提高了效率
上面流程的代碼實(shí)現(xiàn)如下

public class ImageResizer {
    public ImageResizer(){}

    /**
     * 從資源文件中獲取相應(yīng)的圖片
     * @param res
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
    {
        Options options = new Options();
        //設(shè)置只加載寬高標(biāo)志位
        options.inJustDecodeBounds = true;
        //加載原始圖片寬高到Options中
        BitmapFactory.decodeResource(res, resId, options);
        //計(jì)算采樣率谭羔,通過所需寬高和原始圖片寬高
        options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
        //還原并再次加載圖片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
    /** 
     * 從文件描述符中獲取相應(yīng)的圖片
     * @param reqWidth
     * @param reqHeight
     * @param options
     * @return
     */
    public Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight)
    {
        
        Options options = new Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
        options.inJustDecodeBounds = false;
        return  BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

    //計(jì)算采樣率
    public static int calculateSampleSize(int reqWidth, int reqHeight, Options options)
    {
        //如果傳入0參數(shù),則將采樣率設(shè)成1斟冕,即不壓縮
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        int inSampleSize = 1;
        int width = options.outWidth;
        int height = options.outHeight;
        
        //當(dāng)所需寬高比實(shí)際寬高小時才進(jìn)行壓縮
        if(reqWidth < width && reqHeight < height)
        {
            int halfWidth = width >>= 1;
            int halfHeight = height >>= 1;
            //保證壓縮后的寬高不能小于所需寬高
            while(reqWidth <= halfWidth && reqHeight <= halfHeight)
            {
                inSampleSize <<= 1;
                halfWidth /= inSampleSize;
                halfHeight /= inSampleSize;
            }
        }
        return inSampleSize;
    }
}

實(shí)際使用的時候根據(jù)需要壓縮圖片口糕,比如ImageView所期望的圖片大小為100*100,則可以這樣實(shí)現(xiàn)

iv.setImageBitmap(mImageResizer.decodeSampleBitmapFromResource(getResources(), R.drawable.lizhuo, 100, 100));

12.2 Android中的緩存策略

Android中三級緩存策略:內(nèi)存-磁盤-網(wǎng)絡(luò)磕蛇。即在獲取資源時比如圖片景描,先從內(nèi)存緩存中讀取,如果沒有則從磁盤緩存中讀取秀撇,最后還沒有再從網(wǎng)絡(luò)中拉取圖片超棺。
Android中通過LruCache實(shí)現(xiàn)內(nèi)存緩存,通過DiskLruCache實(shí)現(xiàn)磁盤緩存呵燕,它們采用的都是LRU(Least Recently Used)最近最少使用算法來移除緩存

12.2.1 LruCache

使用LruCache類時建議使用v4包中的以兼容Android2.2版本棠绘,它是在Android3.1開始默認(rèn)所提供的一個緩存類

1.LruCache實(shí)現(xiàn)原理

LruCache底層是使用LinkedHashMap來實(shí)現(xiàn)的,所以LruCache也是一個泛型類,利用LinkedHashMap的accessOrder屬性可以實(shí)現(xiàn)LRU算法氧苍。
因?yàn)長inkedHashMap利用一個雙重鏈接鏈表來維護(hù)所有條目夜矗,accessOrder屬性決定了LinkedHashMap的鏈表順序
* 為true則以訪問順序維護(hù)鏈表,即被訪問過的元素會安排到鏈表的尾部让虐;
* 為false則以插入的順序維護(hù)鏈表紊撕。
而LruCache利用的正是accessOrder為true的LinkedHashMap來實(shí)現(xiàn)LRU算法的。

  1. put:通過LinkedHashMap的put來實(shí)現(xiàn)元素的插入赡突,在插入后調(diào)用trimToSize來調(diào)整緩存的大小对扶,如果大于設(shè)定的最大緩存大小,則將LinkedHashMap頭部的節(jié)點(diǎn)刪除惭缰,直到size小于maxSize浪南。注:插入的過程還是要先尋找有沒有相同的key的數(shù)據(jù),如果有則替換掉舊值漱受,并且將該節(jié)點(diǎn)移到鏈表的尾部
  2. get:通過LinkedHashMap的get來實(shí)現(xiàn)络凿,由于accessOrder為true,因此被訪問到的元素會被調(diào)整到鏈表的尾部拜效,因此不常被訪問的元素就會留到鏈表的頭部喷众,當(dāng)觸發(fā)清理緩存時不常被訪問的元素就會被刪除,這里是實(shí)現(xiàn)LRU最關(guān)鍵的地方
  3. remove:通過LinkedHashMap的remove來實(shí)現(xiàn)
  4. size:LruCache中很重要的兩個成員size和maxSize紧憾,因?yàn)榍謇砭彺娴氖窃趕ize>maxSize時觸發(fā)的到千,因此在初始化的時候要傳入maxSize定義緩存的大小,然后重寫sizeOf方法赴穗,因?yàn)長ruCache是通過sizeOf方法來計(jì)算每次添加一個元素或者刪除一個元素而改變的size的大小

引用的分類

  • 強(qiáng)引用:直接的對象引用憔四,不會被gc回收
  • 軟引用:系統(tǒng)內(nèi)存不足時,對象會被gc回收
  • 弱引用:對象隨時被gc回收
    LruCache里的LinkedHashMap以強(qiáng)引用的方式存儲外界的緩存對象

2.LruCache的使用

  1. 設(shè)計(jì)LruCache的最大緩存大邪忝肌:一般是通過計(jì)算當(dāng)前可用的內(nèi)存大小繼而來獲取到應(yīng)該設(shè)置的緩存大小
  2. 創(chuàng)建LruCache對象:傳入最大緩存大小的參數(shù)了赵,同時重寫sizeOf方法來設(shè)置存在LruCache里的每個對象的大小
  3. 通過put、get和remove方法來實(shí)現(xiàn)數(shù)據(jù)的添加甸赃、獲取和刪除
//初始化LruCache對象
public void initLruCache()
{
    //獲取當(dāng)前進(jìn)程的可用內(nèi)存柿汛,轉(zhuǎn)換成KB單位
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    //分配緩存的大小
    int maxSize = maxMemory / 8;
    //創(chuàng)建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;
            }
        };
}
//LruCache對數(shù)據(jù)的操作
public void fun()
{
    //添加數(shù)據(jù)
    lruCache.put("lizhuo", bm1);
    lruCache.put("sushe", bm2);
    lruCache.put("jiqian", bm3);
    //獲取數(shù)據(jù)
    Bitmap b1 = (lruCache.get("lizhuo"));
    Bitmap b2 = (lruCache.get("sushe"));
    Bitmap b3 = (lruCache.get("jiqian"));
    //刪除數(shù)據(jù)
    lruCache.remove("sushe");
}

一般會對將數(shù)據(jù)添加進(jìn)內(nèi)存和獲取數(shù)據(jù)做一個封裝

/**
 * 將圖片存入緩存
 * @param key 圖片的url轉(zhuǎn)化成的key
 * @param bitmap
 */
private void addBitmapToMemoryCache(String key, Bitmap bitmap)
{
    if(getBitmapFromMemoryCache(key) == null)
    {
        mLruCache.put(key, bitmap);
    }
}

private Bitmap getBitmapFromMemoryCache(String key)
{
    return mLruCache.get(key);
}

/**
 * 因?yàn)橥饨缫话惬@取到的是url而不是key,因此再做一層封裝
 * @param url http url
 * @return bitmap
 */
private Bitmap loadBitmapFromMemoryCache(String url)
{
    final String key = hashKeyFromUrl(url);
    return getBitmapFromMemoryCache(key);
}

12.2.2 DiskLruCache

DiskLruCache用于實(shí)現(xiàn)磁盤緩存埠对,通過將緩存對象寫入文件系統(tǒng)從而實(shí)現(xiàn)緩存的效果络断。DiskLruCache并不屬于Android SDK的一部分,需要另行添加
注:由于DiskLruCache是采用文件存儲的项玛,存儲和讀取都是通過IOStream來處理的貌笨,所以并不存在什么類型,所以DiskLruCache并不是泛型類襟沮,不能像LurCache一樣添加泛型

1.DiskLruCache實(shí)現(xiàn)原理

DiskLruCache是通過文件的形式將數(shù)據(jù)存在磁盤上的锥惋,和LruCache一樣昌腰,是通過LinkedHashMap來實(shí)現(xiàn)LRU算法的,不同的是膀跌,LruCache是直接將數(shù)據(jù)存在LinkedHashMap當(dāng)中遭商,即key對應(yīng)的value;而DiskLruCache在LinkedHashMap中存儲的值是Entry對象捅伤,可以認(rèn)為它指向磁盤中的文件株婴,因此DiskLruCache對文件數(shù)據(jù)的操作(寫入、讀仁钊稀)是通過Entry對象間接進(jìn)行的。以下是各數(shù)據(jù)操作的原理

  1. 添加數(shù)據(jù):Editor->OutputStream->Entry->File
    先是獲取到對應(yīng)Key的Editor大审,然后Editor里有LinkekHashMap中對應(yīng)的Entry蘸际,Entry對應(yīng)的就是緩存文件,因此通過Editor的OutputStream可以將數(shù)據(jù)直接寫到對應(yīng)的緩存文件當(dāng)中徒扶。當(dāng)然中間的過程還是要先查找粮彤,如果查找到了直接換舊值,并且把節(jié)點(diǎn)移到鏈表的尾部姜骡,這跟LruCache一樣导坟,只不過是通過Editor的OutputStream來添加數(shù)據(jù)
    其實(shí)仔細(xì)想想也不難理解啊,因?yàn)榫彺娴氖俏募Τ海募膶懭肟隙ㄊ荗utputStream來達(dá)到的惫周,而Edittor就是將OutputStream和Entry連接起來的橋梁,這么想就理解了為什么DiskLruCache的添加數(shù)據(jù)比LruCache麻煩
  2. 獲取數(shù)據(jù):Snapshot->InputStream->Entry->File
    上面添加數(shù)據(jù)過程理解之后獲取數(shù)據(jù)就簡單多了康栈,同樣要讀取文件是需要InputStream的递递,而Snapshot就是將LinkedHashMap中對應(yīng)key的Entry和InputStream連接的對象,然后從Snapshot對象中可以獲取到文件的輸入流從而達(dá)到讀取緩存文件的效果
  3. 刪除數(shù)據(jù):通過LinkedHashMap直接刪除對應(yīng)key的Entry
  4. 日志文件:DiskLruCache中的特別的地方還有一個日志文件journal
    它用于記錄DiskLruCache進(jìn)行過的數(shù)據(jù)操作啥么,DiskLruCache能夠正常工作的前提就是依賴journal文件中的內(nèi)容登舞,它一共有DIRTY,CLEAN,REMOVE和READ四種前綴,分別記錄了正在修改的數(shù)據(jù)悬荣,干凈的菠秒,被移除的和被讀取的數(shù)據(jù)。

這里強(qiáng)調(diào)一下Editor和Snapshot的使用氯迂,以免對它們的使用不夠清楚了解践叠。它們可以理解成在DiskLruCache中,每個key對應(yīng)一個Editor和一個Snapshot囚戚,每個Editor和Snapshot又對應(yīng)著一個Entry繼而對應(yīng)著一個文件FIle酵熙,因此在獲取Editor或者Snapshot對象時需要相應(yīng)的key,如下:

Editor editor = mDiskLruCache.edit(key);
OutputStream = editor.newOutputStream(0);

Snapshot snapshot = mDiskLruCache.get(key);
FileInputStream fileInputStream = snapshot.getInputStream(0);

2.DiskLruCache的使用

使用過程和LruCache大同小異驰坊,還是分配空間匾二,創(chuàng)建對象和數(shù)據(jù)操作,由于DiskLruCache是將數(shù)據(jù)存在磁盤上的,因此比LruCache多了一步設(shè)置緩存目錄

  1. 計(jì)算分配DiskLruCache分配空間大胁烀辍:設(shè)成50MB的寫法為1024*1024*50
  2. 設(shè)置緩存目錄:通常都會存放在 /sdcard/Android/data/ "application package"/cache 這個路徑下面皮璧,但同時我們又需要考慮如果這個手機(jī)沒有SD卡,或者SD正好被移除了的情況分飞,因此比較優(yōu)秀的程序都會專門寫一個方法來獲取緩存地址(后面說明)
  3. 創(chuàng)建DiskLruCache對象:通過靜態(tài)方法open(File directory, int appVersion, int valueCount, long maxSize) 來創(chuàng)建對象悴务,因?yàn)閯?chuàng)建對象時還要對journal文件進(jìn)行操作,因此不能直接通過構(gòu)造方法來創(chuàng)建譬猫。其中第一個參數(shù)指定的是數(shù)據(jù)的緩存地址讯檐,第二個參數(shù)指定當(dāng)前應(yīng)用程序的版本號,第三個參數(shù)指定同一個key可以對應(yīng)多少個緩存文件染服,基本都是傳1别洪,這里的1導(dǎo)致在Editor獲取OutputStream和Snapshot獲取InputStream時需要傳入的參數(shù)為0,表示數(shù)組的第一個柳刮,第四個參數(shù)指定最多可以緩存多少字節(jié)的數(shù)據(jù)挖垛。
  4. 數(shù)據(jù)操作:通過Editor進(jìn)行數(shù)據(jù)的添加,通過Snapshot進(jìn)行數(shù)據(jù)的讀取秉颗,通過remove進(jìn)行數(shù)據(jù)的刪除痢毒,注意在添加數(shù)據(jù)的時候最后要調(diào)用editor.commit()保證被其它的Reader看到,并且commit方法可以保證緩存大小不超過閾值
  5. flush():最后記得調(diào)用flush將數(shù)據(jù)寫入磁盤

注:由于磁盤緩存和內(nèi)存緩存一般是同時工作的蚕甥,因此這里在使用DiskLruCache的時候會順便使用LruCache以更好的觀察它們之間如何相互作用哪替。主要的一個關(guān)系就是,在從磁盤讀出數(shù)據(jù)的同時梢灭,通常會將該數(shù)據(jù)加載到內(nèi)存當(dāng)中

DiskLruCache的初始化

以下是DiskLruCache的初始化夷家,包括了分配參數(shù),設(shè)置目錄和對象的創(chuàng)建

//初始化DiskLruCache敏释,包括一些參數(shù)的設(shè)置
public void initDiskLruCache
{
    //配置固定參數(shù)
    // 緩存空間大小
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    //下載圖片時的緩存大小
    private static final long IO_BUFFER_SIZE = 1024 * 8;
    // 緩存空間索引库快,用于Editor和Snapshot,設(shè)置成0表示Entry下面的第一個文件
    private static final int DISK_CACHE_INDEX = 0;

    //設(shè)置緩存目錄
    File diskLruCache = getDiskCacheDir(mContext, "bitmap");
    if(!diskLruCache.exists())
        diskLruCache.mkdirs();
    //創(chuàng)建DiskLruCache對象钥顽,當(dāng)然是在空間足夠的情況下
    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個方法
//設(shè)置緩存目錄
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();
}

//獲取應(yīng)用版本號义屏,注意不同的版本號會清空緩存
public int getAppVersion(Context context) {
    try {
        PackageInfo info = context.getPackageManager().getPackageInfo(
                context.getPackageName(), 0);
        return info.versionCode;
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }
    return 1;
}


DiskLruCache寫入數(shù)據(jù)

下面是DiskLruCache的寫入數(shù)據(jù)的方法,即從網(wǎng)上拉取圖片到磁盤中蜂大,考慮到實(shí)用性闽铐,這里直接給出從網(wǎng)絡(luò)獲取圖片的方法,里面其實(shí)包含了幾個過程:從網(wǎng)上獲取圖片的輸入流InputStream奶浦,通過InputStream和OutputStream將該資源寫入到磁盤緩存中兄墅,最后從磁盤中獲取所需大小的圖片。
其中第二個過程正是DiskLruCache的寫入方法澳叉,配合其他過程能夠更好的理解DiskLruCache的工作

/**
 * 從網(wǎng)絡(luò)中下載圖片到磁盤中并獲取到按需壓縮后的圖片
 * 1.由于涉及到網(wǎng)絡(luò)通信隙咸,因此該方法應(yīng)該運(yùn)行在子線程當(dāng)中
 * 2.圖片是完整下載下來的沐悦,reqWidth和reqHeight只是在從磁盤讀取的時候進(jìn)行
 *      壓縮用的
 * 3.在存到磁盤的時候url是轉(zhuǎn)換過的編碼的
 *  4.要通過editor的commit保證被其它的Reader看到,而且在commit中會保證緩存大小不超過閾值
 * 5.最后記得調(diào)用flush()將數(shù)據(jù)確實(shí)寫入文件系統(tǒng)
 * 
 * @param url 圖片網(wǎng)址
 * @param reqWidth 所需寬度
 * @param reqHeight 所需高度
 * @return 按需壓縮后的圖片
 * @throws IOException
 */
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException
{
    if(Looper.myLooper() == Looper.getMainLooper())
        throw new RuntimeException("can not visit network from UI Thread.");
    if(mDiskLruCache == null)
        return null;
    
    String key = hashKeyForDisk(url);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if(editor != null)
    {
        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
        //寫入完成后絕不能忘了commit和flush
        if(downloadUrlToStream(url, outputStream))
        {
            editor.commit();
        }else
        {
            editor.abort();
        }
        //flush確保數(shù)據(jù)寫入文件系統(tǒng)
        mDiskLruCache.flush();
    }
    
    return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}   

上面是這個流程五督,其中使用了3個方法

  1. hashKeyForDisk是將url編碼成key方便存儲和查找
  2. downloadUrlToStream是將資源寫到outputStream中也就是寫到文件系統(tǒng)中
  3. loadBitmapFromDiskCache(url, reqWidth, reqHeight)是從磁盤中讀取剛剛寫入的資源
    注意這里傳入的參數(shù)是url藏否,不傳key是因?yàn)樵谧x取這個方法中還會將url過同樣的處理變成key.
    至于為什么不拿key做參數(shù),那是因?yàn)樵谄渌胤綇拇疟P中獲取資源的時候只需要傳入url就行了充包,不用自行轉(zhuǎn)化成key

下面是其中兩個方法副签,最后一個讀取磁盤另外講

/**
 * 考慮到直接使用URL作為DiskLruCache中LinkedHashMap的Key不太適合,
 * 因?yàn)閳D片URL中可能包含一些特殊字符基矮,這些字符有可能在命名文件時是不合法的淆储。
 * 其實(shí)最簡單的做法就是將圖片的URL進(jìn)行MD5編碼,編碼后的字符串肯定是唯一的家浇,
 * 并且只會包含0-F這樣的字符遏考,完全符合文件的命名規(guī)則。
 *
 * 該方法在寫入磁盤和從磁盤讀取時都需要用到蓝谨,因?yàn)樗怯?jì)算出索引的方法
 * 
 * @param key 圖片的url
 * @return MD5編碼之后的key
 */
public String hashKeyForDisk(String key) {  
    String cacheKey;  
    try {  
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");  
        mDigest.update(key.getBytes());  
        cacheKey = bytesToHexString(mDigest.digest());  
    } catch (NoSuchAlgorithmException e) {  
        cacheKey = String.valueOf(key.hashCode());  
    }  
    return cacheKey;  
}  
  
private String bytesToHexString(byte[] bytes) {  
    StringBuilder sb = new StringBuilder();  
    for (int i = 0; i < bytes.length; i**) {  
        String hex = Integer.toHexString(0xFF & bytes[i]);  
        if (hex.length() == 1) {  
            sb.append('0');  
        }  
        sb.append(hex);  
    }  
    return sb.toString();  
} 

/**
 * 將圖片資源寫到文件系統(tǒng)上
 * @param urlString 資源
 * @param outputStream Editor的對應(yīng)Entry下的文件的OutputStream
 * @return
 */
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
    HttpURLConnection urlConnection = null;  
    BufferedOutputStream out = null;  
    BufferedInputStream in = null;  
    try {  
        final URL url = new URL(urlString);  
        urlConnection = (HttpURLConnection) url.openConnection();  
        in = new BufferedInputStream(urlConnection.getInputStream(),   
                IO_BUFFER_SIZE);  
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);  
        int b;  
        while ((b = in.read()) != -1) {  
            out.write(b);  
        }  
        return true;  
    } catch (final IOException e) {  
        e.printStackTrace();  
    } finally {  
        if (urlConnection != null) {  
            urlConnection.disconnect();  
        }  
        try {  
            if (out != null) {  
                out.close();  
            }  
            if (in != null) {  
                in.close();  
            }  
        } catch (final IOException e) {  
            e.printStackTrace();  
        }  
    }  
    return false;  
} 
    

以上就是DiskLruCache的寫入數(shù)據(jù)的過程,其實(shí)核心只是3步青团,即
1.獲取mDiskLruCache中對應(yīng)key的Editor
2.獲取Editor的OutputStream
3.通過OutputStream寫入數(shù)據(jù)
而上面寫那么多是為了能夠更好地理解DiskLruCache的工作流程和如何使用才是最好的譬巫,比如講url轉(zhuǎn)換成key這個方法可以我們意識到這樣才能使DiskLruCache工作得更好

Editor editor = mDiskLruCache.edit(key);
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);//DISK_CACHE_INDEX == 0
outputStream.wirte(data);
DiskLruCache讀取數(shù)據(jù)

下面介紹的是根據(jù)需要的寬高加載磁盤緩存中的圖片,先是計(jì)算url對應(yīng)的key督笆,然后從mDiskLruCache中獲取到相應(yīng)的數(shù)據(jù)芦昔,最后在返回之前加載到LruCache當(dāng)中

/**
     * 磁盤緩存的讀取
     * @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;      
}

跟Editor寫入數(shù)據(jù)只需要3步一樣,讀取數(shù)據(jù)的核心也只需要3步
1.根據(jù)key獲取mDiskLruCache中對應(yīng)的Snapshot
2.通過Snapshot獲取到文件數(shù)據(jù)的InputStream(可以向下轉(zhuǎn)型為FileInputStream)
3.通過InputStream讀取數(shù)據(jù)

Snapshot snapshot = mDiskLruCache.get(key);
InputStream inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);//DISK_CACHE_INDEX == 0
int data = inputStream.read();

12.2.3 ListView的列表錯位問題

1.問題

在Adapter中娃肿,通常會復(fù)用View來優(yōu)化ListView或者GridView的加載咕缎,而復(fù)用View帶來的問題就是,圖片在ListView列表中顯示的位置錯亂料扰,即本應(yīng)該顯示B圖片的位置顯示了A圖片

2.原因

假設(shè)在ListView中凭豪,每個位置對應(yīng)著一個ImageView控件用于顯示一張圖片,而圖片是從網(wǎng)絡(luò)中拉壬硅尽(沒有緩存的情況下)嫂伞,對應(yīng)如下表所示。

列表位置 對應(yīng)控件 對應(yīng)圖片
itemA ImageView 圖片A
itemX ImageView 圖片X
itemY ImageView 圖片Y
itemZ ImageView 圖片Z
itemB ImageView 圖片B

問題產(chǎn)生的原因在于:

  1. ListView在Adapter中復(fù)用每個item的布局包括里面的控件
    即在屏幕中顯示的每個ListView的item對應(yīng)的布局只有在第一次的時候被加載拯钻,然后緩存在convertView里面帖努,之后滑動改變ListView時調(diào)用的getView就會復(fù)用緩存在converView中的布局和控件,所以可以使得ListView變得流暢(因?yàn)椴挥弥貜?fù)加載布局)
  2. 異步加載圖片需要時間
    假設(shè)每個ListView中的item都有一個ImageView顯示圖片粪般,比如itemA需要ImageView顯示圖片A拼余,itemB需要顯示圖片B。而圖片都是需要異步從網(wǎng)絡(luò)加載亩歹,所以需要時間
  3. 加載過程中ListView發(fā)生滑動
    itemA在使用ImageView加載圖片A時匙监,ListView發(fā)生了滑動凡橱,導(dǎo)致itemB滑動到了itemA的位置
  4. itemB復(fù)用itemA的布局和控件
    由于Adapter復(fù)用convertView的原因,itemB的布局直接復(fù)用原來該位置的ImageView舅柜,并同時利用該ImageView加載圖片B
  5. itemA異步加載圖片A的方法還持有被復(fù)用的ImgaView
    由于是異步加載圖片梭纹,所以itemA在加載圖片的過程中會持有剛才的ImageView,導(dǎo)致圖片A在下載完成之后會被加載到該ImageView控件當(dāng)中致份,因此造成了itemB對應(yīng)顯示的是圖片A变抽,這就是列表錯亂問題

3.解決方法

從上面可以看出,問題的根源在于圖片A在被加載ImageView之前氮块,ListView發(fā)生滑動導(dǎo)致ImageView被itemB復(fù)用绍载,此時該ImageView就不能顯示圖片A了。
那么就從根源入手滔蝉,在圖片A被加載到ImageView之前做一個判斷击儡,判斷該ImageView是否還是對應(yīng)的是itemA,如果是則將圖片加載到ImageView當(dāng)中蝠引,如果不是則放棄加載(因?yàn)閕temB已經(jīng)啟動了圖片B的加載阳谍,所以不用擔(dān)心控件出現(xiàn)空白的情況)
所以問題就變成了,如何判斷ImageView對應(yīng)的item已經(jīng)改變了

方法:

  1. 在每次getView的復(fù)用布局控件時螃概,對會被復(fù)用的控件設(shè)置一個標(biāo)簽(在這里就是對ImageView設(shè)置標(biāo)簽)矫夯,這里使用圖片的url作為標(biāo)簽內(nèi)容,然后再異步加載圖片
  2. 在圖片下載完成后要加載到ImageView之前做判斷吊洼,判斷該ImageView的標(biāo)簽內(nèi)容是否和圖片的url一樣
    如果一樣說明ImageView沒有被復(fù)用训貌,可以將圖片加載到ImageView當(dāng)中;
    如果不一樣冒窍,說明ListView發(fā)生了滑動递沪,導(dǎo)致其他item調(diào)用了getView從而將該ImageView的標(biāo)簽改變,此時放棄圖片的加載(盡管圖片已經(jīng)被下載成功了)
    注:如果是直接從內(nèi)存里讀取的數(shù)據(jù)综液,則不需要對比tag款慨,因?yàn)檫@幾乎不需要時間

4.具體實(shí)現(xiàn)

1)在getView中給ImageView設(shè)置標(biāo)簽內(nèi)容

imageView.setTag(uri);//對應(yīng)imageView.getTag()
//or
imageView.setTag(TAG_KEY_URI, uri);//對應(yīng)imageView.getTag(TAG_KEY_URI) TAG_KEY_URL是常量      

2)在給ImageView設(shè)置圖片前判斷圖片的uri是否和ImageView的標(biāo)簽內(nèi)容

if(uri.equals(imageView.getTag(TAG_KEY_URI)))
{
    //如果相等才設(shè)置圖片
    imageView.setImageBitmap(bitmap);
}else
{
    Log.w(TAG,"set image bitmap, but url has changed, ignored!");
}      

12.2.4 ImageLoader的實(shí)現(xiàn)

ImageLoader封裝了Bitmap的高效加載、LruCache和DiskLruCache谬莹,學(xué)習(xí)ImageLoader的實(shí)現(xiàn)有利于對Android的Bitmap的加載和緩存機(jī)制有更深刻的理解
ImageLoader應(yīng)該具備以下功能

  • 圖片的同步加載
  • 圖片的異步加載
  • 圖片壓縮
  • 內(nèi)存緩存
  • 磁盤緩存
  • 網(wǎng)絡(luò)拉取

圖片的同步加載

同步:先復(fù)習(xí)一下同步的意思樱调,就是整個流程會按順序執(zhí)行,如果有阻塞那就一直等待阻塞返回届良。

ImageLoader采取的圖片的同步加載的方法是loadBitmap笆凌,其中的流程是
1.loadBitmapFromMemoryCache:從內(nèi)存緩存提取圖片
2.loadBitmapFromDisk:從磁盤緩存提取圖片
3.loadBitmapFromHttp:從網(wǎng)絡(luò)拉取圖片(要經(jīng)過磁盤的存儲)
*4.downloadBitmapFromUrl:直接從網(wǎng)絡(luò)提取圖片(不經(jīng)過磁盤存儲)

注:
* 只有前一步提取不成功時才會執(zhí)行下一步;
* 第4步是為了防止因?yàn)榇疟P不夠而第3步執(zhí)行不成功的情況士葫,本質(zhì)都是通過網(wǎng)絡(luò)獲取圖片

從ImageLoader這個名稱上來看就知道圖片加載時ImageLoader的最主要的工作乞而,因此要深刻理解ImageLoader中圖片加載的步驟。ImageLoader封裝了整個加載流程慢显,只向外提供了統(tǒng)一的接口loadBitmap爪模,這方便了調(diào)用者不用再去關(guān)心加載的流程欠啤,以下是loadBitmap的程序:

/**
 * load bitmap from memory cache or disk or network
 * NOTE that should run in a new Thread
 * @param url http url
 * @param reqWidth  the width that imageView desire
 * @param reqHeight the height that imageView desire
 * @return bitmap maybe null
 */
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight)
{
    Bitmap bitmap = loadBitmapFromMemoryCache(url);
    
    if(bitmap != null)
    {
        Log.d(TAG, "loadBitmapFromMemoryCache, url:"+url);
        return bitmap;
    }
    
    try
    {
        bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
        if(bitmap != null)
        {
            Log.d(TAG, "loadBitmapFromDiskCache, url:"+url);
            return bitmap;
        }
        bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);
        Log.d(TAG, "loadBitmapFromHttp, url:"+url);
        
    }catch(Exception e)
    {
        e.printStackTrace();
    }
    
    if(bitmap == null && !mIsDiskLruCacheCreated)
    {
        Log.w(TAG, "encounter error, DiskLruCache is not created.");
        bitmap = downloadBitmapFromUrl(url);
    }
    
    return bitmap;
}
  1. loadBitmapFromMemoryCache:從內(nèi)存緩存提取圖片

private Bitmap loadBitmapFromMemoryCache(String url)
{
    final String key = hashKeyForDisk(url);
    return getBitmapFromMemoryCache(key);
}

private Bitmap getBitmapFromMemoryCache(String key)
{
    return mLruCache.get(key);
}

private void addBitmapToMemoryCache(String key, Bitmap bitmap)
{
    if(getBitmapFromMemoryCache(key) == null)
    {
        mLruCache.put(key, bitmap);
    }
}

2.loadBitmapFromDisk:從磁盤緩存提取圖片


private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)
{
    if(Looper.myLooper() == Looper.getMainLooper())
    {
        Log.w(TAG, "load bitmap from UI Thread is not recomment.");
    }
    if(mDiskLruCache == null)
    {
        return null;
    }
    
    Bitmap bitmap = null;
    final String key = hashKeyForDisk(url);
    Snapshot snapshot = mDiskLruCache.get(key);
    if(snapshot == null)
    {
        return 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.loadBitmapFromHttp:從網(wǎng)絡(luò)拉取圖片(要經(jīng)過磁盤的存儲) ,其實(shí)是先存到磁盤到從磁盤中讀取


private Bitmap loadBitmapFromHttp (String url, int reqWidth, int reqHeight)
    throws IOException
{
    if(Looper.myLooper() == Looper.getMainLooper())
    {
        throw new RuntimeException("can not visit network in from UI Thread");
    }
    if(mDiskLruCache == null)
    {
        return null;
    }
    
    final String key = hashKeyForDisk(url);
    Editor editor = mDiskLruCache.edit(key);
    if(editor != null)
    {
        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
        if(downloadUrlToStream(url, outputStream))
        {
            editor.commit();
        }else
        {
            editor.abort();
        }
    }
    mDiskLruCache.flush();
    
    return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}

/**
 * 將圖片資源寫到文件系統(tǒng)上
 * @param urlString 資源
 * @param outputStream Editor的對應(yīng)Entry下的文件的OutputStream
 * @return
 */
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
    HttpURLConnection urlConnection = null;  
    BufferedOutputStream out = null;  
    BufferedInputStream in = null;  
    try {  
        final URL url = new URL(urlString);  
        urlConnection = (HttpURLConnection) url.openConnection();  
        in = new BufferedInputStream(urlConnection.getInputStream(), 
        IO_BUFFER_SIZE);  
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);  
        int b;  
        while ((b = in.read()) != -1) {  
            out.write(b);  
        }  
        return true;  
    } catch (final IOException e) {  
        e.printStackTrace();  
    } finally {  
        if (urlConnection != null) {  
            urlConnection.disconnect();  
        }  
        try {  
            if (out != null) {  
                out.close();  
            }  
            if (in != null) {  
                in.close();  
            }  
        } catch (final IOException e) {  
            e.printStackTrace();  
        }  
    }  
    return false;  
} 

*4.downloadBitmapFromUrl:直接從網(wǎng)絡(luò)提取圖片(不經(jīng)過磁盤存儲)


private Bitmap downloadBitmapFromUrl(String urlString)
{
    Bitmap bitmap = null;
    HttpURLConnection connection = null;
    BufferedInputStream bis = null;
    
    try
    {
        URL url = new URL(urlString);
        connection = (HttpURLConnection) url.openConnection();
        bis = new BufferedInputStream(connection.getInputStream(), 
        IO_BUFFER_SIZE);
        bitmap = BitmapFactory.decodeStream(bis);
    }catch(IOException e)
    {
        Log.e(TAG,"Error in downloadBitmap: " + e);
    }finally
    {
        if(connection != null)
        {
            connection.disconnect();
        }
        if(bis != null)
        {
            try {
                bis.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    return bitmap;
}

圖片的異步加載

異步:指在執(zhí)行整個流程的時候屋灌,如果發(fā)生阻塞洁段,那么此時會去做其他跟阻塞返回?zé)o關(guān)事情,如果阻塞返回了再去執(zhí)行跟阻塞相關(guān)的事情共郭。
簡單來說異步就是在主線程里開另一個線程里做阻塞的事情祠丝,而主線程完成其他的工作,然后在子線程阻塞返回后通過回調(diào)(在Android中一般是通過Handler)將結(jié)果返回給主線程除嘹,此時主線程會按照預(yù)定好的規(guī)則(handleMessage)處理相關(guān)的結(jié)果

在ImageLoader當(dāng)中需要圖片的異步加載的原因:因?yàn)閳D片的下載是需要時間的写半,也就是需要在子線程中下載,而圖片下載完成之后需要加載到外界向ImageLoader提供的ImageView當(dāng)中尉咕,也就是需要在主線程中加載圖片叠蝇,還需要線程間通信。
所以ImageLoader不僅要同步加載圖片年缎,還應(yīng)該向外提供異步加載的接口悔捶,使得外界提供url和ImageView之后就不用再管圖片在子線程中的下載和在主線程中的加載了

實(shí)現(xiàn)異步加載的條件
1.Excutor:線程池,用于開啟新線程下載圖片
2.Handler:主線程的Handler单芜,用于將下載完成后的圖片在主線程中加載到ImageView當(dāng)中
3.bindBitmap:異步加載圖片炎功,包括了在子線程中同步加載圖片和在主線程中設(shè)置圖片
*4.列表錯亂預(yù)防:利用ImageView的標(biāo)簽防止異步加載圖片時發(fā)生列表錯亂
注:第4步是跟ListView或者GridView列表錯亂有關(guān),實(shí)際意義上的實(shí)現(xiàn)異步只需要前面三個條件

1.線程池的創(chuàng)建
這里使用線程池而不使用AsyncTask的原因缓溅,AsyncTask在3.0之后無法實(shí)現(xiàn)并發(fā)的效果(雖然AsyncTask里也使用了線程池,但是自帶的SerialExecutor導(dǎo)致任務(wù)只能一個個被執(zhí)行)赁温。而ImageLoader需要并發(fā)的效果坛怪,所以使用了線程池

//線程池的配置
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    
    private final AtomicInteger mCount = new AtomicInteger(1);
    
    @Override
    public Thread newThread(Runnable r) {
        // TODO Auto-generated method stub
        return new Thread(r,"ImageLoader#" + mCount.getAndIncrement());
    }
};
//作為靜態(tài)成員的線程池
//線程池的創(chuàng)建
public static final Executor THREAD_POOL_EXEUTOR = 
        new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
                KEEP_ALIVE, TimeUnit.SECONDS, 
                new LinkedBlockingQueue<Runnable>(), 
                sThreadFactory);


2.Handler的創(chuàng)建
由于該Handler是向主線程發(fā)送消息的,因此Handler需要在主線程創(chuàng)建股囊。由于ImageLoader很可能在子線程中創(chuàng)建而導(dǎo)致成員都在子線程中被創(chuàng)建袜匿,因此該Handler的創(chuàng)建需要借助主線程的Looper對象,即Looper.getMainLooper()
其中在給ImageView設(shè)置圖片前先判斷列表項(xiàng)是否已經(jīng)改變

//主線程Handler創(chuàng)建的方法
private Handler mMainHandler = new Handler(Looper.getMainLooper())
{
    @Override
    public void handleMessage(android.os.Message msg) {
        
        LoaderResult result = (LoaderResult) msg.obj;
        ImageView imageView = result.imageView;
        String uri = result.uri;
        if(uri.equals(imageView.getTag(TAG_KEY_URI)))
        {
            imageView.setImageBitmap(result.bitmap);
        }else
        {
            Log.w(TAG, "set image bitmap, but url has changed, ignored!");
        }
        
    };
};

/**
 * 用于線程間通信的實(shí)體類稚疹,防止列表錯亂的關(guān)鍵
 *  因?yàn)樗鼣y有原圖的url和要被加載的ImageView
 */
private static class LoaderResult
{
    public String uri;
    public ImageView imageView;
    public Bitmap bitmap;
    public LoaderResult(String uri, ImageView imageView, Bitmap bitmap)
    {
        this.uri = uri;
        this.imageView = imageView;
        this.bitmap = bitmap;
    }
}

3.異步加載圖片bindBitmap
包括了在子線程中同步加載圖片(從內(nèi)存居灯、磁盤和網(wǎng)絡(luò)加載)以及通過Handler將結(jié)果送給主線程
它是一個向外提供的接口,因此外界可以直接使用它

/**
 * load bitmap from memory cache or disk or http, 
 *  then bind bitmap and ImageView
 * @param uri
 * @param imageView
 */
public void bindBitmap(final String uri, final ImageView imageView)
{
    bindBitmap(uri, imageView, 0, 0);
}

/**
 * 封裝了從url獲取圖片到加載到特定的ImageView當(dāng)中的方法
 * 其中先是從內(nèi)存中獲取圖片内狗,如果獲取不到在調(diào)用loadBitmap獲取圖片
 * 
 * 如果是從網(wǎng)絡(luò)中獲取圖片怪嫌,則
 * 在將圖片加載到ImageView前,會將圖片的url和ImageView的tag進(jìn)行對比
 *  如果相同則說明ImageView沒有被復(fù)用柳沙,因此可以加載圖片
 *  如果不同則說名ImageView被復(fù)用隔了岩灭,放棄加載下載完成的圖片
 * 
 * @param url
 * @param imageView
 * @param reqWidth
 * @param reqHeight
 */
public void bindBitmap(final String url, final ImageView imageView,
        final int reqWidth, final int reqHeight)
{
    imageView.setTag(TAG_KEY_URI);
    
    Bitmap bitmap = loadBitmapFromMemoryCache(url);
    if(bitmap != null)
    {
        imageView.setImageBitmap(bitmap);
        return;
    }
    
    Runnable loadBitmapTask = new Runnable()
    {
        @Override
        public void run() {
            Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
            if(bitmap != null)
            {
                LoaderResult result = new LoaderResult(url, imageView, bitmap);
                mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
                    .sendToTarget();
            }
        };
    };
    
    THREAD_POOL_EXEUTOR.execute(loadBitmapTask);
    
}

*4.防止列表錯亂
前面一節(jié)已經(jīng)提到了如何防止列表錯亂的方法,這里強(qiáng)調(diào)一下ImageLoader中設(shè)置標(biāo)簽和對比標(biāo)簽的位置

1.給ImageView設(shè)置標(biāo)簽
在bindBitmap中赂鲤,在利用子線程同步加載圖片前將ImageView設(shè)置標(biāo)簽噪径,然后該imageView會在圖片下載完成后和圖片一起通過Handler傳送到主線程

imageView.setTag(TAG_KEY_URI);

2.取出ImageView的標(biāo)簽與圖片url對比
在Handler的handleMessage中柱恤,在用imageView設(shè)置圖片之前進(jìn)行對比

LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String uri = result.uri;
if(uri.equals(imageView.getTag(TAG_KEY_URI)))
{
    imageView.setImageBitmap(result.bitmap);
}else
{
    Log.w(TAG, "set image bitmap, but url has changed, ignored!");
}

圖片壓縮

參照12.1節(jié)的ImageResizer.java

內(nèi)存緩存

參照12.2.1節(jié)的LruCache
loadBitmapFromMemoryCache(String url)

磁盤緩存

參照12.2.2節(jié)的DiskLruCache
loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)

網(wǎng)絡(luò)拉取

參照12.2.2節(jié)的DiskLruCache
loadBitmapFromHttp(String url, int reqWidth, int reqHeight)


12.3 ListView或GridView的優(yōu)化

ListView是開發(fā)時經(jīng)常用到的控件,如果使用的不夠好找爱,會造成卡頓等體驗(yàn)不好的現(xiàn)象梗顺,下面就介紹可用且好用的優(yōu)化方法

核心思想

不要在主線程中做太耗時的操作

方法

1.不要再Adapter的getView中執(zhí)行耗時操作
列表的滑動觸發(fā)最多的方法就是Adapter的getView方法,如果getView方法里面執(zhí)行的操作太耗時车摄,就會造成卡頓現(xiàn)象寺谤。如果有耗時的操作比如像上面的從網(wǎng)絡(luò)加載圖片,那么就應(yīng)該開啟新線程異步加載圖片

2.控制異步任務(wù)的執(zhí)行頻率
既然上面使用了異步任務(wù)练般,那么要考慮到用戶頻繁滑動的情景矗漾,這個是百分百發(fā)生的場景,所以一定要注意
方法是薄料,ListView或GridView設(shè)置OnScollListener敞贡,在Adapter中設(shè)置一個布爾成員mIsListViewIdle用于記錄當(dāng)前列表是否在滑動從而在getView里面限制列表的加載,程序如下

/*
    為列表控件設(shè)置監(jiān)聽器摄职,并在滑動的時候設(shè)置Adapter的mIsListViewIdle成員
 */
mImageGridView.setOnScrollListener(new OnScollListener()
{
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        // TODO Auto-generated method stub
        if(scrollState == OnScrollListener.SCROLL_STATE_IDLE)
        {
            mImageAdapter.setIsGridViewIdle(true);
            mImageAdapter.notifyDataSetChanged();
        }else
        {
            mImageAdapter.setIsGridViewIdle(false);
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) {
        // TODO Auto-generated method stub
        
    }
    });

下面是ImageAdapter.java中的部分代碼

//ImageAdapter.java中的getView誊役,只有在mIsGridViewIdle為true即列表停止滑動的時候才加載數(shù)據(jù)
private boolean mIsGridViewIdle = true;

public void setIsGridViewIdle(boolean mIsGridViewIdle) {
    this.mIsGridViewIdle = mIsGridViewIdle;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // TODO Auto-generated method stub
    ViewHolder viewHolder = null;
    if(convertView == null)
    {
        convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item_image, parent, false);
        viewHolder = new ViewHolder();
        viewHolder.imageView = (ImageView) convertView.findViewById(R.id.iv_list_item);
        convertView.setTag(viewHolder);
    }else
    {
        viewHolder = (ViewHolder)convertView.getTag();
    }
    
    ImageView imageView = viewHolder.imageView;
    String url = getItem(position);
    
    if(!url.equals(imageView.getTag()+""))
    {
        imageView.setImageDrawable(mDefaultBitmapDrawable);
    }
    
    if(mIsGridViewIdle)
    {
        imageView.setTag(url);
        mImageLoader.bindBitmap(url, imageView, mImageWidth,mImageWidth);
    }
    
    return convertView;
}    

3.為Activity開啟硬件加速

android:hardwareAccelerated="true";
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谷市,隨后出現(xiàn)的幾起案子蛔垢,更是在濱河造成了極大的恐慌,老刑警劉巖迫悠,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹏漆,死亡現(xiàn)場離奇詭異,居然都是意外死亡创泄,警方通過查閱死者的電腦和手機(jī)艺玲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞠抑,“玉大人饭聚,你說我怎么就攤上這事「樽荆” “怎么了秒梳?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長箕速。 經(jīng)常有香客問我酪碘,道長,這世上最難降的妖魔是什么盐茎? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任婆跑,我火速辦了婚禮,結(jié)果婚禮上庭呜,老公的妹妹穿的比我還像新娘滑进。我一直安慰自己犀忱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布扶关。 她就那樣靜靜地躺著阴汇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪节槐。 梳的紋絲不亂的頭發(fā)上搀庶,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音铜异,去河邊找鬼哥倔。 笑死,一個胖子當(dāng)著我的面吹牛揍庄,可吹牛的內(nèi)容都是我干的咆蒿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼蚂子,長吁一口氣:“原來是場噩夢啊……” “哼沃测!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起食茎,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蒂破,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后别渔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體附迷,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年哎媚,在試婚紗的時候發(fā)現(xiàn)自己被綠了喇伯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抄伍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出管宵,到底是詐尸還是另有隱情截珍,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布箩朴,位于F島的核電站岗喉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏炸庞。R本人自食惡果不足惜钱床,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埠居。 院中可真熱鬧查牌,春花似錦事期、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胁孙,卻和暖如春唠倦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涮较。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工稠鼻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狂票。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓候齿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親苫亦。 傳聞我的和親對象是個殘疾皇子毛肋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容