Android Bitmap的加载和Cache

Bitmap的高效加载

如何加载一个图片?首先BitmapFactory类提供了四种方法: decodeFile(), decodeResource(), decodeStream(), decodeByteArray(). 分别用于从文件系统, 资源文件, 输入流以及字节数组加载出一个Bitmap对象. 其中decodeFile和decodeResource又间接调用了decodeStream()方法, 这四类方法最终是在Android的底层实现的, 对应着BitmapFactory类的几个native方法.
高效加载的Bitmap的核心思想:采用BitmapFactory.Options来加载所需尺寸的图片. 比如说一个ImageView控件的大小为300300. 而图片的大小为800800. 这个时候如果直接加载那么就比较浪费资源, 需要更多的内存空间来加载图片, 这不是很必要的. 这里我们就可以先把图片按一定的采样率来缩小图片在进行加载. 不仅降低了内存占用,还在一定程度上避免了OOM异常. 也提高了加载bitmap时的性能.
而通过Options参数来缩放图片: 主要是用到了inSampleSize参数, 即采样率。

如果是inSampleSize=1那么和原图大小一样,
如果是inSampleSize=2那么宽高都为原图1/2, 而像素为原图的1/4, 占用的内存大小也为原图的1/4
如果是inSampleSize=3那么宽高都为原图1/3, 而像素为原图的1/9, 占用的内存大小也为原图的1/9
以此类推…..

要知道Android中加载图片具体在内存中的占有的大小是根据图片的像素决定的, 而与图片的实际占用空间大小没有关系.而且如果要加载mipmap下的图片, 还会根据不同的分辨率下的文件夹进行不同的放大缩小.
列举现在有一张图片像素为:10241024, 如果采用ARGB8888(四个颜色通道每个占有一个字节,相当于1点像素占用4个字节的空间)的格式来存储.(这里不考虑不同的资源文件下情况分析) 那么图片的占有大小就是102410244那现在这张图片在内存中占用4MB.
如果针对刚才的图片进行inSampleSize=2, 那么最后占用内存大小为512512*4, 也就是1MB
采样率的数值必须是大于1的整数是才会有缩放效果, 并且采样率同时作用于宽/高, 这将导致缩放后的图片以这个采样率的2次方递减, 即内存占用缩放大小为1/(inSampleSize的二次方). 如果小于1那么相当于=1的时候. 在官方文档中指出, inSampleSize的取值应该总是为2的指数, 比如1,2,4,8,16,32…如果外界传递inSampleSize不为2的指数, 那么系统会向下取整并选择一个最接近的2的指数来代替. 比如如果inSampleSize=3,那么系统会选择2来代替. 但是这条规则并不作用于所有的android版本, 所以可以当成一个开发建议
整理一下开发中代码流程:

将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片。
从BitmapFactory.Options取出图片的原始宽高信息, 他们对应于outWidth和outHeight参数。
根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
将BitmapFactory.Options的inJustDecodeBounds参数设为false, 然后重新加载。

inJustDecodeBounds这个参数的作用就是在加载图片的时候是否只是加载图片宽高信息而不把图片全部加载到内存. 所以这个操作是个轻量级的.
通过这些步骤就可以整理出以下的工具加载图片类调用decodeFixedSizeForResource()即可.

public class MyBitmapLoadUtil {
    /**
     * 对一个Resources的资源文件进行指定长宽来加载进内存, 并把这个bitmap对象返回
     *
     * @param res   资源文件对象
     * @param resId 要操作的图片id
     * @param reqWidth 最终想要得到bitmap的宽度
     * @param reqHeight 最终想要得到bitmap的高度
     * @return 返回采样之后的bitmap对象
     */
    public static Bitmap decodeFixedSizeForResource(Resources res, int resId, int reqWidth, int reqHeight){
        // 首先先指定加载的模式 为只是获取资源文件的大小
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        //Calculate Size  计算要设置的采样率 并把值设置到option上
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 关闭只加载属性模式, 并重新加载的时候传入自定义的options对象
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
    /**
     *  一个计算工具类的方法, 传入图片的属性对象和 想要实现的目标大小. 通过计算得到采样值
     */
    private static int calculateInSampleSize(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 (reqWidth < height || reqHeight < width){
            int halfWidth = width/2;
            int halfHeight = height/2;
            // 判断原始长宽的一半是否比目标大小小, 如果小那么增大采样率2倍, 直到出现修改后原始值会比目标值大的时候
            while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

android中的缓存策略

1.lrucache

lrucache是api level 12提供的一个泛型类,它内部采用一个linkedhashmap以强引用的方式存储外界的缓存对象,提供了get和put方法来完成缓存的获取和添加操作,当缓存满了,lrucache会remove掉较早使用的缓存对象,然后再添加新的对象。

过去实现内存缓存的常用做法是使用softreference或者使用weakreference,但是并不推荐这种做法,从api level 9以后,gc强制回收掉soft、weak引用,从而导致这些缓存并没有任何效率的提升。

lrucache的实现原理:
根据lru的算法思想,我们需要一种数据结构来快速定位哪个对象是最近访问的,哪个对象是最长时间未访问的,lrucache选择的是linkedhashmap这个数据结构,它是一个双向循环链表。来瞅一眼linkedhashmap的构造函数:

/** 初始化linkedhashmap

     * 第一个参数:initialcapacity,初始大小
     * 第二个参数:loadfactor,负载因子=0.75f

     * 第三个参数:accessorder=true,基于访问顺序;accessorder=false,基于插入顺序
public linkedhashmap(int initialcapacity, float loadfactor, boolean accessorder) { super(initialcapacity, loadfactor); init(); this.accessorder = accessorder; }

所以在lrucache中应该选择accessorder = true,当我们调用put、get方法时,linkedhashmap内部会将这个item移动到链表的尾部,即在链表尾部是最近刚刚使用的item,链表头部就是最近最少使用的item。当缓存空间不足时,可以remove头部结点释放缓存空间。

下面举例lrucache的典型使用姿势:

int maxmemory = (int) (runtime.getruntime().maxmemory() / 1024);

int cachesize = maxmemory / 8;

mmemorycache = new lrucache(cachesize) {

    @override

    protected int sizeof(string key, bitmap bitmap) {

        return bitmap.getrowbytes() * bitmap.getheight() / 1024;

    }

};

 
// 向 lrucache 中添加一个缓存对象 private void addbitmaptomemorycache(string key, bitmap bitmap) { if (getbitmapfrommemcache(key) == null) { mmemorycache.put(key, bitmap); } } //获取一个缓存对象 private bitmap getbitmapfrommemcache(string key) { return mmemorycache.get(key); }

上述示例代码中,总容量的大小是当前进程的可用内存的八分之一(官方推荐是八分之一哈,你们可以自己视情况定),sizeof()方法计算了bitmap的大小,sizeof方法默认返回的是你缓存item数目,源码中直接return 1(这里的源码比较简单,可以自己看看~)。

如果你需要cache中某个值释放,可以重写entryremoved()方法,这个方法会在元素被put或者remove的时候调用,源码默认是空实现。重写entryremoved()方法还可以实现二级内存缓存,进一步提高性能。思路如下:重写entryremoved(),把删除掉的item,再次存入另一个linkedhashmap中。这个数据结构当做二级缓存,每次获得图片的时候,按照一级缓存 、二级缓存、sdcard、网络的顺序查找,找到就停止。

2.disklrucache

当我们需要存大量图片的时候,我们指定的缓存空间可能很快就用完了,lrucache会频繁地进行trimtosize操作将最近最少使用的数据remove掉,但是hold不住过会又要用这个数据,又从网络download一遍,为此有了disklrucache,它可以保存这些已经下载过的图片。当然,从磁盘读取图片的时候要比内存慢得多,并且应该在非ui线程中载入磁盘图片。disklrucache顾名思义,实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存效果。

ps: 如果缓存的图片经常被使用,可以考虑使用contentprovider。

disklrucache的实现原理:
lrucache采用的是linkedhashmap这种数据结构来保存缓存中的对象,那么对于disklrucache呢?由于数据是缓存在本地文件中,相当于是持久保存的一个文件,即使app kill掉,这些文件还在滴。so ,,,,, 到底是啥?disklrucache也是采用linekedhashmap这种数据结构,但是不够,需要加持buff

日志文件。日志文件可以看做是一块“内存”,map中的value只保存文件的简要信息,对缓存文件的所有操作都会记录在日志文件中。

disklrucache的初始化:

下面是disklrucache的创建过程:

private static final long disk_cache_size = 1024 * 1024 * 50; //50mb

file diskcachedir = getdiskcachedir(mcontext, "bitmap");

if (!diskcachedir.exists()) {

    diskcachedir.mkdirs();

}

if (getusablespace(diskcachedir) > disk_cache_size) {

    try {

        mdisklrucache = disklrucache.open(diskcachedir, 1, 1,

                disk_cache_size);

    } catch (ioexception e) {

        e.printstacktrace();

    }

}

瞅了一眼,可以知道重点在open()函数,其中第一个参数表示文件的存储路径,缓存路径可以是sd卡上的缓存目录,具体是指/sdcard/android/data/package_name/cache,package_name表示当前应用的包名,当应用被卸载后, 此目录会一并删除掉。如果你希望应用卸载后,这些缓存文件不被删除,可以指定sd卡上其他目录。第二个参数表示应用的版本号,一般设为1即可。第三个参数表示单个结点所对应数据的个数,一般设为1。第四个参数表示缓存的总大小,比如50mb,当缓存大小超过这个设定值后,disklrucache会清除一些缓存保证总大小不会超过设定值

disklrucache的数据缓存与获取缓存:
数据缓存操作是借助disklrucache.editor类完成的,editor表示一个缓存对象的编辑对象。

new thread(new runnable() {  

    @override  

    public void run() {  

        try {  

            string imageurl = "http://d.url.cn/myapp/qq_desk/friendprofile_def_cover_001.png";  

            string key = hashkeyfordisk(imageurl);  //md5对url进行加密,这个主要是为了获得统一的16位字符

            disklrucache.editor editor = mdisklrucache.edit(key);  //拿到editor,往journal日志中写入dirty记录

            if (editor != null) {  

                outputstream outputstream = editor.newoutputstream(0);  

                if (downloadurltostream(imageurl, outputstream)) {  //downloadurltostream方法为下载图片的方法,并且将输出流放到outputstream

                    editor.commit();  //完成后记得commit(),成功后,再往journal日志中写入clean记录

                } else {  

                    editor.abort();  //失败后,要remove缓存文件,往journal文件中写入remove记录

                }  

            }  

            mdisklrucache.flush();  //将缓存操作同步到journal日志文件,不一定要在这里就调用

        } catch (ioexception e) {  

            e.printstacktrace();  

        }  

    }  

}).start();

上述示例代码中,每次调用edit()方法时,会返回一个新的editor对象,通过它可以得到一个文件输出流;调用commit()方法将图片写入到文件系统中,如果失败,通过abort()方法进行回退。

而获取缓存和缓存的添加过程类似,将url转换为key,然后通过disklrucache的get方法得到一个snapshot对象,接着通过snapshot对象得到缓存的文件输入流。有了文件输入流,bitmap就get到了。

 bitmap bitmap = null;

    string key = hashkeyformurl(url);

    disklrucache.snapshot snapshot = mdisklrucache.get(key);

    if (snapshot != null) {

        fileinputstream fileinputstream = (fileinputstream)snapshot.getinputstream(disk_cache_index);

        filedescriptor filedescriptor = fileinputstream.getfd();

        bitmap = mimageresizer.decodesampledbitmapfromfiledescriptor(filedescriptor,

                reqwidth, reqheight);

        ......

    }
disklrucache优化思考:
disklrucache是基于日志文件的,每次对缓存文件操作都需要进行日志记录,我们可以不用日志文件,在第一次构造disklrucache时,直接从程序访问缓存目录下的文件,并将每个缓存文件的访问时间作为初始值记录在map中的value值,每次访问或保存缓存都更新相应key对应的缓存文件的访问时间,避免了频繁地io操作。

#####3. 缓存策略对比与总结
lrucache是android中已经封装好的类,disklrucache需要导入相应的包才可以使用。
可以在ui线程中直接使用lrucache;使用disklrucache时,由于缓存或者获取都需要对本地文件进行操作,因此要在子线程中实现。
lrucache主要用于内存缓存,当app kill掉的时候,缓存也跟着没了;而disklrucache主要用于存储设备缓存,app kill掉的时候,缓存还在
lrucache的内部实现是linkedhashmap,对于元素的添加或获取用put、get方法即可。而disklrucache是通过文件流的形式进行缓存,所以对于元素的添加或获取通过输入输出流来实现。

你可能感兴趣的:(Android Bitmap的加载和Cache)