我的图片四级缓存

开发App一定涉及到图片加载、图片处理,那就必须会用到三方的图片框架,要么选择自己封装。至于主流的三方图片框架,就不得不说老牌的ImageLoader、如今更流行的Glide、Picasso和Fresco。但三方的框架本文不会过多介绍。

Glide等框架,毕竟是大神及团队花费很大精力开发和维护的开源框架,他们的设计思路、性能优化、代码规范等等很值得我们学习,之前一段时间也研究过Glide的源码(不得不由衷佩服)。

今天,将自己对于图片加载的思路想法,也借鉴了开源框架的一些好的点,封装了一个图片加载框架——JsLoader。(github地址:https://github.com/shuaijia/JsImageLoader)与大家分享。

文章目录:


这里写图片描述

前言

至于图片的网络请求,我这里还是使用Android原生提供的HttpUrlConnection;请求网络图片时,开启子线程进行操作,使用线程池对线程进行统一管理;线程间通信还是用了Handler;提到图片加载,大家肯定会立刻想到图片的三级缓存(内存—外存—网络),但我这里提供一个新的思路——四级缓存,与三级缓存不同的是内存又分为了两级,这些稍后会详细介绍到。

本文目的在于和大家分享一个图片框架的封装思路,至于代码的优化,如使用OkHttp替换HttpUrlConnection,使用RxJava替换Handler等,或者有别的不足的地方,也希望大家能够反馈给我,我们一起进步。

先看下整体流程图:

这里写图片描述

线程池

public class MyThreadFactory {

    //Android的线程池类
    private static ThreadPoolExecutor threadPoolExecutor=null;
    //获取当前用户的手机的CPU的核心数
    private static int num= Runtime.getRuntime().availableProcessors();
    //用于存储提交任务的任务队列
    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;
    }

}

当前类是一个线程池的管理类。由于当前的线程池,在整个项目中不需要创建多个对象,直接使用单例模式进行创建。

补充:Android中的线程池
在Android中使用线程池的类是:ThreadPoolExecutor;

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

参数:

  • int corePoolSize : 线程池中的核心线程数
  • int maxinumPoolSize :线程池中允许的最大线程数目
  • long keepAliveTime :非核心线程的超时时间,超出这个时间非核心线程会被回收
  • TimeUnit unit :非核心线程的超时时间的时间单位
  • BlockingDeque<Runnable> workQueue : 保存需要线程池执行的任务的列表
  • ThreadFactory threadFactory : 线程工厂,只是一个接口,只有一个方法Thread newThread(Runnable r)

在上文展示的类中,我们获取了手机的CPU核心数num,本线程池的核心线程数为CPU数的2倍,最大线程数为CPU核心数的4倍。

内存一级缓存

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

    /**
     * 这个方法是是put或putAll时调用,默认返回false,表示添加数据时不移除最旧的数据.
     * @param eldest
     * @return
     */
    @Override
    protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) {
        if (size() > M_LINK_SIZE) {
            // 当map的size大于30时,把最近不常用的key放到mSoftBitmapCache中,从而保证mHardBitmapCache的效率
            Bitmap value = eldest.getValue();
            if (value != null) {
                mWeakBitmapCache.put(eldest.getKey(),new SoftReference<Bitmap>(value));
            }
            return true;
        }
        return false;
    }
};

定义的内存中的一级缓存,即保存作为强引用的位置的HashMap。

此处HashMap使用的是LinkedHashMap。LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

正是由于LinkedHashMap具有记忆功能,最近插入的最新访问,就符合了我们的最近最多使用的原则。但由于其遍历速度慢,我们对其容量进行设定,最多30和元素。

重写removeEldestEntry方法,当map的size大于30时,把最近不常用的key放到mSoftBitmapCache中(也就是内存第二级缓存),从而保证mHardBitmapCache的效率。

这里我们在Map中是以Url和Bitmap为Key-Value存储的,由于LinkedHashMap存放少,而且插入移出快,所以这里用的是Bitmap的强引用。

如果LinkedHashMap中包含我们需要的图片,则将图片直接返回。但是注意:此时我们认为此图使用频率更高,因此我们需要先将该元素移出,在加入(这是由于该map后插入的遍历时先读取)。

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

此为内存的一级缓存。

内存二级缓存

如果内存的LinkedHashMap中未获取到我们想要的图片的话,在二级缓存中进行查找。

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

这时就用到了ConcurrentHashMap,它的最大特点就是线程安全、高并发、存储量大。由于存储量大,所以我们存放Bitmap时就需要使用其软引用了。

如果此map中含有需要的图片,则先取出其软引用,在从软引用中获取Bitmap对象返回。再将其移至一级缓存中。

内存的读取整体代码如下:

    /**
     * 这里定义的操作方法完成的是从内存中的Map中获取图片的对象
     * 既然已经在内存中了,默认已经完成了压缩
     *
     * @param netUrlKey  作为图片在Map中唯一标志的网络图片URL
     * @return
     */
    public static Bitmap getBitmapFromRAM(String netUrlKey){

        if(mHardBitmapCache.containsKey(netUrlKey)){

            Bitmap usefulBitmap=mHardBitmapCache.get(netUrlKey);
            if(null!=usefulBitmap){
                //如果存在正在内存中的Bitmap图片,将图片的使用级别向前提,并返回Bitmap对象
                mHardBitmapCache.remove(netUrlKey);
                mHardBitmapCache.put(netUrlKey,usefulBitmap);
                return usefulBitmap;

            }else{
                //这里的情况是虽然在集合中包含对应的Key但是通过key得不到对应的Bitmap,此时将
                //key从Map中清楚,并返回null
                mHardBitmapCache.remove(netUrlKey);
                return null;
            }
        }else{
            //如果在强引用中不包含对应的key,那么在软引用中进行查找
            if(mWeakBitmapCache.containsKey(netUrlKey)){
                SoftReference<Bitmap> usefulSoftBitmap=mWeakBitmapCache.get(netUrlKey);
                if(null!=usefulSoftBitmap){
                    //从软应用中获取出对应的Bitmap对象
                    Bitmap usefulBitmap = usefulSoftBitmap.get();
                    if(null!=usefulBitmap){
                        //将软引用中的低级别图片转移到强引用中
                        mHardBitmapCache.put(netUrlKey,usefulBitmap);
                        return usefulBitmap;
                    }else{
                        //软引用中包含key但是获取不到图片
                        mWeakBitmapCache.remove(netUrlKey);
                        return null;
                    }

                }else{
                    //软引用中包含key但是获取不到图片
                    mWeakBitmapCache.remove(netUrlKey);
                    return null;
                }
            }else{
                //软引用中也不包括这个key,那么从判断SD卡中是否存在这个资源图片
                return null;
            }
        }
    }

特别声明:在存放入内存前,会将图片进行压缩。

SD卡缓存

内存中没有图片的话,就去文件中查找:

    /**
     * 获取已经保存的数据的位置的路径
     *
     * @param netUrlorPath
     * @return
     */
    private static String getSavedPath(String netUrlorPath) {

        String savedPath = null;
        if (StorageUtil.isPhoneHaveSD()) {
            // 创建以SD卡根目录为路径的File对象
            File fileBySD = new File(StorageUtil.getPathBySD());
            // 创建SD卡根目录下以当前应用包名为文件夹的文件对象,并验证是否存在当前目录
            File fileBySDSon = new File(fileBySD, PackageUtil.getAppPackageName());
            // File fileBySDSon=new File(fileBySD,"AA");
            if (fileBySDSon.exists()) {
                String md5Url = EncryptUtil.md5(netUrlorPath);
                // 以包名为文件夹的对象存在的时候,通过将文件对象和图片的名称的拼接构建文件对象
                File imageFile = new File(fileBySDSon, URLEncoder.encode(md5Url));
                if (imageFile.exists()) {
                    // 图片文件对象存在的时候获取当前的图片对象对应的路径
                    savedPath = imageFile.getAbsolutePath();
                } else {
                    return null;
                }
            } else {
                return null;
            }
        } else {
            // 创建以Cache根目录为路径的File对象
            File fileByCache = new File(StorageUtil.getPathBycache());
            // 创建SD卡根目录下以当前应用包名为文件夹的文件对象,并验证是否存在当前目录
            File fileByCacheSon = new File(fileByCache, PackageUtil.getAppPackageName());
            // File fileByCacheSon=new File(fileByCache,"AA");
            if (fileByCacheSon.exists()) {
                String md5Url = EncryptUtil.md5(netUrlorPath);
                // 以包名为文件夹的对象存在的时候,通过将文件对象和图片的名称的拼接构建文件对象
                File imageFile = new File(fileByCacheSon, URLEncoder.encode(md5Url));
                if (imageFile.exists()) {
                    // 图片文件对象存在的时候获取当前的图片对象对应的路径
                    savedPath = imageFile.getAbsolutePath();
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }
        return savedPath;

    }

上方代码是根据图片url获取到图片在文件中的路径。

所以的缓存图片,会保存在本包名文件夹下,以url的md5值为名字的文件中,判断到有此文件的话,将文件路径返回。

    /**
     * 这里完成的操作是判断传递进来的路径是否包括Bitmap对象,如果存在将Bitmap对象返回 否则返回null
     *
     * @param saveTime
     *            图片的保存时间
     * @param netUrl
     *            网络图片的网络路径作为文件名称
     * @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("已经存储的图片的路径::" + 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("文件超时了!");
            return null;

        }
        /**
         * 这里的逻辑是当文件对象存在的时候将该文件对象获取出来,并生成Bitmap对象并返回
         */
        // Bitmap sdBitmap= BitmapFactory.decodeFile(imageSavePath);
        // 从SD卡中获取图片的时候直接进行图片的压缩处理防止OOM

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

    }

判断到文件中有我们需要的图片,会拿到文件路径。但是,我们有设定文件有效时间,超过该时间则视为超时,返回null,否则读取该文件。根据图片的路径和当前手机的默认屏幕分辨率进行图片压缩再返回。

文件中有该图片,那就将该图片移植内存中,以提高优先级,而且内存两级中都放入该图片。

网络获取

以上都没拿到图片的话,那只能从网络来获取啦!

对http还是https进行判断,分别对应使用HttpUrlConnection和HttpsUrlConnection。他们代码类似,就只贴其中一个了。

    public static InputStream getHttpIOByGet(String netUrl) throws IOException {

//        System.out.println("网络的链接:"+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,表示请求成功,就将输入流返回,否则返回null。

Bitmap bitmap= BitmapFactory.decodeStream(inputStream);

获取输入流后,使用上方代码获取Bitmap对象,原因大家懂的。

获取到图片后,再依次存入sd卡和内存中,因为是好是操作,就在子线程中进行了。

new Thread(){
    @Override
    public void run() {
        //3.1、从网络获取图片
        //3.2、将图片压缩后的保存到SD卡或机身内存中
        FileUtil.putBitmapToSD(netUrl, finalThreeCacheBitmap);
        //3.4、将图片保存到Map中
        CacheRAM.putBitmapToRAM(netUrl, finalThreeCacheBitmap);
    }
}.start();

图片压缩

这里主要想介绍下图片的压缩:因为图片加载很容易造成OOM,所以图片压缩处理显得尤为重要。

提供集中压缩方式:

  • 根据期望大小压缩
  • 根据期望尺寸压缩
  • 根据当前手机的默认屏幕分辨率进行图片的压缩

这里就不再贴代码了,可以去我的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、添权限

<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、请求

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);

由于本人水平有限,不免有不对或不足的地方,希望大家能够提出,我们共同进步。

更多精彩内容,请关注我的微信公众号——Android机动车

这里写图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,221评论 11 349
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,904评论 25 707
  • 思考:正态分布,用在服装行业,比如,店铺里服装的款式、价格、颜色等都属于正态分布。 还有,比如店铺员工的业绩排名、...
    杨雪雪阅读 415评论 0 1
  • 我们对自尊的过分强调无法解决社会问题和人自身的问题,反而会引起这些问题。 这句话让我突然想起小时候看过的一本书,叫...
    柳涛虹阅读 282评论 0 0
  • 微视频引流法 (微商加粉营销更多请看:2017微商吸粉运营教程) 1.营销内容化,内容即营销 具体来说就是将娱乐和...
    助梦微创阅读 209评论 0 0