您的位置:首页 > 移动开发 > Android开发

Bitmap的高效加载

2015-10-25 21:38 447 查看
这算是我正式写的第一篇博客了,写博客的主要目的还是为了提升自己吧,经常看CSDN一些大神的博客,真的很佩服这些博主们,高质量高产的文章帮助了很多人,在我学习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版本测试发现是成立的,因为懒有没有在其他版本测试。大家没事可以测试测试。

综上所述,流程如下:

将BitmapFactory.Options类中的
inJustDecodeBounds
参数设置为true(打开透视眼睛)。

取出图片的原始宽高信息(获取姑娘三围)。

根绝控件大小和取出的原始宽高信息来计算出合适的
isSampleSize
值(选择最适合的姑娘三围的内衣)。

将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.Options
inSampleSize
的值是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流就头痛躲避,现在都还回来了!

其他解决方案

下面引用他人博客的一些文章分析的很透彻,大家可以看一下(我自己没有验证下面的方法):

InputStream为什么不能被重复读取?

通过mark和reset方法重复利用InputStream

也有人说可以重新获取连接,我也没有验证,不过就算可以不是浪费流量吗?

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

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

接下来的准备

研究图片的缓存。

图片下载有关于线程的问题。

在listview等控件AdapterView中展示多图的优化。

理解ImageLoader本质,尝试自己从头到尾写一边。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息