这算是我正式写的第一篇博客了,写博客的主要目的还是为了提升自己吧,经常看优快云一些大神的博客,真的很佩服这些博主们,高质量高产的文章帮助了很多人,在我学习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大小,可以通过公示
另外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流就头痛躲避,现在都还回来了!
其他解决方案
下面引用他人博客的一些文章分析的很透彻,大家可以看一下(我自己没有验证下面的方法):
也有人说可以重新获取连接,我也没有验证,不过就算可以不是浪费流量吗?
感觉自己写的还是太罗嗦了,漫漫长路慢慢走吧!
下面有自己个自己的疑惑: 有没有必要从网上下载的图片的时候就采用这种方式压缩,因为下载的时候这些数据已经被下载到内存里了吧,还是说下载下来之后先存入缓存,之后从缓存再利用isSampleSize来加载。
接下来的准备
- 研究图片的缓存。
- 图片下载有关于线程的问题。
- 在listview等控件AdapterView中展示多图的优化。
- 理解ImageLoader本质,尝试自己从头到尾写一边。