还原堆内存溢出的错误
首先来还原一下堆内存溢出的错误。首先在sd卡上放一张照片,分辨率为(3776 x 2520),大小为3.88mb,是我自己用相机拍的一张照片。应用的布局很简单,一个button一个imageview,然后按照常规的方式,使用bitmapfactory加载一张照片并使用一个imageview展示。
代码如下:
btn_loadimage.setonclicklistener(new view.onclicklistener() {
@override
public void onclick(view v) {
bitmap bitmap=bitmapfactory.decodefile("/sdcard/a.jpg");
iv_bigimage.setimagebitmap(bitmap);
}
}
当点击按钮后,程序会报错,查看日志为:
先来分析一下这个错误,首先dalvikvm(android虚拟机)发现需要的内存38mb大于应用的堆内存24mb,这个时候尝试使用软加载的方式加载数据,我们知道当内存不足的时候dalvikvm会自动进行gc(garbage collection),大概清理了55k的空间出来,耗时203毫秒,但是内存还是不够,所以最后发生堆内存溢出的错误。
分析堆内存溢出
android系统主要用于低能耗的移动设备,所以对内存的管理有很多限制,一个应用程序,android系统缺省会为其分配最大16mb(某些机型是24mb)的空间作为堆内存空间,我这里使用的模拟器调试的,这个模拟器被设定为24mb,可以在android virtual device manager中查看到。
而这里的图片明明只有3.88mb,远远小于android为应用分配的堆内存,而加载到内存中,为什么需要消耗大约38mb的内存呢?
我们都知道,图片是由一个一个点分布组成的(分辨率),通常加载这类数据都会在内存中创建一个二维数组,数组中的每一项代表一个点,而这个图片的分辨率是3776 * 2520,每一点又是由argb色组成,每个色素占4个byte,所以这张图片加载到内存中需要消耗的内存为:
3776 * 2520 * 4byte = 38062080byte
大约需要38mb的内存才能正确加载这张图片,这就是上面错误描述需要38mb的内存空间,大小略有出入,因为图片还有一些exif信息需要存储,会比仅靠分辨率计算要大一些。
如何加载大分辨率图片
有时候我们确实会需要加载一些大分辨率的图片,但是对于移动设备而言,哪怕加载能成功那么大的内存也是一种浪费(屏幕分辨率限制),所以就需要想办法把图片按照一定比率压缩,使分辨率降低,以至于又不需要耗费很大的堆内存空间,又可以最大的利用设备屏幕的分辨率来显示图片。这里就用到一个bitmapfactory.options对象,下面来介绍它。
bitmapfactory.options为bitmapfactory的一个内部类,它主要用于设定与存储bitmapfactory加载图片的一些信息。下面是options中需要用到的属性:
injustdecodebounds:如果设置为true,将不把图片的像素数组加载到内存中,仅加载一些额外的数据到options中。
outheight:图片的高度。
outwidth:图片的宽度。
insamplesize:如果设置,图片将依据此采样率进行加载,不能设置为小于1的数。例如设置为4,分辨率宽和高将为原来的1/4,这个时候整体所占内存将是原来的1/16。
示例demo
下面通过一个简单的demo来演示上面提到的内容,代码中注释比较清晰,这里就不再累述了。
package cn.bgxt.loadbigimg;
import android.os.bundle;
import android.os.environment;
import android.app.activity;
import android.graphics.bitmap;
import android.graphics.bitmapfactory;
import android.graphics.bitmapfactory.options;
import android.view.menu;
import android.view.view;
import android.view.windowmanager;
import android.widget.button;
import android.widget.imageview;
public class mainactivity extends activity {
private button btn_loadimage;
private imageview iv_bigimage;
@override
protected void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
btn_loadimage = (button) findviewbyid(r.id.btn_loadimage);
iv_bigimage = (imageview) findviewbyid(r.id.iv_bigimage);
btn_loadimage.setonclicklistener(new view.onclicklistener() {
@override
public void onclick(view v) {
// bitmap bitmap=bitmapfactory.decodefile("/sdcard/a.jpg");
// iv_bigimage.setimagebitmap(bitmap);
bitmapfactory.options opts = new options();
// 不读取像素数组到内存中,仅读取图片的信息
opts.injustdecodebounds = true;
bitmapfactory.decodefile("/sdcard/a.jpg", opts);
// 从options中获取图片的分辨率
int imageheight = opts.outheight;
int imagewidth = opts.outwidth;
// 获取android屏幕的服务
windowmanager wm = (windowmanager) getsystemservice(window_service);
// 获取屏幕的分辨率,getheight()、getwidth已经被废弃掉了
// 应该使用getsize(),但是这里为了向下兼容所以依然使用它们
int windowheight = wm.getdefaultdisplay().getheight();
int windowwidth = wm.getdefaultdisplay().getwidth();
// 计算采样率
int scalex = imagewidth / windowwidth;
int scaley = imageheight / windowheight;
int scale = 1;
// 采样率依照最大的方向为准
if (scalex > scaley && scaley >= 1) {
scale = scalex;
}
if (scalex < scaley && scalex >= 1) {
scale = scaley;
}
// false表示读取图片像素数组到内存中,依照设定的采样率
opts.injustdecodebounds = false;
// 采样率
opts.insamplesize = scale;
bitmap bitmap = bitmapfactory.decodefile("/sdcard/a.jpg", opts);
iv_bigimage.setimagebitmap(bitmap);
}
});
}
}
效果展示:
总结
这里讲解了如何加载一个大分辨率的图片到内存中并使用它。不过一般好一点的图片处理软件,都会有图片放大功能,如果仅做此处理,单纯的把处理后的图片放大,会影响显示效果,图片还原度不高。一般会重新获取放大区域的图片的分辨率像素数组,然后重新处理加载到内存中进行显示。