安卓OOM和Bitmap图片二级缓存机制(一)

本文出自 “阿敏其人” 博客,转载或引用请注明出处。

OOM(Out Of Memory)

什么是OOM

手机系统内存份存储内存(ROM)和运行内存(RAM),我们谈论OOM讨论的是运行内存,这点如果是新人需要明确。。现在一般来说手机运行内存是2G,3G基本就算很顶配了,4G运行内存的话只有个别手机配置了。

简而言之,OOM就是我们申请的内存太大了,超出了系统分配给我们(app或者说进程)的可用内存。

android系统的app的每个进程或者每个虚拟机有个最大内存限制,如果申请的内存资源超过这个限制,系统就会抛出OOM错误。跟整个设备的剩余内存没太大关系。比如比较早的android系统的一个虚拟机最多16M内存,当一个app启动后,虚拟机不停的申请内存资源来装载图片,当超过内存上限时就出现OOM。

安卓OOM和Bitmap图片二级缓存机制(一)_第1张图片
举个栗子.png

举个栗子,一条金鱼,每次只能吃24颗饲料,你偏偏要喂它30颗,结果,金鱼受不鸟,就挂掉了。

安卓手机有多少内存

早期的手机是每个进程(每个app)分配16M。
后来随着慢慢发展,开始有了24M的,32M的,再变态就是64了。
具体每个手机的给app分配的运行内存根据厂商和机型的不同而定,但是基本的几个数值是一样的。

安卓手机基于Linux系统,Linux是一个多用户的操作系统,一个app在安卓手机里面就是一个用户,一个用户分配到了16m(假如是16m,那么统一每一个app就是16m),当我当前这个app挂了,不会影响我其他程序的运行。
比如我的手机里面有10个app,其中3个在运行,那么这个手机就有3个进程在运行,这3个进程每一个都分配到了(16m)的运行内存。

每个App的内存怎么分配

我是一个app,我被启动了,我分配到了16m的空间,而且,这16m还不是完完整整给你当前程序自己玩个够的,有一部分还必须分给native内存。

  • 那么每一个程序的分配到的运行内存到底是怎么分配的呢?

16M = dalvik内存(Java) + native内存(C/C++)
APP内存由 dalvik内存 和 native内存 2部分组成,dalvik也就是java堆,创建的对象就是就是在这里分配的,而native是通过c/c++方式申请的内存,Bitmap就是以这种方式分配的。(android3.0以后,系统都默认通过dalvik分配的,native作为堆来管理)。这2部分加起来不能超过android对单个进程,虚拟机的内存限制。

至于这Dvlyik和Native两部分的分配,有个特点值得说一下。那就是Dalvik(Java)申请的内存即使释放了,native也别想去申请,只能Dalvik自己用,Dalivk申请过的内存Native就不能用了。

以下为引用部分

基于Android开发多媒体和游戏应用时,可能会挺经常出现Out Of Memory 异常 ,顾名思义这个异常是说你的内存不够用或者耗尽了。
在Android中,一个Process 只能使用16M内存,如果超过了这个限制就会跳出这个异常。这样就要求我们要时刻想着释放资源。Java的回收工作是交给GC的,如何让GC能及时的回收已经不是用的对象,这个里面有很多技巧,大家可以google一下。
因为总内存的使用超过16M而导致OOM的情况,非常简单,我就不继续展开说。值得注意的是Bitmap在不用时,一定要recycle,不然OOM是非常容易出现的。
本文想跟大家一起讨论的是另一种情况:明明还有很多内存,但是发生OOM了。
这种情况经常出现在生成Bitmap的时候。有兴趣的可以试一下,在一个函数里生成一个13m 的int数组。
再该函数结束后,按理说这个int数组应该已经被释放了,或者说可以释放,这个13M的空间应该可以空出来,
这个时候如果你继续生成一个10M的int数组是没有问题的,反而生成一个4M的Bitmap就会跳出OOM。这个就奇怪了,为什么10M的int够空间,反而4M的Bitmap不够呢?
这个问题困扰很久,在网上,国外各大论坛搜索了很久,一般关于OOM的解释和解决方法都是,如何让GC尽快回收的代码风格之类,并没有实际的支出上述情况的根源。
直到昨天在一个老外的blog上终于看到了这方面的解释,我理解后归纳如下:
在Android中:
1.一个进程的内存可以由2个部分组成:java 使用内存 ,C 使用内存 ,这两个内存的和必须小于16M,不然就会出现大家熟悉的OOM,这个就是第一种OOM的情况。
2.更加奇怪的是这个:一旦内存分配给Java后,以后这块内存即使释放后,也只能给Java的使用,这个估计跟java虚拟机里把内存分成好几块进行缓存的原因有关,反正C就别想用到这块的内存了,所以如果Java突然占用了一个大块内存,即使很快释放了:
C能使用的内存 = 16M - Java某一瞬间占用的最大内存。
而Bitmap的生成是通过malloc进行内存分配的,占用的是C的内存,这个也就说明了,上述的4MBitmap无法生成的原因,因为在13M被Java用过后,剩下C能用的只有3M了。

引用至此结束
点此查看原文地址

引用这一部分的描述,就是为了进一步证明,每个app所占用的16m(比如说16m)运行内存不是自己可以玩个够的,还得和另外一个小伙伴分享

另外清楚一点,1、我们在Bitmap的时候申请的内存是输入C/C++的,也就是Native这一块的

OOM一般在什么时候发生?

造成OOM的可以概括为两种情况:
1、Bitmap的使用上 (利用Lru的LruCache和DiskLruCache两个类来解决)
2、线程的管理上(利用线程池管理解决。不纳入本次探讨)

Bitmap导致的OOM是比较常见的,而针对Bitmap,常见的有两种情况:

  • 单个ImageView加载高清大图的时候
  • ListView或者GridView等批量快速加载图片的时候
    简而言之,几乎都是操作Bitmap的时候发生的。

制造一个OOM的例子

当前环境:

  • Android Studio1.4
  • win7 64bit
  • 模拟器: Genymotion Nexus One 2.3.7 API10 480*800

如何获得当前手机把为每个app(进程)分配的运行内存

// 测试每个app可用的最大内存(安卓每一个app都运行在自己独立的沙箱里面)
ActivityManager activityManager=(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();// 返回的就是本机给每个app分配的运行内存      

当我们当前测模拟器返回的 32M 的运行内存

安卓OOM和Bitmap图片二级缓存机制(一)_第2张图片
加载图片

在此附上相关代码:

public class MainActivity extends Activity implements View.OnClickListener {

    private TextView mTvBtn;  // 按钮
    private TextView mTvNum;  // 显示最大内存
    private TextView mTvLoadBigPic;  // 加载图片按钮
    private ImageView  mIvPic;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        initView();

    }

    private void initView() {
        mTvBtn= (TextView) findViewById(R.id.mTvBtn);
        mTvNum= (TextView) findViewById(R.id.mTvNum);
        mTvLoadBigPic= (TextView) findViewById(R.id.mTvLoadBigPic);
        mIvPic= (ImageView) findViewById(R.id.mIvPic);

        mTvBtn.setOnClickListener(this);

        mTvLoadBigPic.setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.mTvBtn:
                // 测试每个app可用的最大内存(安卓每一个app都运行在自己独立的沙箱里面)
                ActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
                int memoryClass = activityManager.getMemoryClass();// 返回的就是本机给每个app分配的运行内存
                mTvNum.setText("最大内存: "+memoryClass);
                break;

            case R.id.mTvLoadBigPic:
                Bitmap bigPicBitMap=BitmapFactory.decodeResource(getResources(),R.mipmap.test_pic);
                mIvPic.setImageBitmap(bigPicBitMap);
                break;
        }
    }
}

看完代码,我们这里应该停下来看看一下Bitmap类,补充一些知识

通过这样的代码就可以首先从资源文件里面加载图片

Bitmap bigPicBitMap=BitmapFactory.decodeResource(getResources(),R.mipmap.test_pic);
mIvPic.setImageBitmap(bigPicBitMap); 

关于Bitmap和BitmapFactory的知识可以百度补充
提一下,BitmapFactory提供了4类方法用于加载Bitmap对象

  • 1、decodeFile
  • 2、decodeResource
  • 3、decodeStream
  • 4、decodeByteArray
    分别从文件系统、资源、输入流和字节数组读取Bitmap对象
    其中,decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法都是在安卓底层实现的,对应BitmapFactory类的几个Native类。

decodeResource这个方法内应说到底还是需要创建一个位图(Bitmap),
对于创建位图,我们来补充一个知识,先看一下的下面这个方法:

public static Bitmap createBitmap (int[] colors, int width, int height, Bitmap.Config config) 

具体安卓内部如何调用这个方法本人不得而知,但是我们要明白的是 config 这个参数,每一个位图都有一个默认confit参数,默认值是 ARGB8888
对于config,有几个值,我们借用一个文章说明一下:

A:透明度
R:红色
G:绿
B:蓝
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位。
一般情况下我们都是使用的ARGB_8888,由此可知它是最占内存的,因为一个像素占32位,8位=1字节(byte),所以一个像素占4字节的内存。假设有一张480x800的图片,如果格式为ARGB_8888,那么将会占用(480x800x32)/(8x1024) = 1500KB的内存。

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

梦回唐朝,接着说OOM的例子

有了Bitmap和config的知识之后,我们的OOM的成功与否就看我们图片的分辨率了

加入说,图片分辨率是2500+1000,那么加载这张图片所需要的运行内存我们可以大概这么算:
Bitmap.Config ARGB_8888情况下,一个像素点占32位,也就是4个字节。
2500 * 1000 * 4 得出多少个byte
(2500 * 1000) / 1024 得出kb
(2500 * 1000) / 1024 / 1024 得出m
经过运算,得出加载所需的运行内存大致为9.5m

是不是说我们当前手机的这个app就一定可以加载这张图片呢?
不一定,如果这个时候app还有其他代码也占用着的内存,可能就加载不了了。而且我们说过,分配到的16m内存不是自己玩个够,还得两个哥们分着玩。
如果想一针见血,彻底减小,可以整个5000*2000的图片,肯定马上挂掉,爆出OOM。
5000 * 2000 * 4 / 1024 / 1024 得出 38m多。一针见血


高效加载大图和二级缓存,避免OOM

知道了OOM是什么,怎么发生的,接下来我们就应该知道怎么解决问题了。
提出的问题的人很多,拿出解决办法才是关键。

如何高效加载大图?

造成OOM的核心原因:图片分辨率过大

核心解决办法:图片,我们只加载适合的、需要的尺寸!!利用BitmapFactory.Options可完成这一项任务。

注意:我们要处理的分辨率的问题,而不是图片本身大小的问题,一个100*100的10m的图片和一张2000*2000的2m的图片,对我们来说,2m的那张对我们来说反而是大图片,我们针对的是分辨率

通过BitmapFactory.Options通过指定的采样率来缩小图片的分辨率,把缩小到合适分辨率的图片的放到ImageView上面来显示,大大降低了内存压力,有效避免OOM,至于缩小的怎样的分辨率才算合适,谷歌有为我们提供了一段代码,就可以得出这个合适的度!这段代码后面会贴出。

inSimpleSize的比例计算

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

inSimpleSize数值的说明

  • inSimpleSize的值必须是整数
  • inSimpleSize的值不能是负数,负数无效
  • inSimpleSize的值谷歌建议是2的整数倍,当然你可以写个3,但是最好不要这么干

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));

安卓OOM和Bitmap图片二级缓存机制(一)_第3张图片
当前模拟器为32M运行内存的

注意看下面控制台的打印信息
加载一张宽高为 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=3200
10-23 08:34:13.884 13265-13265/oomtest.amqr.com.oomandbitmap D/ImageResizer: sampleSize:16

高效加载图片不报OOM就先说到这里啦,下一篇再说图片的二级缓存,也叫图片的存取机制
二级,即为内存缓存,本地缓存,网络,,三者一起构成了图片的存取机制。
内存缓存拿不到就去本地拿。本地拿不到就去网络拿。当我们第一次获取A图片,肯定是是从网络获取的,网络获取后,图片A就存储到本地缓存,就这还会缓存到内存缓存。

缓存主要利用的一个机制是Lru,(Least Recently Used)最近最少使用的。
而Lru和只要是利用两个类,LruCache 和 DiskLruCache。

LruCache主要针对的是 内存缓存 (缓存)
DiskLruCache 主要针对的是 存储缓存 (本地)

第二篇链接
安卓OOM和Bitmap图片二级缓存机制(二)

本篇完。


本篇相关参考:
Android Out Of Memory(OOM) 的详细研究

Android应用中OOM问题剖析和解决方案

Android开发艺术探索

你可能感兴趣的:(安卓OOM和Bitmap图片二级缓存机制(一))