Bitmap的高效加载

这算是我正式写的第一篇博客了,写博客的主要目的还是为了提升自己吧,经常看优快云一些大神的博客,真的很佩服这些博主们,高质量高产的文章帮助了很多人,在我学习android的路上,给予了很多的启发。

看过郭神的《第一行代码》,最近在研究任主席的《Android开发艺术探索》,前者是入门好书,后者是进阶书籍,我打算结合博客内容,书中内容及其自己的一些理解来组织自己的博客,通过写博客来让自己的印象更深刻,也希望在此过程中让自己可以更多的自助思考。

我选择从Bitmap这个切入点作为自己博客的开端。

为什么选择Bitmap

嫌啰嗦的朋友可以跳过这段。

上一份工作是开发有关汽车保养的APP,APP分为用户端和商户端,我主要负责商户端的开发,公司就我和高总两个人做android,说句题外话我俩乃最佳基友,平时交流很多也都没有保留,在这段时间帮助了我很多,表示感谢。

进入开发阶段的时候,高总对于内存优化已经有一定的认识,刚进入职场的我对这些进阶技术并不是太了解,高总为我演示了一遍优化前以及优化后内存的使用情况,当时为我演示的是有关于Bitmap的加载优化,我也是知其然不知其所以然,这次就来谈个究竟。

一开始在郭神的博客中看到了bitmap加载相关的内容,开始对与这块内容有所认识。

Bitmap的影响

作为一个android小菜鸟,之前在app中使用图片的时候,都是直接在imageview中src=”@drawable/xxxx”,或者使用imageview.setImageDrawable(drawable)来设置图片,那么问题来了,实际开发中我在商户版的首页中设置了六张图片,gc后内存暂用率高达百分之90多,高总对此表示鄙视反问我要是OOM咋办,what the fuck,我哪里知道啊,我之前开发都不知道要考虑内存的好吗,既然高总发话让我降低内存占用率,我只好遵命了。

现在app当中对图片的使用是很多的,如果我们一股脑的把图片全都加载进来,更何况很多的图片分辨率都很高,这样就有可能造成内存溢出了。

  • 系统会为单个应用程序施加一定的内存限制-例如16M,假如内存无限制那也不用考虑这考虑那了。
  • 系统默认图片是ARGB_8888类型,一个像素点占4个字节,一张分辨率为2048 * 1536的图片,就占2048 * 1535 * 4字节也就是12M的大小,试想一下加载10张就是120M了啊,假如你用100 * 100像素大小的ImageView来加载这张图,你说这何必呢,没有一点好处。

所以我们必须想办法解决这个问题,可以选择压缩bitma,这样bitmap的大小就会降低不少,降低了OOM的概率。

如何高效加载呢?

我们只需要获取图片的尺寸,根据图片的尺寸,以及我们需要的尺寸计算出一个压缩值,然后按照压缩值来对图片进行加载。

通常我们利用BitmapFactory提供的四种方法来加载图片。

  • decodeFile:从sd,文件中加载图片。
  • decodeResource :从drawable中加载图片。
  • decodeStream:从网络加载图片。
  • decodeByteArray:从ByteArray中加载图片。

四种方法在decode图片的 时候都需要设置一个decoding options,这个选项由BitmapFactory.Options类提供,这个很好理解,就是一个设置选项嘛,可以想像一下用烤箱,你得设定用什么火烤多长时间吧。这个选项当中有一个很重要的参数:inJustDecodeBounds参数(布尔型),当设置为ture的时候,在decode图片的时候,不会把图片加载到内存中,并且可以获得图片的原始宽高等信息,想象你有打开了透视眼,这是时候看美女就不用脱掉她的衣服,但是你关闭透视眼的话,就必须要先脱掉她的衣服了!(设置为false 图片就会被加载到内存中)。

获取到原始图片的尺寸之后就可以设置图片压缩的比例了,这个时候需要用到isSampleSize参数,可以理解为采样率,假设isSampleSize 设置为2,那么分辨率为2048 * 1535的图片,宽和高都会变成之前的1/2,占内存大小为(2048 * 1535 * 4 * 1/2 * 1/2=3M),只相当于缩放前的1/4大小,可以通过公示

1/(inSampleSize2)
来计算缩放比例, 我之前有考虑过一个问题,假如Imageview是100*100像素,但是图片是200 * 300像素,那是缩放是100 * 150像素,还是缩放到67 *100像素呢,后来看任主席书中有解释过这个问题,我们应该按照第一种模式缩放,如果按照长边缩放,67<100,我们压缩后的图片将会在ImageView中被拉伸,结果肯定不好看。

另外isSampleSize 的值一定是2的指数,如果不是,将向下取整为最接近的2的指数来代替,例如计算结果为5,那么系统将选择4来替代,不要问我为什么我母鸡(看了官方文档是这样建议的,发现官方总会说that is a good practice to do xxxxx)任主席书中指出这个结论并不是在所有android版本都成立,我在4.4版本测试发现是成立的,因为懒有没有在其他版本测试。大家没事可以测试测试。

综上所述,流程如下:

  1. 将BitmapFactory.Options类中的inJustDecodeBounds 参数设置为true(打开透视眼睛)。
  2. 取出图片的原始宽高信息(获取姑娘三围)。
  3. 根绝控件大小和取出的原始宽高信息来计算出合适的isSampleSize 值(选择最适合的姑娘三围的内衣)。
  4. 将BitmapFactory.Options类中的inJustDecodeBounds 参数设置为false(关闭透视眼睛),重新解析加载图片。

代码如下:

public static Bitmap decodeBitmapFromResource(Resources res, int resId,int targetWidth, int targetHeight) {
        // 将inJustDecodeBounds设置为true,用来获取资源图片的宽高,并且不将资源载入内存
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        System.out.println("之前的inSampleSize:" + options.inSampleSize);
        // 计算缩放值
        options.inSampleSize = calculateInSampleSize(options, targetWidth,targetHeight);
        // 重新加在图片 载入内存
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

计算缩放比例:

public static int calculateInSampleSize(BitmapFactory.Options options,int targetWidth, int targetHeight) {
        // 原始资源的宽高
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > targetHeight || width > targetWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
        //官方给出的while条件都是>,任主席给出的是>=,我自己举例算了一下假如按照>来处理的话,ImageView大小100 * 100,图片像素300*200的一半为150*100,不满足都大于100*100的条件,那么inSampleSize还是为1,就不符合实际情况了。
            while ((halfHeight / inSampleSize) > =targetHeight
                    && (halfWidth / inSampleSize) >= targetWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

最后只需要在代码中如下调用,就可以将压缩后的图片显示在ImageView上:

//BitmapGet是我自己写的工具类,将解析的方法都写在这个工具类中便于调用,100,100可以替代为你需要显示的大小,这里是像素,但是xml中width和height的值是dp为单位的,所以你还需要写个方法将dp转换成piexl。
bitmapIv.setImageBitmap(BitmapGet.decodeBitmapFromResource(
        this.getResources(), R.drawable.rawbitmap, 100, 100));

//将dp转换成piexl   
public static float dp2px(Context context, float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,context.getResources().getDisplayMetrics());}

亲测结果图

压缩之前的内存使用情况如图:

压缩之前的内存使用情况

压缩之后的内存使用情况如图:

压缩之后的内存使用情况

压缩之前的bitmap数据
压缩之前的大小

压缩之后的bitmap数据
压缩之后的大小

可以看到压缩之后BitmapFactory.OptionsinSampleSize 的值是8,也就是缩放比例为1/64,并且1966088/64=307200,证明压缩成功。那么问题来了,我本身放进去的图片是960 * 1280啊,但是Log输出的是2560*1920啊,我没有详细查资料,可能与Density 以及放置的drawable文件夹 有关,我做了测试放在不同的drawable文件夹下面得出的数值确实是不同的,这个疑问有待解决。

decodeStream方法使用出现问题

本来有关于Bitmap的高效加载就到此结束了,我奔着学习的精神(其实是无聊)接着去测试了一下从decodeStream方法,从网上解析一张图片采用相同的方法,来压缩图片。前方高能…….

网络解析Bitmap部分代码:

URL url = new URL(imageUrl);
con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(5 * 1000);
con.setReadTimeout(10 * 1000);
con.setDoInput(true);
con.setDoOutput(true);
input = con.getInputStream();
options.inJustDecodeBounds = true;
bitmap = BitmapFactory.decodeStream(input,null,options);
if (bitmap == null) {
    System.out.println("1---null");
}
options.inSampleSize = calculateInSampleSize(options,
        targetWidth,targetHeight);
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeStream(input, null, options);
if (bitmap == null) {
    System.out.println("2---null");
}
没错二次解析后的bitmap返回值为null!

本来我很开心即将发布自己第一篇博客,现实给了我呵呵一拳。我们来分析一下,我直接把inJustDecodeBounds 设置为false只进行一次decode,这样没有压缩的功能,但是可以返回bitmap,那么问题就出现在第二次decodeStream的时候,好吧百度,谷歌,stackoverflow,在写这篇博客的时候我已经投入谷歌怀抱了。

问题出现的原因:

果然就是第二次decodeStream出了问题,inputStream只能被读取一次,因为有一个指针指向流,每一次读取后指针都会指向下一次要读取的位置,指针的值不会重复,就像是一杯水,读取流的时候,就相当于把水倒出来,下一次在读取的时候,水自然就不存在了。

decodeStream二次读取解决方案

解决方案:直接上代码了:

URL url = new URL(imageUrl);
con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(5 * 1000);
con.setReadTimeout(10 * 1000);
con.setDoInput(true);
con.setDoOutput(true);
input = con.getInputStream();
options.inJustDecodeBounds = true;
byte[] data = inputStream2ByteArray(input);
bitmap = BitmapFactory.decodeByteArray(data, 0,      data.length,options);
if (bitmap == null) {
    System.out.println("1---nullll");
}
options.inSampleSize = calculateInSampleSize(options,
        targetWidth,targetHeight);
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeByteArray(data, 0,
        date.length,options);
if (bitmap == null) {
    System.out.println("2---nullll");
}

利用ByteArrayStream将流缓存到内存中,然后就可以反复从内存中获取了。当时学java的时候看见I/O流就头痛躲避,现在都还回来了!
其他解决方案
下面引用他人博客的一些文章分析的很透彻,大家可以看一下(我自己没有验证下面的方法):

感觉自己写的还是太罗嗦了,漫漫长路慢慢走吧!

下面有自己个自己的疑惑: 有没有必要从网上下载的图片的时候就采用这种方式压缩,因为下载的时候这些数据已经被下载到内存里了吧,还是说下载下来之后先存入缓存,之后从缓存再利用isSampleSize来加载。

接下来的准备

  1. 研究图片的缓存。
  2. 图片下载有关于线程的问题。
  3. 在listview等控件AdapterView中展示多图的优化。
  4. 理解ImageLoader本质,尝试自己从头到尾写一边。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值