[轉(zhuǎn)]教你寫Android ImageLoader框架之圖片緩存 (四)

本文轉(zhuǎn)自Mr.Simple的博客,如侵刪


前言

教你寫Android ImageLoader框架系列博文中,我們從基本架構(gòu)到具體實(shí)現(xiàn)已經(jīng)更新了大部分的內(nèi)容。今天铜秆,我們來講最后一個(gè)關(guān)鍵點(diǎn)誓琼,即圖片的緩存。為了用戶體驗(yàn)巍扛,通常情況下我們都會(huì)將已經(jīng)下載的圖片緩存起來领跛,一般來說內(nèi)存和本地都會(huì)有圖片緩存。那既然是框架电湘,必然需要有很好的定制性隔节,這讓我們又自然而然的想到了抽象。下面我們就一起來看看緩存的實(shí)現(xiàn)吧寂呛。


緩存接口

教你寫Android ImageLoader框架之圖片加載與加載策略我們聊到了Loader怎诫,然后闡述了AbsLoader的基本邏輯,其中就有圖片緩存贷痪。因此AbsLoader中必然含有緩存對(duì)象的引用幻妓。我們看看相關(guān)代碼:

/**
 * @author mrsimple
 */
public abstract class AbsLoader implements Loader {

    /**
     * 圖片緩存
     */
    private static BitmapCache mCache = SimpleImageLoader.getInstance().getConfig().bitmapCache;

    // 代碼省略
}

AbsLoader中定義了一個(gè)static的BitmapCache對(duì)象,這個(gè)就是圖片緩存對(duì)象劫拢。那為什么是static呢肉津?因?yàn)椴还躄oader有多少個(gè),緩存對(duì)象都應(yīng)該是共享的舱沧,也就是緩存只有一份妹沙。說了那么多,那我們先來了解一下BitmapCache吧熟吏。

public interface BitmapCache {

    public Bitmap get(BitmapRequest key);

    public void put(BitmapRequest key, Bitmap value);

    public void remove(BitmapRequest key);

}

BitmapCache很簡單距糖,只聲明了獲取、添加牵寺、移除三個(gè)方法來操作圖片緩存悍引。這里有依賴了一個(gè)BitmapRequest類,這個(gè)類代表了一個(gè)圖片加載請(qǐng)求帽氓,該類中有該請(qǐng)求對(duì)應(yīng)的ImageView趣斤、圖片uri、顯示Config等屬性黎休。在緩存這塊我們主要要使用圖片的uri來檢索緩存中是否含有該圖片浓领,緩存以圖片的uri為key,Bitmap為value來關(guān)聯(lián)存儲(chǔ)玉凯。另外需要BitmapRequest的ImageView寬度和高度,以此來按尺寸加載圖片镊逝。

定義BitmapCache接口還是為了可擴(kuò)展性壮啊,面向接口的編程的理念又再一次的浮現(xiàn)在你面前。如果是你撑蒜,你會(huì)作何設(shè)計(jì)呢歹啼?自己寫代碼來練習(xí)一下吧,看看自己作何考慮座菠,如果實(shí)現(xiàn)狸眼,這樣你才會(huì)從中有更深的領(lǐng)悟。


內(nèi)存緩存

既然是框架浴滴,那就需要接受用戶各種各樣的需求拓萌。但通常來說框架會(huì)有一些默認(rèn)的實(shí)現(xiàn),對(duì)于圖片緩存來說內(nèi)存緩存就其中的一個(gè)默認(rèn)實(shí)現(xiàn)升略,它會(huì)將已經(jīng)加載的圖片緩存到內(nèi)存中微王,大大地提升圖片重復(fù)加載的速度。內(nèi)存緩存我們的策略是使用LRU算法品嚣,直接使用了support.v4中的LruCache類炕倘,相關(guān)代碼如下。

/**
 * 圖片的內(nèi)存緩存,key為圖片的uri,值為圖片本身
 * 
 * @author mrsimple
 */
public class MemoryCache implements BitmapCache {

    private LruCache<String, Bitmap> mMemeryCache;

    public MemoryCache() {

        // 計(jì)算可使用的最大內(nèi)存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 取4分之一的可用內(nèi)存作為緩存
        final int cacheSize = maxMemory / 4;
        mMemeryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };

    }

    @Override
    public Bitmap get(BitmapRequest key) {
        return mMemeryCache.get(key.imageUri);
    }

    @Override
    public void put(BitmapRequest key, Bitmap value) {
        mMemeryCache.put(key.imageUri, value);
    }

    @Override
    public void remove(BitmapRequest key) {
        mMemeryCache.remove(key.imageUri);
    }

}

就是簡單的實(shí)現(xiàn)了BitmapCache接口翰撑,然后內(nèi)部使用LruCache類實(shí)現(xiàn)內(nèi)存緩存罩旋。比較簡單,就不做說明了眶诈。


sd卡緩存

對(duì)于圖片緩存涨醋,內(nèi)存緩存是不夠的,更多的需要是將圖片緩存到sd卡中逝撬,這樣用戶在下次進(jìn)入app時(shí)可以直接從本地加載圖片浴骂,避免重復(fù)地從網(wǎng)絡(luò)上讀取圖片數(shù)據(jù),即耗流量宪潮,用戶體驗(yàn)又不好靠闭。sd卡緩存我們使用了Jake Wharton的DiskLruCache類,我們的sd卡緩存類為DiskCache,代碼如下 :

public class DiskCache implements BitmapCache {

    /**
     * 1MB
     */
    private static final int MB = 1024 * 1024;

    /**
     * cache dir
     */
    private static final String IMAGE_DISK_CACHE = "bitmap";
    /**
     * Disk LRU Cache
     */
    private DiskLruCache mDiskLruCache;
    /**
     * Disk Cache Instance
     */
    private static DiskCache mDiskCache;

    /**
     * @param context
     */
    private DiskCache(Context context) {
        initDiskCache(context);
    }

    public static DiskCache getDiskCache(Context context) {
        if (mDiskCache == null) {
            synchronized (DiskCache.class) {
                if (mDiskCache == null) {
                    mDiskCache = new DiskCache(context);
                }
            }

        }
        return mDiskCache;
    }

    /**
     * 初始化sdcard緩存
     */
    private void initDiskCache(Context context) {
        try {
            File cacheDir = getDiskCacheDir(context, IMAGE_DISK_CACHE);
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            mDiskLruCache = DiskLruCache
                    .open(cacheDir, getAppVersion(context), 1, 50 * MB);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取sd緩存的目錄,如果掛載了sd卡則使用sd卡緩存坎炼,否則使用應(yīng)用的緩存目錄。
     * @param context Context
     * @param uniqueName 緩存目錄名,比如bitmap
     * @return
     */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            Log.d("", "### context : " + context + ", dir = " + context.getExternalCacheDir());
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }


        @Override
    public synchronized Bitmap get(final BitmapRequest bean) {
        // 圖片解析器
        BitmapDecoder decoder = new BitmapDecoder() {

            @Override
            public Bitmap decodeBitmapWithOption(Options options) {
                final InputStream inputStream = getInputStream(bean.imageUriMd5);
                Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null,
                        options);
                IOUtil.closeQuietly(inputStream);
                return bitmap;
            }
        };

        return decoder.decodeBitmap(bean.getImageViewWidth(),
                bean.getImageViewHeight());
    }

    private InputStream getInputStream(String md5) {
        Snapshot snapshot;
        try {
            snapshot = mDiskLruCache.get(md5);
            if (snapshot != null) {
                return snapshot.getInputStream(0);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    public void put(BitmapRequest key, Bitmap value) {
        // 代碼省略 
    }

    public void remove(BitmapRequest key) {
        // 代碼省略
    }

}

代碼比較簡單拦键,也就是實(shí)現(xiàn)BitmapCache谣光,然后包裝一下DiskLruCache類的方法實(shí)現(xiàn)圖片文件的增加、刪除芬为、獲取方法萄金。這里給大家介紹一個(gè)類蟀悦,是我為了簡化圖片按ImageView尺寸加載的輔助類,即BitmapDecoder氧敢。


BitmapDecoder

BitmapDecoder是一個(gè)按ImageView尺寸加載圖片的輔助類日戈,一般我加載圖片的過程是這樣的:

  1. 創(chuàng)建BitmapFactory.Options options,設(shè)置options.inJustDecodeBounds = true,使得只解析圖片尺寸等信息;
  2. 根據(jù)ImageView的尺寸來檢查是否需要縮小要加載的圖片以及計(jì)算縮放比例;
  3. 設(shè)置options.inJustDecodeBounds = false,然后按照options設(shè)置的縮小比例來加載圖片.

BitmapDecoder類使用decodeBitmap方法封裝了這個(gè)過程 ( 模板方法噢 ),用戶只需要實(shí)現(xiàn)一個(gè)子類孙乖,并且覆寫B(tài)itmapDecoder的decodeBitmapWithOption實(shí)現(xiàn)圖片加載即可完成這個(gè)過程(參考DiskCache中的get方法)浙炼。代碼如下 :

/**
 * 封裝先加載圖片bound,計(jì)算出inSmallSize之后再加載圖片的邏輯操作
 * 
 * @author mrsimple
 */
public abstract class BitmapDecoder {

    /**
     * @param options
     * @return
     */
    public abstract Bitmap decodeBitmapWithOption(Options options);

    /**
     * @param width 圖片的目標(biāo)寬度
     * @param height 圖片的目標(biāo)高度
     * @return
     */
    public Bitmap decodeBitmap(int width, int height) {
        // 如果請(qǐng)求原圖,則直接加載原圖
        if (width <= 0 || height <= 0) {
            return decodeBitmapWithOption(null);
        }

        // 1唯袄、獲取只加載Bitmap寬高等數(shù)據(jù)的Option, 即設(shè)置options.inJustDecodeBounds = true;
        BitmapFactory.Options options = getJustDecodeBoundsOptions();
        // 2弯屈、通過options加載bitmap,此時(shí)返回的bitmap為空,數(shù)據(jù)將存儲(chǔ)在options中
        decodeBitmapWithOption(options);
        // 3恋拷、計(jì)算縮放比例, 并且將options.inJustDecodeBounds設(shè)置為false;
        calculateInSmall(options, width, height);
        // 4资厉、通過options設(shè)置的縮放比例加載圖片
        return decodeBitmapWithOption(options);
    }

    /**
     * 獲取BitmapFactory.Options,設(shè)置為只解析圖片邊界信息
     */
    private Options getJustDecodeBoundsOptions() {
        //
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 設(shè)置為true,表示解析Bitmap對(duì)象,該對(duì)象不占內(nèi)存
        options.inJustDecodeBounds = true;
        return options;
    }

    protected void calculateInSmall(Options options, int width, int height) {
        // 設(shè)置縮放比例
        options.inSampleSize = computeInSmallSize(options, width, height);
        // 圖片質(zhì)量
        options.inPreferredConfig = Config.RGB_565;
        // 設(shè)置為false,解析Bitmap對(duì)象加入到內(nèi)存中
        options.inJustDecodeBounds = false;
        options.inPurgeable = true;
        options.inInputShareable = true;
    }

    private int computeInSmallSize(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 (height > reqHeight || width > reqWidth) {
            // Calculate ratios of height and width to requested height and
            // width
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);

            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
            final float totalPixels = width * height;

            // Anything more than 2x the requested pixels we'll sample down
            // further
            final float totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
                inSampleSize++;
            }
        }
        return inSampleSize;
    }

}

在decodeBitmap中蔬顾,我們首先創(chuàng)建BitmapFactory.Options對(duì)象,并且設(shè)置
options.inJustDecodeBounds = true宴偿,
然后第一次調(diào)用decodeBitmapWithOption(options),
使得只解析圖片尺寸等信息;然后調(diào)用calculateInSmall方法诀豁,該方法會(huì)調(diào)用computeInSmallSize來根據(jù)ImageView的尺寸來檢查是否需要縮小要加載的圖片以及計(jì)算縮放比例窄刘,在calculateInSmall方法的最后將 options.inJustDecodeBounds = false,使得下次再次decodeBitmapWithOption(options)時(shí)會(huì)加載圖片且叁;那最后一步必然就是調(diào)用decodeBitmapWithOption(options)啦都哭,這樣圖片就會(huì)按照按照options設(shè)置的縮小比例來加載圖片了。

我們使用這個(gè)輔助類封裝了這個(gè)麻煩逞带、重復(fù)的過程欺矫,在一定程度上簡化了代碼,也使得代碼的可復(fù)用性更高展氓,也是模板方法模式的一個(gè)較好的示例穆趴。


二級(jí)緩存

有了內(nèi)存和sd卡緩存,其實(shí)這還不夠遇汞。我們的需求很可能就是這個(gè)緩存會(huì)同時(shí)有內(nèi)存和sd卡緩存未妹,這樣上述兩種緩存的優(yōu)點(diǎn)我們就會(huì)具備,這里我們把它稱為二級(jí)緩存空入÷缢看看代碼吧,也很簡單歪赢。

/**
 * 綜合緩存,內(nèi)存和sd卡雙緩存
 * 
 * @author mrsimple
 */
public class DoubleCache implements BitmapCache {
    DiskCache mDiskCache;
    MemoryCache mMemoryCache = new MemoryCache();

    public DoubleCache(Context context) {
        mDiskCache = DiskCache.getDiskCache(context);
    }

    @Override
    public Bitmap get(BitmapRequest key) {
        Bitmap value = mMemoryCache.get(key);
        if (value == null) {
            value = mDiskCache.get(key);
            saveBitmapIntoMemory(key, value);
        }
        return value;
    }

    private void saveBitmapIntoMemory(BitmapRequest key, Bitmap bitmap) {
        // 如果Value從disk中讀取,那么存入內(nèi)存緩存
        if (bitmap != null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    @Override
    public void put(BitmapRequest key, Bitmap value) {
        mDiskCache.put(key, value);
        mMemoryCache.put(key, value);
    }

    @Override
    public void remove(BitmapRequest key) {
        mDiskCache.remove(key);
        mMemoryCache.remove(key);
    }

}

其實(shí)就是封裝了內(nèi)存緩存和sd卡緩存的相關(guān)操作嘛~ 那我就不要再費(fèi)口舌了


自定義緩存

緩存是有很多實(shí)現(xiàn)策略的化戳,既然我們要可擴(kuò)展性,那就要允許用戶注入自己的緩存實(shí)現(xiàn)埋凯。只要你實(shí)現(xiàn)BitmapCache点楼,就可以將它通過ImageLoaderConfig注入到ImageLoader內(nèi)部扫尖。

private void initImageLoader() {
     ImageLoaderConfig config = new ImageLoaderConfig()
             .setLoadingPlaceholder(R.drawable.loading)
             .setNotFoundPlaceholder(R.drawable.not_found)
             .setCache(new MyCache())
     // 初始化
     SimpleImageLoader.getInstance().init(config);
 }
MyCache.java
// 自定義緩存實(shí)現(xiàn)類
public class MyCache implements BitmapCache {

    // 代碼

    @Override
    public Bitmap get(BitmapRequest key) {
        // 你的代碼
    }

    @Override
    public void put(BitmapRequest key, Bitmap value) {
        // 你的代碼  
    }

    @Override
    public void remove(BitmapRequest key) {
        // 你的代碼
    }

}

總結(jié)

ImageLoader系列到這里就算結(jié)束了,我們從基本架構(gòu)掠廓、具體實(shí)現(xiàn)换怖、設(shè)計(jì)上面詳細(xì)的闡述了一個(gè)簡單、可擴(kuò)展性較好的ImageLoader實(shí)現(xiàn)過程蟀瞧,希望大家看完這個(gè)系列之后能夠自己去實(shí)現(xiàn)一遍沉颂,這樣你會(huì)發(fā)現(xiàn)一些具體的問題,領(lǐng)悟能夠更加的深刻黄橘。如果你在看這系列博客的過程中兆览,真的能夠從中體會(huì)到面向?qū)ο蟮幕驹瓌t、設(shè)計(jì)思考等東西塞关,而不是說”我擦抬探,我又找到了一個(gè)可以copy來用的ImageLoader”,那我就覺得我做的這些分享到達(dá)目的了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帆赢,一起剝皮案震驚了整個(gè)濱河市小压,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌椰于,老刑警劉巖怠益,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異瘾婿,居然都是意外死亡蜻牢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門偏陪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抢呆,“玉大人,你說我怎么就攤上這事笛谦”埃” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵饥脑,是天一觀的道長恳邀。 經(jīng)常有香客問我,道長灶轰,這世上最難降的妖魔是什么谣沸? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮笋颤,結(jié)果婚禮上乳附,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好许溅,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秉版,像睡著了一般贤重。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上清焕,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天并蝗,我揣著相機(jī)與錄音,去河邊找鬼秸妥。 笑死滚停,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的粥惧。 我是一名探鬼主播键畴,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼突雪!你這毒婦竟也來了起惕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤咏删,失蹤者是張志新(化名)和其女友劉穎惹想,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體督函,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘀粱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辰狡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锋叨。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖搓译,靈堂內(nèi)的尸體忽然破棺而出悲柱,到底是詐尸還是另有隱情,我是刑警寧澤些己,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布豌鸡,位于F島的核電站,受9級(jí)特大地震影響段标,放射性物質(zhì)發(fā)生泄漏涯冠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一逼庞、第九天 我趴在偏房一處隱蔽的房頂上張望蛇更。 院中可真熱鬧,春花似錦、人聲如沸派任。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掌逛。三九已至师逸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豆混,已是汗流浹背篓像。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留皿伺,地道東北人员辩。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像鸵鸥,于是被迫代替她去往敵國和親奠滑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

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