我的圖片四級(jí)緩存

開發(fā)App一定涉及到圖片加載、圖片處理,那就必須會(huì)用到三方的圖片框架,要么選擇自己封裝。至于主流的三方圖片框架,就不得不說老牌的ImageLoader、如今更流行的Glide、Picasso和Fresco。但三方的框架本文不會(huì)過多介紹。

Glide等框架,畢竟是大神及團(tuán)隊(duì)花費(fèi)很大精力開發(fā)和維護(hù)的開源框架,他們的設(shè)計(jì)思路、性能優(yōu)化、代碼規(guī)范等等很值得我們學(xué)習(xí),之前一段時(shí)間也研究過Glide的源碼(不得不由衷佩服)。

今天,將自己對(duì)于圖片加載的思路想法,也借鑒了開源框架的一些好的點(diǎn),封裝了一個(gè)圖片加載框架——JsLoader。(github地址:https://github.com/shuaijia/JsImageLoader)與大家分享。

文章目錄:


這里寫圖片描述

前言

至于圖片的網(wǎng)絡(luò)請(qǐng)求,我這里還是使用Android原生提供的HttpUrlConnection;請(qǐng)求網(wǎng)絡(luò)圖片時(shí),開啟子線程進(jìn)行操作,使用線程池對(duì)線程進(jìn)行統(tǒng)一管理;線程間通信還是用了Handler;提到圖片加載,大家肯定會(huì)立刻想到圖片的三級(jí)緩存(內(nèi)存—外存—網(wǎng)絡(luò)),但我這里提供一個(gè)新的思路——四級(jí)緩存,與三級(jí)緩存不同的是內(nèi)存又分為了兩級(jí),這些稍后會(huì)詳細(xì)介紹到。

本文目的在于和大家分享一個(gè)圖片框架的封裝思路,至于代碼的優(yōu)化,如使用OkHttp替換HttpUrlConnection,使用RxJava替換Handler等,或者有別的不足的地方,也希望大家能夠反饋給我,我們一起進(jìn)步。

先看下整體流程圖:

這里寫圖片描述

線程池

public class MyThreadFactory {

    //Android的線程池類
    private static ThreadPoolExecutor threadPoolExecutor=null;
    //獲取當(dāng)前用戶的手機(jī)的CPU的核心數(shù)
    private static int num= Runtime.getRuntime().availableProcessors();
    //用于存儲(chǔ)提交任務(wù)的任務(wù)隊(duì)列
    private static BlockingDeque<Runnable> workQueue=new LinkedBlockingDeque<>(num*50);
    private MyThreadFactory(){
    }
    public static ThreadPoolExecutor getThreadPoolExecutor(){
        if(null==threadPoolExecutor){
            threadPoolExecutor=new ThreadPoolExecutor(num*2, num*4, 8, TimeUnit.SECONDS, workQueue, new ThreadPoolExecutor.CallerRunsPolicy());
//            threadPoolExecutor=new ThreadPoolExecutor(1, 1, 8, TimeUnit.SECONDS, workQueue, new ThreadPoolExecutor.CallerRunsPolicy());
        }
        return threadPoolExecutor;
    }

}

當(dāng)前類是一個(gè)線程池的管理類。由于當(dāng)前的線程池,在整個(gè)項(xiàng)目中不需要?jiǎng)?chuàng)建多個(gè)對(duì)象,直接使用單例模式進(jìn)行創(chuàng)建。

補(bǔ)充:Android中的線程池
在Android中使用線程池的類是:ThreadPoolExecutor;

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(int corePoolSize, int maxinumPoolSize, long keepAliveTime, TimeUnit unit, BlockingDeque<Runnable> workQueue, ThreadFactory threadFactory);

參數(shù):

  • int corePoolSize : 線程池中的核心線程數(shù)
  • int maxinumPoolSize :線程池中允許的最大線程數(shù)目
  • long keepAliveTime :非核心線程的超時(shí)時(shí)間,超出這個(gè)時(shí)間非核心線程會(huì)被回收
  • TimeUnit unit :非核心線程的超時(shí)時(shí)間的時(shí)間單位
  • BlockingDeque<Runnable> workQueue : 保存需要線程池執(zhí)行的任務(wù)的列表
  • ThreadFactory threadFactory : 線程工廠,只是一個(gè)接口,只有一個(gè)方法Thread newThread(Runnable r)

在上文展示的類中,我們獲取了手機(jī)的CPU核心數(shù)num,本線程池的核心線程數(shù)為CPU數(shù)的2倍,最大線程數(shù)為CPU核心數(shù)的4倍。

內(nèi)存一級(jí)緩存

private static final HashMap<String,Bitmap> mHardBitmapCache=new LinkedHashMap<String,Bitmap>(
            M_LINK_SIZE/2,0.75f,true){

    /**
     * 這個(gè)方法是是put或putAll時(shí)調(diào)用,默認(rèn)返回false,表示添加數(shù)據(jù)時(shí)不移除最舊的數(shù)據(jù).
     * @param eldest
     * @return
     */
    @Override
    protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) {
        if (size() > M_LINK_SIZE) {
            // 當(dāng)map的size大于30時(shí),把最近不常用的key放到mSoftBitmapCache中,從而保證mHardBitmapCache的效率
            Bitmap value = eldest.getValue();
            if (value != null) {
                mWeakBitmapCache.put(eldest.getKey(),new SoftReference<Bitmap>(value));
            }
            return true;
        }
        return false;
    }
};

定義的內(nèi)存中的一級(jí)緩存,即保存作為強(qiáng)引用的位置的HashMap。

此處HashMap使用的是LinkedHashMap。LinkedHashMap 是HashMap的一個(gè)子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時(shí),先得到的記錄肯定是先插入的。也可以在構(gòu)造時(shí)用帶參數(shù),按照應(yīng)用次數(shù)排序。在遍歷的時(shí)候會(huì)比HashMap慢,不過有種情況例外,當(dāng)HashMap容量很大,實(shí)際數(shù)據(jù)較少時(shí),遍歷起來可能會(huì)比 LinkedHashMap慢,因?yàn)長(zhǎng)inkedHashMap的遍歷速度只和實(shí)際數(shù)據(jù)有關(guān),和容量無關(guān),而HashMap的遍歷速度和他的容量有關(guān)。

正是由于LinkedHashMap具有記憶功能,最近插入的最新訪問,就符合了我們的最近最多使用的原則。但由于其遍歷速度慢,我們對(duì)其容量進(jìn)行設(shè)定,最多30和元素。

重寫removeEldestEntry方法,當(dāng)map的size大于30時(shí),把最近不常用的key放到mSoftBitmapCache中(也就是內(nèi)存第二級(jí)緩存),從而保證mHardBitmapCache的效率。

這里我們?cè)贛ap中是以Url和Bitmap為Key-Value存儲(chǔ)的,由于LinkedHashMap存放少,而且插入移出快,所以這里用的是Bitmap的強(qiáng)引用。

如果LinkedHashMap中包含我們需要的圖片,則將圖片直接返回。但是注意:此時(shí)我們認(rèn)為此圖使用頻率更高,因此我們需要先將該元素移出,在加入(這是由于該map后插入的遍歷時(shí)先讀?。?。

mHardBitmapCache.remove(netUrlKey);
mHardBitmapCache.put(netUrlKey,usefulBitmap);

此為內(nèi)存的一級(jí)緩存。

內(nèi)存二級(jí)緩存

如果內(nèi)存的LinkedHashMap中未獲取到我們想要的圖片的話,在二級(jí)緩存中進(jìn)行查找。

private static Map<String, SoftReference<Bitmap>> mWeakBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(
            M_LINK_SIZE / 2);

這時(shí)就用到了ConcurrentHashMap,它的最大特點(diǎn)就是線程安全、高并發(fā)、存儲(chǔ)量大。由于存儲(chǔ)量大,所以我們存放Bitmap時(shí)就需要使用其軟引用了。

如果此map中含有需要的圖片,則先取出其軟引用,在從軟引用中獲取Bitmap對(duì)象返回。再將其移至一級(jí)緩存中。

內(nèi)存的讀取整體代碼如下:

    /**
     * 這里定義的操作方法完成的是從內(nèi)存中的Map中獲取圖片的對(duì)象
     * 既然已經(jīng)在內(nèi)存中了,默認(rèn)已經(jīng)完成了壓縮
     *
     * @param netUrlKey  作為圖片在Map中唯一標(biāo)志的網(wǎng)絡(luò)圖片URL
     * @return
     */
    public static Bitmap getBitmapFromRAM(String netUrlKey){

        if(mHardBitmapCache.containsKey(netUrlKey)){

            Bitmap usefulBitmap=mHardBitmapCache.get(netUrlKey);
            if(null!=usefulBitmap){
                //如果存在正在內(nèi)存中的Bitmap圖片,將圖片的使用級(jí)別向前提,并返回Bitmap對(duì)象
                mHardBitmapCache.remove(netUrlKey);
                mHardBitmapCache.put(netUrlKey,usefulBitmap);
                return usefulBitmap;

            }else{
                //這里的情況是雖然在集合中包含對(duì)應(yīng)的Key但是通過key得不到對(duì)應(yīng)的Bitmap,此時(shí)將
                //key從Map中清楚,并返回null
                mHardBitmapCache.remove(netUrlKey);
                return null;
            }
        }else{
            //如果在強(qiáng)引用中不包含對(duì)應(yīng)的key,那么在軟引用中進(jìn)行查找
            if(mWeakBitmapCache.containsKey(netUrlKey)){
                SoftReference<Bitmap> usefulSoftBitmap=mWeakBitmapCache.get(netUrlKey);
                if(null!=usefulSoftBitmap){
                    //從軟應(yīng)用中獲取出對(duì)應(yīng)的Bitmap對(duì)象
                    Bitmap usefulBitmap = usefulSoftBitmap.get();
                    if(null!=usefulBitmap){
                        //將軟引用中的低級(jí)別圖片轉(zhuǎn)移到強(qiáng)引用中
                        mHardBitmapCache.put(netUrlKey,usefulBitmap);
                        return usefulBitmap;
                    }else{
                        //軟引用中包含key但是獲取不到圖片
                        mWeakBitmapCache.remove(netUrlKey);
                        return null;
                    }

                }else{
                    //軟引用中包含key但是獲取不到圖片
                    mWeakBitmapCache.remove(netUrlKey);
                    return null;
                }
            }else{
                //軟引用中也不包括這個(gè)key,那么從判斷SD卡中是否存在這個(gè)資源圖片
                return null;
            }
        }
    }

特別聲明:在存放入內(nèi)存前,會(huì)將圖片進(jìn)行壓縮。

SD卡緩存

內(nèi)存中沒有圖片的話,就去文件中查找:

    /**
     * 獲取已經(jīng)保存的數(shù)據(jù)的位置的路徑
     *
     * @param netUrlorPath
     * @return
     */
    private static String getSavedPath(String netUrlorPath) {

        String savedPath = null;
        if (StorageUtil.isPhoneHaveSD()) {
            // 創(chuàng)建以SD卡根目錄為路徑的File對(duì)象
            File fileBySD = new File(StorageUtil.getPathBySD());
            // 創(chuàng)建SD卡根目錄下以當(dāng)前應(yīng)用包名為文件夾的文件對(duì)象,并驗(yàn)證是否存在當(dāng)前目錄
            File fileBySDSon = new File(fileBySD, PackageUtil.getAppPackageName());
            // File fileBySDSon=new File(fileBySD,"AA");
            if (fileBySDSon.exists()) {
                String md5Url = EncryptUtil.md5(netUrlorPath);
                // 以包名為文件夾的對(duì)象存在的時(shí)候,通過將文件對(duì)象和圖片的名稱的拼接構(gòu)建文件對(duì)象
                File imageFile = new File(fileBySDSon, URLEncoder.encode(md5Url));
                if (imageFile.exists()) {
                    // 圖片文件對(duì)象存在的時(shí)候獲取當(dāng)前的圖片對(duì)象對(duì)應(yīng)的路徑
                    savedPath = imageFile.getAbsolutePath();
                } else {
                    return null;
                }
            } else {
                return null;
            }
        } else {
            // 創(chuàng)建以Cache根目錄為路徑的File對(duì)象
            File fileByCache = new File(StorageUtil.getPathBycache());
            // 創(chuàng)建SD卡根目錄下以當(dāng)前應(yīng)用包名為文件夾的文件對(duì)象,并驗(yàn)證是否存在當(dāng)前目錄
            File fileByCacheSon = new File(fileByCache, PackageUtil.getAppPackageName());
            // File fileByCacheSon=new File(fileByCache,"AA");
            if (fileByCacheSon.exists()) {
                String md5Url = EncryptUtil.md5(netUrlorPath);
                // 以包名為文件夾的對(duì)象存在的時(shí)候,通過將文件對(duì)象和圖片的名稱的拼接構(gòu)建文件對(duì)象
                File imageFile = new File(fileByCacheSon, URLEncoder.encode(md5Url));
                if (imageFile.exists()) {
                    // 圖片文件對(duì)象存在的時(shí)候獲取當(dāng)前的圖片對(duì)象對(duì)應(yīng)的路徑
                    savedPath = imageFile.getAbsolutePath();
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }
        return savedPath;

    }

上方代碼是根據(jù)圖片url獲取到圖片在文件中的路徑。

所以的緩存圖片,會(huì)保存在本包名文件夾下,以u(píng)rl的md5值為名字的文件中,判斷到有此文件的話,將文件路徑返回。

    /**
     * 這里完成的操作是判斷傳遞進(jìn)來的路徑是否包括Bitmap對(duì)象,如果存在將Bitmap對(duì)象返回 否則返回null
     *
     * @param saveTime
     *            圖片的保存時(shí)間
     * @param netUrl
     *            網(wǎng)絡(luò)圖片的網(wǎng)絡(luò)路徑作為文件名稱
     * @return
     */
    public static Bitmap getBitmapFromSD(long saveTime, String netUrl) {

        long nativeSaveTime = saveTime > 0 ? saveTime : DATA_DEFAULT_SAVETIME;
        long actualSaveTime = 0L;
        if (null == netUrl) {
            return null;
        }
        String imageSavePath = getSavedPath(netUrl);
    //  System.out.println("已經(jīng)存儲(chǔ)的圖片的路徑::" + imageSavePath);
        if (null == imageSavePath) {
            return null;
        }
        File imageFile = new File(imageSavePath);
        if (!imageFile.exists()) {
            // throw new StructException("需要的文件不存在!");
            return null;
        }
        actualSaveTime = System.currentTimeMillis() - imageFile.lastModified();
        if (actualSaveTime > nativeSaveTime) {
            imageFile.delete();
            //System.out.println("文件超時(shí)了!");
            return null;

        }
        /**
         * 這里的邏輯是當(dāng)文件對(duì)象存在的時(shí)候?qū)⒃撐募?duì)象獲取出來,并生成Bitmap對(duì)象并返回
         */
        // Bitmap sdBitmap= BitmapFactory.decodeFile(imageSavePath);
        // 從SD卡中獲取圖片的時(shí)候直接進(jìn)行圖片的壓縮處理防止OOM

        //System.out.println("保存的圖片的鏈接:" + imageSavePath);
        Bitmap sdBitmap = ImageUtil.getCompressBitmapBYScreen(imageSavePath);
        return sdBitmap;

    }

判斷到文件中有我們需要的圖片,會(huì)拿到文件路徑。但是,我們有設(shè)定文件有效時(shí)間,超過該時(shí)間則視為超時(shí),返回null,否則讀取該文件。根據(jù)圖片的路徑和當(dāng)前手機(jī)的默認(rèn)屏幕分辨率進(jìn)行圖片壓縮再返回。

文件中有該圖片,那就將該圖片移植內(nèi)存中,以提高優(yōu)先級(jí),而且內(nèi)存兩級(jí)中都放入該圖片。

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

以上都沒拿到圖片的話,那只能從網(wǎng)絡(luò)來獲取啦!

對(duì)http還是https進(jìn)行判斷,分別對(duì)應(yīng)使用HttpUrlConnection和HttpsUrlConnection。他們代碼類似,就只貼其中一個(gè)了。

    public static InputStream getHttpIOByGet(String netUrl) throws IOException {

//        System.out.println("網(wǎng)絡(luò)的鏈接:"+netUrl);

        URL url = new URL(netUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        int code = conn.getResponseCode();
//        System.out.println("返回碼::"+code);
        if (code == 200) {
            InputStream is = conn.getInputStream();
            return is;
        }else{
            return null;
        }

    }

返回碼200,表示請(qǐng)求成功,就將輸入流返回,否則返回null。

Bitmap bitmap= BitmapFactory.decodeStream(inputStream);

獲取輸入流后,使用上方代碼獲取Bitmap對(duì)象,原因大家懂的。

獲取到圖片后,再依次存入sd卡和內(nèi)存中,因?yàn)槭呛檬遣僮?,就在子線程中進(jìn)行了。

new Thread(){
    @Override
    public void run() {
        //3.1、從網(wǎng)絡(luò)獲取圖片
        //3.2、將圖片壓縮后的保存到SD卡或機(jī)身內(nèi)存中
        FileUtil.putBitmapToSD(netUrl, finalThreeCacheBitmap);
        //3.4、將圖片保存到Map中
        CacheRAM.putBitmapToRAM(netUrl, finalThreeCacheBitmap);
    }
}.start();

圖片壓縮

這里主要想介紹下圖片的壓縮:因?yàn)閳D片加載很容易造成OOM,所以圖片壓縮處理顯得尤為重要。

提供集中壓縮方式:

  • 根據(jù)期望大小壓縮
  • 根據(jù)期望尺寸壓縮
  • 根據(jù)當(dāng)前手機(jī)的默認(rèn)屏幕分辨率進(jìn)行圖片的壓縮

這里就不再貼代碼了,可以去我的github中查看。https://github.com/shuaijia/JsImageLoader/blob/master/jsimageloader/src/main/java/com/jia/jsloader/utils/ImageUtil.java

使用

1、添依賴

allprojects {
  repositories {
    ...
    maven { url 'https://www.jitpack.io' }
  }
}

dependencies {
  compile 'com.github.shuaijia:JsImageLoader:v1.0'
}

2、添權(quán)限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

3、繼承JsApplication

4、請(qǐng)求

JsLoader.with(this)
    .load("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=699359866,1092793192&fm=27&gp=0.jpg")
    .defaultImg(R.mipmap.default)
    .errorImg(R.mipmap.error)
    .into(imageView);

由于本人水平有限,不免有不對(duì)或不足的地方,希望大家能夠提出,我們共同進(jìn)步。

更多精彩內(nèi)容,請(qǐng)關(guān)注我的微信公眾號(hào)——Android機(jī)動(dòng)車

這里寫圖片描述
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,799評(píng)論 11 349
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,039評(píng)論 25 709
  • 思考:正態(tài)分布,用在服裝行業(yè),比如,店鋪里服裝的款式、價(jià)格、顏色等都屬于正態(tài)分布。 還有,比如店鋪員工的業(yè)績(jī)排名、...
    楊雪雪閱讀 489評(píng)論 0 1
  • 我們對(duì)自尊的過分強(qiáng)調(diào)無法解決社會(huì)問題和人自身的問題,反而會(huì)引起這些問題。 這句話讓我突然想起小時(shí)候看過的一本書,叫...
    柳濤虹閱讀 315評(píng)論 0 0
  • 微視頻引流法 (微商加粉營(yíng)銷更多請(qǐng)看:2017微商吸粉運(yùn)營(yíng)教程) 1.營(yíng)銷內(nèi)容化,內(nèi)容即營(yíng)銷 具體來說就是將娛樂和...
    助夢(mèng)微創(chuàng)閱讀 258評(píng)論 0 0

友情鏈接更多精彩內(nèi)容