Bitmap的高效加载
2015-10-25 21:38
447 查看
这算是我正式写的第一篇博客了,写博客的主要目的还是为了提升自己吧,经常看CSDN一些大神的博客,真的很佩服这些博主们,高质量高产的文章帮助了很多人,在我学习android的路上,给予了很多的启发。
看过郭神的《第一行代码》,最近在研究任主席的《Android开发艺术探索》,前者是入门好书,后者是进阶书籍,我打算结合博客内容,书中内容及其自己的一些理解来组织自己的博客,通过写博客来让自己的印象更深刻,也希望在此过程中让自己可以更多的自助思考。
我选择从Bitmap这个切入点作为自己博客的开端。
上一份工作是开发有关汽车保养的APP,APP分为用户端和商户端,我主要负责商户端的开发,公司就我和高总两个人做android,说句题外话我俩乃最佳基友,平时交流很多也都没有保留,在这段时间帮助了我很多,表示感谢。
进入开发阶段的时候,高总对于内存优化已经有一定的认识,刚进入职场的我对这些进阶技术并不是太了解,高总为我演示了一遍优化前以及优化后内存的使用情况,当时为我演示的是有关于Bitmap的加载优化,我也是知其然不知其所以然,这次就来谈个究竟。
一开始在郭神的博客中看到了bitmap加载相关的内容,开始对与这块内容有所认识。
现在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类提供,这个很好理解,就是一个设置选项嘛,可以想像一下用烤箱,你得设定用什么火烤多长时间吧。这个选项当中有一个很重要的参数:
获取到原始图片的尺寸之后就可以设置图片压缩的比例了,这个时候需要用到
另外
综上所述,流程如下:
将BitmapFactory.Options类中的
取出图片的原始宽高信息(获取姑娘三围)。
根绝控件大小和取出的原始宽高信息来计算出合适的
将BitmapFactory.Options类中的
代码如下:
计算缩放比例:
最后只需要在代码中如下调用,就可以将压缩后的图片显示在ImageView上:
压缩之后的内存使用情况如图:
压缩之前的bitmap数据
压缩之后的bitmap数据
可以看到压缩之后
网络解析Bitmap部分代码:
利用ByteArrayStream将流缓存到内存中,然后就可以反复从内存中获取了。当时学java的时候看见I/O流就头痛躲避,现在都还回来了!
其他解决方案
下面引用他人博客的一些文章分析的很透彻,大家可以看一下(我自己没有验证下面的方法):
InputStream为什么不能被重复读取?
通过mark和reset方法重复利用InputStream
也有人说可以重新获取连接,我也没有验证,不过就算可以不是浪费流量吗?
感觉自己写的还是太罗嗦了,漫漫长路慢慢走吧!
下面有自己个自己的疑惑: 有没有必要从网上下载的图片的时候就采用这种方式压缩,因为下载的时候这些数据已经被下载到内存里了吧,还是说下载下来之后先存入缓存,之后从缓存再利用isSampleSize来加载。
图片下载有关于线程的问题。
在listview等控件AdapterView中展示多图的优化。
理解ImageLoader本质,尝试自己从头到尾写一边。
看过郭神的《第一行代码》,最近在研究任主席的《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本质,尝试自己从头到尾写一边。
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories