OOM异常(一)

来源:互联网 发布:听收音机的软件 编辑:程序博客网 时间:2024/06/05 07:27

一、什么是OOM?

首先在开始我们今天的学习之前,我们必须要了解安卓的系统内存分为存储内存(ROM)和运行内存(RAM),而我们今天要讲的OOM就是和RAM有关,这一点必须要明确。现在的手机大多是2G的运行内存,可能有3G或者4G,总之,不管运行内存多大,OOM就是我们程序申请的内存太大了,超过了系统分配给我们的可用内存。

安卓系统的app的每一个进程或者虚拟机都有最大内存限制,如果超过这个限制,就会抛出OOM错误。较早的的安卓手机一个虚拟机最多只有16M内存,如果不停的加载图片,超过内存限制,就会出现OOM。

说了这么多,也就一句话总结,什么是OOM,OOM就是Out of memory,就是我们的程序当前占用的内存加上我们申请的内存资源超过了虚拟机分配的最大内存,这个时候就会抛出OOM。

这里写图片描述

举个栗子,一条金鱼每天能吃20颗饲料,你非要喂30颗,金鱼受不了,直接选择狗带了。。。

二、OOM产生的原因以及解决方案?

在知道了什么是OOM之后,我们就可以来研究一下产生OOM的原因了。那安卓中什么东西最有可能造成OOM呢?最大的可能性就是图片的加载。

图片造成的OOM

我们知道现在图片的分辨率是越来越高,动不动就是2000万柔光双摄,照亮你的美,恨不得把人的毛细血管的都照出来。。。扯远了,继续我们的分析,接下来,我要给大家计算一下一张高清大图加载到内存中会有多大,但是在那之前,我们先讲一下,安卓中是怎么加载图片的。
我们屏幕上看到的图片感觉像是完整的一块,实际上图片是由一个个的像素点组成的,你看到的图片内容包括边框都是加了颜色的像素点,而每一个像素点占多少个字节,这个又跟图片加载的参数有关,主要有以下四个:

  • Bitmap.Config ARGB_4444:由4个4位组成,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位;
  • Bitmap.Config ARGB_8888:由4个8位组成,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位;
  • Bitmap.Config RGB_565:即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位;
  • Bitmap.Config ALPHA_8:只有透明度,没有颜色,那么一个像素点占8位。
    其中:A:透明度、R:红色、G:绿、B:蓝。

一般情况下我们都是使用的ARGB_8888,由此可知它是最占内存的,因为一个像素占32位,8位=1字节(byte),所以一个像素占4字节的内存。假设有一张480x800的图片,如果格式为ARGB_8888,那么将会占用(480x800x32)/(8x1024) = 1500KB的内存。

简单来说,我们可以知道,Bitmap默认的ARGB8888是一个质量较好参数,毕竟一个像素点有32个比特位(bit),相当于4个字节(Byte)了。

那么假如说,一张图片的分辨率是2500*1000,把这张图片加载到我们的内存中需要多大的内存呢:按照默认的ARGB_8888格式来加载,一个像素占32位,也就是4个字节。2500*1000*4得到字节数;(2500*1000*4)/1024得到Kb;(2500*1000*4)/1024/1024得到M;计算过后,大概是9.5M。

也就说,这一张照片就占了9.5M,那么其他的程序占用的内存加上9.5M超过了16M,这张图片是不是就加载不出来了。再比如说,如果需要加载的是5000*2000的图片,那它所占的内存是38M,那这还怎么加载,直接OOM了。

解决图片造成的OOM

那么如何来解决这个问题呢?其实很好解决。
我们知道图片造成OOM的原因是它的分辨率过大,注意是分辨率,我们可以使用BitmapFactory.Options通过指定的采样率来缩小图片的分辨率,把缩小到合适分辨率的图片的放到ImageView上面来显示。

inSimpleSize的比例计算

计算采样率,主要是通过 BitmaoFactory.Options 的inSimpleSize参数进行。
这里我们以120*800的分辨率的图片举例子
当inSimpleSize为1时,图片的分辨率就是原来的分辨率,也就是1200*800
当inSimpleSize为2时,表示图片的宽和高都是为原来的1/2,所整张图变成了原来的1/4
当inSimpleSize位4时,表示图片的宽和高都是为原来的1/4,所以整张图也就变成原来的1/16
依次类推

inSimpleSize的数值怎么确定

这里我们以为400*400图片为例子
比如我们ImageView的大小位100*100,那么我们的,那么这时我们写一个 inSimpleSize 为2的值,那么久刚好变成原图的四分之一,那么很好,刚刚好,那么如果ImageView的大小是320*120之类的呢?问题就来了,怎么去的一个合适的值呢,还有就是,一个页面有多个ImageView,难道我们为每一个ImageView都去挨个计算取样值吗?明显不可能。

inSimpleSize怎么用

谷歌为我们提供了一个规则,很好用,看代码之前,我们还是文字说一下吧,主要逻辑如下,分三步走:

(1) 将 BitmapFactory的 inJustDecodeBounds 参数设置为true,当设置为true,代表此时不真正加载图片,而是将图片的原始宽和高数值读取出来(2) 利用options取出原始图片的宽高和请求的宽高进行比较,计算出一个合适的inSimpleSize的值(3) 将 BitmapFactory的 inJustDecodeBounds 参数设置为false,真正开始加载图片(这时候加载就是经过计算后的分辨率)

谷歌提供的方法:

import java.io.FileDescriptor;import android.content.res.Resources;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.util.Log;public class ImageResizer {    private static final String TAG = "ImageResizer";    public ImageResizer() {    }    // 从资源加载     public Bitmap decodeSampledBitmapFromResource(Resources res,int resId, int reqWidth, int reqHeight) {        // 设置inJustDecodeBounds = true ,表示先不加载图片        final BitmapFactory.Options options = new BitmapFactory.Options();        options.inJustDecodeBounds = true;        BitmapFactory.decodeResource(res, resId, options);        // 调用方法计算合适的 inSampleSize        options.inSampleSize = calculateInSampleSize(options, reqWidth,                reqHeight);        // inJustDecodeBounds 置为 false 真正开始加载图片        options.inJustDecodeBounds = false;        return BitmapFactory.decodeResource(res, resId, options);    }    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {        // 设置inJustDecodeBounds = true ,表示先不加载图片        final BitmapFactory.Options options = new BitmapFactory.Options();        options.inJustDecodeBounds = true;        BitmapFactory.decodeFileDescriptor(fd, null, options);        // 调用方法计算合适的 inSampleSize        options.inSampleSize = calculateInSampleSize(options, reqWidth,                reqHeight);        // inJustDecodeBounds 置为 false 真正开始加载图片        options.inJustDecodeBounds = false;        return BitmapFactory.decodeFileDescriptor(fd, null, options);    }    // 计算 BitmapFactpry 的 inSimpleSize的值的方法     public int calculateInSampleSize(BitmapFactory.Options options,            int reqWidth, int reqHeight) {        if (reqWidth == 0 || reqHeight == 0) {            return 1;        }        // 获取图片原生的宽和高        final int height = options.outHeight;        final int width = options.outWidth;        Log.d(TAG, "origin, w= " + width + " h=" + height);        int inSampleSize = 1;    // 如果原生的宽高大于请求的宽高,那么将原生的宽和高都置为原来的一半         if (height > reqHeight || width > reqWidth) {            final int halfHeight = height / 2;            final int halfWidth = width / 2;        // 主要计算逻辑             // Calculate the largest inSampleSize value that is a power of 2 and            // keeps both            // height and width larger than the requested height and width.            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {                inSampleSize *= 2;            }        }        Log.d(TAG, "sampleSize:" + inSampleSize);        return inSampleSize;    }}

来一个调用的代码示例:

mIvPic.setImageBitmap(new ImageResizer().decodeSampledBitmapFromResource(getResources(),R.mipmap.test_pic,300,200));

注意看下面控制台的打印信息
加载一张宽高为 5120*3200的图片,依然没问题,sampleSize为16
16*16=256,代表现在加载的这样图是原图的256分之1.
差别好大:

10-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: origin, w= 5120 h=320010-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: sampleSize:16

总结

总结一下,我们在加载分辨率很大的图片的时候,我们先把 BitmapFactory的 inJustDecodeBounds 参数设置为true,这样的话,它就仅仅加载图片的边框而不去加载图片的内容,图片的边框像素点很少,所以加载出来很容易,然后我们,通过一个方法来计算采样率,也就是inSampleSize,谷歌提供的方法是这样的:先获取图片的原始宽高,并设置采样率为1,如果宽高大于请求的宽高,就把宽高都减少一半,除以采样率,得到的宽高跟请求的宽高对比,如果宽或者高有一个比请求的宽高大,就把采样率翻倍,1变成2,2变成4,以此类推,再把减半后的宽高除以新的采样率,得到的宽高再跟请求的宽高做对比,直到宽高有一个比请求的宽高小就结束,并把BitmapFactory的 inJustDecodeBounds 参数设置为false,同时加载图片,这样加载出来的图片就比原来的图片小很多,也即是说占用的内存小了很多。

有一点啰嗦,但是,多读几遍理解了就可以,最好用自己的话说出来。

好了,这篇内容已经很多了,所以,先到这里吧,关于OOM的内容还没有结束,欲知后事如何,且听下回分解~~