Android 图片缓存

前言

本篇主要包含两个方面的内容:

  1. 图片的加载和优化图片的加载
  2. LruCache,DisLruCache的使用与图片的三级缓存

图片的加载和优化

我们在编写Android程序的时候经常要用到很多的图片,在大多数情况下,这些图片都会大于我们程序所需要的大小。我们编写的应用程序都是有一定的内存限制,程序占用了过高的内存就容易出现OOM(Out Of Memory)异常。如下代码可以看出每个应用程序最高可用内存:

    //查询应用程序可用的最大内存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        Log.e("MainActivity", "Max memory is " + maxMemory + "KB");
2019-06-02 09:09:55.156 4281-4281/com.hdq.study E/MainActivity: Max memory is 393216KB

1. 加载bitmap

Bitmap在Android中指的是一张图片,png格式、jpg等其他常见的图片格式。
BitmapFactory类提供了四个类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四个类方法最终在Android的底层实现的,对应着BitmapFactory类的几个native方法。

2. 优化加载bitmap

Bitmap优化加载的核心思想就是采用BitmapFactory.Options来加载所需尺寸的图片。

通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小;当inSampleSize大于1时,比如2,那么采样后的图片宽高均为原图大小的1/2,像素数为原图的1/4,其占有的内存大小也为原图的1/4。
采样率必须是大于1的整数,图片才会有缩小的效果,并且采样率同时作用于宽和高,缩放比例为1/(inSampleSize的2次方),比如inSampleSize为4,那么缩放比例就是1/16。官方文档指出,inSampleSize的取值为2的指数:1、2、4、8、16等等。
使用步骤:

  1. 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;
  2. 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数;
  3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize;
  4. 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
 //解码图片的配置选项
        BitmapFactory.Options options = new BitmapFactory.Options();
        //设置options里面的参数,为true,不去真实地解析Bitmap,而是查询Bitmap的宽高信息(禁为bitmap分配内存)
        options.inJustDecodeBounds = true;
        Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/Download/pic.jpg", options);
        Log.e("TAG", "bitmap==" + bitmap);//bitmap=null

        //获取图片的宽高
        int height = options.outHeight;
        int width = options.outWidth;
        Log.e("TAG", "图片的宽度,width==" + width);
        Log.e("TAG", "图片的高度,width==" + height);

        //获取手机屏幕的宽高,拿到窗体管理者
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        //getDefaultDisplay获取屏幕分辨率
        int screenWidth = wm.getDefaultDisplay().getWidth();
        int screenHeight = wm.getDefaultDisplay().getHeight();
        Log.e("TAG", "屏幕的宽度,width==" + screenWidth);
        Log.e("TAG", "屏幕的高度,width==" + screenHeight);

        //计算图片和屏幕宽高的比例
        int dx = width / screenWidth;
        int dy = height / screenHeight;
       //缩放比例
        int scale = 1;
        //比如图片:960*480  屏幕:480*320  dx=2  dy=1.5,取dx=2,它的宽高都在屏幕里面了
        //dx<1说明图片还没有屏幕高,就不需要缩放了
        if (dx > dy && dy > 1) {
            scale = dx;
        }
        if (dy > dx && dx > 1) {
            scale = dy;
        }

        Log.e("TAG", "scale==" + scale);

        //以缩放的方式把图片加载到手机内存
        options.inSampleSize = scale;
        //真实地解析bitmap
        options.inJustDecodeBounds = false;
        Bitmap bitmap2 = BitmapFactory.decodeFile("/sdcard/Download/pic.jpg", options);
        imageView.setImageBitmap(bitmap2);
/**
 *
 * @param res
 * @param resId
 * @param reqWidth 期望图片宽(像素)
 * @param reqHeight 期望图片高(像素)
 * @return
 */
public  static Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
    final BitmapFactory.Options options= new BitmapFactory.Options();
    options.inJustDecodeBounds=true;
    BitmapFactory.decodeResource(res,resId,options);

    //计算采样率
    options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);

    options.inJustDecodeBounds=false;
    return BitmapFactory.decodeResource(res,resId,options);
}

/**
 * 获取采样率
 * @param options
 * @param reqWidth
 * @param reqHeight
 * @return
 */
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    //获取图片的宽和高
    int width = options.outWidth;
    int height=options.outHeight;
    int inSampleSize=1;

    if (height>reqHeight ||  width>reqWidth){
        final int halfHeight=height/2;
        final int halfWidth=width/2;

        //计算最大的采样率,采样率为2的指数
        while ((halfHeight/inSampleSize)>=reqHeight && (halfHeight/inSampleSize)>=reqWidth){
            inSampleSize *=2;
        }
    }

    return inSampleSize;
}
//ImageView所期望的图片大小为150*150像素,加载显示图片
imageView.setImageBitmap(decodeSampleBitmapFromResource(getResources(),R.drawable.image,150,150));

图片的三级缓存

什么是三级缓存?

  • 内存缓存,优先加载,速度最快

  • 本地缓存,次优先加载,速度快

  • 网络缓存,最后加载,速度慢,浪费流量
    强引用、软引用和弱引用的区别

  • 强引用
    直接的对象引用;内存不足时,JVM也不会被回收。(定义的成员变量都是强引用)

  • 软引用
    SoftReference<T> 当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;

  • 弱引用
    WeakReference<T> 当一个对象只有弱引用存在时,此对象会随时被gc回收。

  • 虚引用
    PhantomReference<T> 代码被调用的时候,就被清理了。
    软引用示例代码(其他几种用法类似):

private SoftReference<ImageView> mSoftReference;

/**
 * 给imageView加载url对应的图片
 * @param iv
 * @param url
 */
public void display(ImageView iv,String url){
    mSoftReference=new SoftReference<ImageView>(iv);
    mSoftReference.clear();//这里是清除里面的iv图片资源
    //取引用--为null
    ImageView imageView = mSoftReference.get();
}

Android-->早期是davike虚拟机,Android Runtime

  1. 3.0之前,垃圾回收机制和JVM是相同的。(可以用这套软引用存储图片)
  2. 3.0之后,davike虚拟机做了升级,只要GC(回收机制)运行,SoftReference和WeakReference一律回收。(在Android中就没用了)
    Android3.0之前软引用的写法(代码如下):
private static Map<String, SoftReference<Bitmap>> mCaches = new LinkedHashMap<String,SoftReference<Bitmap>>();

/**
 * 给imageView加载url对应的图片
 *
 * @param iv
 * @param url
 */
public void display(ImageView iv, String url) {
    SoftReference<Bitmap> reference = mCaches.get(url);
    if (reference==null){
        //内存中没有--》本地去取
    }else {
        Bitmap bitmap = reference.get();
        if (bitmap==null){
            //gc回收了---》本地去取
        }else {
            //内存中有,就显示
        }
    }
}

解决方案:是用了LruCache。

LruCache是线程安全的,定义如下:

public class LruCache<K,V>{
  private final LinkedHashMap<K,V> map;
  ...
}

1. LruCache

内存缓存技术对那些大量占用应用程序内存的图片提供了快速访问的方法。其中最核心的类是LruCache(此类在android-support-v4的包中提供)。它的主要算法原理是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,它提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。

LruCache缓存大小设置

对于分配给LruCache的缓存大小,可以直接指定固定的数值,但是更好的做法应该是通过获取最大内存(int)Runtime.getRuntime.maxMemory,然后通过返回的最大内存/int n的大小动态分配给LruCache。

LruCache的存储和读取

LruCache是以为键值对形式存储数据,所以它的读写方法都和HashMap一样,都可以通过key操作。
存储
LruCache.put(Key,Values)
读取
LruCache.get(Key)
构造一个工具类,用来存储图片到缓存和从缓存中读取图片

public class CustomLruCache {
    private LruCache<String, Bitmap> stringBitmapLruCache;
    int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取最大内存
    int cacheSize = maxMemory / 16;//大小为最大内存的1/16
    private static CustomLruCache customLruCache;

    /**
     * 私有化构造方法
     */
    private CustomLruCache() {
        stringBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
        };
    }

    /**
     * 单例模式获取实例,保证只有一个CustomLruCache对象,同时保证只有一个CustomLruCache.stringBitmapLruCache
     *
     * @return
     */
    public static CustomLruCache getInstance() {
        if (customLruCache == null) {
            customLruCache = new CustomLruCache();
        }
        return customLruCache;
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) != bitmap)//如果缓存中不存在bitmap,就存入缓存
            stringBitmapLruCache.put(key, bitmap);
    }

    public Bitmap getBitmapFromMemoryCache(String key) {
        return stringBitmapLruCache.get(key);
    }
}
public class CustomLruCache {
    private LruCache<String, Bitmap> stringBitmapLruCache;
    int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取最大内存
    int cacheSize = maxMemory / 16;//大小为最大内存的1/16
    private static CustomLruCache customLruCache;

    /**
     * 私有化构造方法
     */
    private CustomLruCache() {
        stringBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
        };
    }

    /**
     * 单例模式获取实例,保证只有一个CustomLruCache对象,同时保证只有一个CustomLruCache.stringBitmapLruCache
     *
     * @return
     */
    public static CustomLruCache getInstance() {
        if (customLruCache == null) {
            customLruCache = new CustomLruCache();
        }
        return customLruCache;
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) != bitmap)//如果缓存中不存在bitmap,就存入缓存
            stringBitmapLruCache.put(key, bitmap);
    }

    public Bitmap getBitmapFromMemoryCache(String key) {
        return stringBitmapLruCache.get(key);
    }
}

加载网络图片

 AsyncTask<String ,Void,Bitmap> bitmapAsyncTask = new AsyncTask<String, Void, Bitmap>() {
        @Override
        protected Bitmap doInBackground(String... params) {
            Bitmap bitmap = null;
            try {
                CustomLruCache customLruCache = CustomLruCache.getInstance();
                bitmap = customLruCache.getBitmapFromMemoryCache(params[0]);
                //先从缓存中读取图片,如果缓存中不存在,再请求网络,从网络读取图片添加至LruCache中
                //启动app后第一次bitmap为null,会先从网络中读取添加至LruCache,如果app没销毁,再执行读取图片操作时
                //就会优先从缓存中读取
                if (bitmap == null) {
                    //从网络中读取图片数据
                    URL url = new URL(params[0]);
                    bitmap = BitmapFactory.decodeStream(url.openStream());
                    //添加图片数据至LruCache
                    customLruCache.addBitmapToMemoryCache(params[0], bitmap);
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            imageView.setImageBitmap(bitmap);
        }
    };
//加载图片
 bitmapAsyncTask.execute(imageURL);

2. 文件缓存-第三方类DiskLruCache

利用DiskLruCache从网络上获取到之后都会存入到本地缓存中,因此即使手机在没有网络的情况下依然能够加载显示图片数据。DiskLruCache存储的位置没有限制,但是一般选择存储在context.ExternolStorageCacheDir(),即这个手机的外部存储这个app的私有区域,即/sdcard/Android/data/应用包名/cache,因为是存储在外部存储私有区域,当app被卸载时,这部分的内容会被一起清除。

DiskLruCache地址:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
或者可以在Jake大神的Github上找到:
https://github.com/JakeWharton/DiskLruCache

使用DiskLruCache

实例化DiskLruCache是通过 DiskLruCache.open(File directory, int appVersion, int valueCount, long maxSize),四个参数分别:为directory缓存的路径;appVersion 应用版本;alueCount 指定同一个key可以对应多少个缓存文件,一般指定为1;maxSize 指定可以缓存多少字节的数据。
工具类

  public class DiskLruCacheHelper {
        DiskLruCache mDiskLruCache = null;
        static DiskLruCacheHelper diskLruCacheHelper;

        private DiskLruCacheHelper(Context context) {
            try {
                File cacheDir = getDiskCacheDir(context, "bitmap");
                //如果文件不存在,则创建
                if (!cacheDir.exists()) {
                    cacheDir.mkdirs();
                }
                mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public static DiskLruCacheHelper getInstance(Context context) {
            if (diskLruCacheHelper == null)
                diskLruCacheHelper = new DiskLruCacheHelper(context);
            return diskLruCacheHelper;
        }

        public File getDiskCacheDir(Context context, String uniqueName) {
            String cachePath;
            if (isExternalStorageWritable()) {
                cachePath = context.getExternalCacheDir().getPath();//如果挂载了sdcard,获取外部存储私有区域路径
            } else {
                cachePath = context.getCacheDir().getPath();//如果没有挂载sdcard,则获取内部存储缓存区域
            }
            return new File(cachePath + File.separator + uniqueName);
        }

        /**
         * 检查外部存储是否可用
         *
         * @return
         */
        private boolean isExternalStorageWritable() {
            String state = Environment.getExternalStorageState();
            if (state.equals(Environment.MEDIA_MOUNTED)) {
                return true;//挂载了sdcard,返回真
            } else {
                return false;//否则返回假
            }
        }

        /**
         * 获取应用版本号
         * 当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为
         * 当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。
         *
         * @param context
         * @return
         */
        public int getAppVersion(Context context) {
            try {
                PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                return info.versionCode;
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
            return 1;
        }

        /**
         * 写入图片数据到文件缓存
         *
         * @param imageUrl
         * @param bitmap
         */
        public void writeToCache(String imageUrl, Bitmap bitmap) {
            try {
                String key = hashKeyForDisk(imageUrl);
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                    if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
                        editor.commit();
                    } else {
                        editor.abort();
                    }

                }
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


        /**
         * 从缓存读取数据
         *
         * @param imageUrl
         * @return
         */

        public Bitmap readFromCache(String imageUrl) {
            Bitmap bitmap = null;
            try {
                String key = hashKeyForDisk(imageUrl);
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                if (snapShot != null) {//如果文件存在,读取数据转换为Bitmap对象
                    InputStream is = snapShot.getInputStream(0);
                    bitmap = BitmapFactory.decodeStream(is);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        /**
         * 将文件名转换成"MD5"编码
         *
         * @param key
         * @return
         */
        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();
        }

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