阅读 2858

Android如何加载大图,防止OOM

Android加载大图,防止OOM

本文是根据Android开发文档写的,其中多次提到了堆内存,不太了解的同学可以先预习下JVM的内存模型,再来食用本文。

OOM与SOF

OutOfMemory(内存溢出),当Java虚拟机由于内存不足而无法分配对象,并且垃圾回收器无法再提供更多内存时,抛出异常OutOfMemoryError。

StackOverFlow(栈溢出),当应用程序递归过深导致堆栈溢出时抛出异常StackOverflowError。

(ps:内存泄露的主要原因是new出来的Object放在Heap上无法被gc回收,会导致内存泄漏)

感谢大家指出的错误,这里补充一下两个的区别

为什么我们的移动设备加载大图需要处理

在网络上有着许许多多的图片,有高清的,有高糊的,在pc端我们可以为所欲为,但在移动端,不可能让我们这样啊,手机的内存没有那么的大,我们的图片都是加载到内存堆中,一个app分配的内存堆是有限的,我们还要存储其他对象使用,还有一个重要因素就是虽然wifi已经普及,但还有大多数上了年纪的人对流量的概念不清楚,用你的app几分钟不仅oom了或者ANR了还欠费了(UI线程加载位图可能会降低程序性能,导致前台响应速度变慢而ANR),所以我们需要“改变图片的大小”。

处理的逻辑

显示的时候符合实际的显示规格,而不是整个图片的加载,当用户自己想看完整分辨率的图片的时候再将完整的图片载入内存显示。

处理方案

最原始的处理方案就是官网的方案,Android对位图的处理提供了整整一组的方法。这我想也应该是其他加载图片框架的基础原理。

CreateScaledBitmap

CreateScaledBitmap是Bitmap中的一个api,能更具自定义创建一个新的Bitmap,如果与原来的宽高相同则不会创建新的Bitmap。 缺点:他必须先创建一个位图。也就是说需要这个图片先被加载,解码。导致性能不高。 所以一般不采用该模式加载大图,除非你的需求是显示同一张图片不同大小。

BitmapFactory bitmapFactory = new BitmapFactory();
Bitmap bitmap = null;
try {
    bitmap = BitmapFactory.decodeStream(getAssets().open(("scg.jpeg")));
} catch (IOException e) {
    e.printStackTrace();
}
if(null != bitmap){
    //必须传入一个不为null的bitmap,宽度,高度,是否使用双线性滤波优化图片
    Bitmap changeBt = Bitmap.createScaledBitmap(bitmap, 480,240,true); 
    imgv.setImageBitmap(bitmap);
    cImgv.setImageBitmap(changeBt);
}
复制代码

效果图:

creatScaledBitmap

inSampleSize

BitmapFactory.Options#inSampleSize

inSampleSize

这是BitmapFactory.Options中的一个成员变量: 使用解码器对原图进行二次取样,接收的是一个int值,因为我们的位图就是我们的像素图,是由一个一个的小像素块组成的(你把一个图使劲放大就能看到它是由一堆小方块拼接而成的了)。小于等于1的值返回的结果都与原图一样,inSampleSize == 4 返回的图像为原始宽度/高度的1/4,像素数目的1/16。
它的原理是:行列方向每隔n格取一个像素,最后合并为一张图片

inSampleSize

BitmapFactory.Options cBitmapOptions = new BitmapFactory.Options();
cBitmapOptions.inSampleSize = 2;
Bitmap cbitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scg, cBitmapOptions);

cImgv.setImageBitmap(cbitmap);
复制代码

效果图:

inSalmpSize.png

BitmapFactory.Options三件套(inScaled+inDensity+inTargetDensity)

inScaled

inScaled

inScaled设置为true时(设置此标志时),如果inDensityinTargetDensity不为0,Bitmap就会在加载的时候直接进行缩放以匹配inTargetDensity,而不是绘制的时候进行缩放。(加载到堆内存时已经缩放了大小了)(.9图会忽略此标志)

inDensity

加载图片的原始宽度,如果此密度与inTargetDensity不匹配,则在返回Bitmap前会将它缩放至目标密度。

inTargetDensity

目标图片的显示宽度,它与inScaled与inDensity结合使用,确定如何在返回Bitmap前对其进行缩放。

原理

它的会进行相应的计算,进行颜色的混合,从而生成新的图片。也就是说图片越大它处理的性能也就越差。但它显示的效果要比inSampleSize的效果好,色彩还原度要高许多。

inDensity

扩展(如何知道图片的原始大小?)

我们要先知道图片的原始大小,然后对其进行目标密度的缩放,但如何知道图片的原始大小呢?先加载到堆内存?然后再生成新的Bitmap?这样的话和CreateScaledBitmap不是几乎一样了吗?还是会有导致OOM的风险。 我们就要介绍inJustDecodeBounds属性,它属于BitmapFactory.Options,将其设置为true,它会进行一次图片的解析,但不会生成Bitmap对象,它返回的是null,但他会对out...属性赋值,允许调用者查询Bitmap而不用为其分配内存。后续我们就可以使用BitmapFactory.Options的实例中的outWidth,outHeight获取原始图片的宽高,就可以进行像素的压缩了。

代码

BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
mBitmapOptions.inJustDecodeBounds = true;

BitmapFactory.decodeResource(getResources(), R.drawable.scg, mBitmapOptions);
int srcWidth = mBitmapOptions.outWidth;
int srcHeight = mBitmapOptions.outHeight;

BitmapFactory.Options cBitmapOptions = new BitmapFactory.Options();
cBitmapOptions.inScaled = true;
cBitmapOptions.inDensity = srcWidth;
cBitmapOptions.inTargetDensity = srcWidth * 2;

Bitmap cbitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scg, cBitmapOptions);

cImgv.setImageBitmap(cbitmap);
Toast.makeText(this,""+mBitmapOptions.outHeight,Toast.LENGTH_SHORT).show(); //结果:265 ,我本地的图片为265x265 
复制代码

效果图:

inJustDecodeBounds

综合

结合上面两种方案,我们可以配套使用 inSampleSize + 三件套 。 先将图片进行快速初略压缩(inSampleSize),再进行细微的精细压缩(三件套),只测量原图的相应属性使用inJustDecodeBounds,最后就可以按需要显示“大图”了。想要原图就使用下载或者只加载对应的原位图到堆内存中就好。

总结

以上就是Android官方文档视频对位图处理的方法。我想也应该是其他框架的压缩原理。官网视频还介绍了Glide与Picass中有异步解码和缓存。(这里我想说的是这两个框架的使用方法几乎一模一样,但好像在with中的绑定是不同的,他们的绑定生命周期不同,你使用Glide的with如果绑定的是this,可能按了home再回到Activity应用会crash掉,如果有兴趣大家可以去了解一下这两个框架)

PS

在最后一个🌰中我发现:上面的那个原理图上写着inTargetDensity / inDensity ,它主要是按像素进行压缩的,是一种图形处理的规则吧!因为我确定了inDensity后,不管怎么修改inTargetDensity图片的宽高都不会改变,但为0的话图片的清晰度不会变,inTargetDensity / inDensity 的值很小的话他会糊掉,应该就是像原理图一样进行色彩混合了,inTargetDensity / inDensity 的值很大的话,会有明显的短暂白屏(应该是计算合并处理会花费大量的时间),再大就oom了,所以在处理大图的时候才有了总结的先使用inSampleSize。想改变图片的长宽的话,就使用inDensity。它是根据你手机的dpi与inDensity的进行缩放的。(我模拟器使用的xmdpi是160 / inDensity,所以我们看效果图没缩小一半)如果inDensity设置为80它就会放大为原图的2倍。同理320就是缩小为一半。inDensity = srcWidth的话就相当于进行了自适应。

就是说inTargetDensity可以不设置,设置后是根据inTargetDensity / inDensity的值对图片进行“优化”;inDensity设置是根据手机的像素密度dpi/inDensity的值进行图片的“缩放”。

bitmap所占内存大小计算方式:

图片长度 x 图片宽度 x 一个像素点占用的字节数(所以压缩像素也是一种压缩方式)