Android性能调优篇之内存溢出

开篇废话

上一篇我们了解了Android里面相关的内存泄露以及相应的处理方案,这一篇,接着上一篇的内存泄露的内容,讲一下Android当中的内存溢出。

内存溢出与内存泄露,很多开发人员都容易产生混淆,有可能是因为这两个概念有点关系,又因为名称上也不太好区分吧。不过,我们依然要清楚,内存溢出(Out Of Memory Error) 与 内存泄露 (Memory Leak)还是有质的区别的。都我们的App多次出现内存泄露,可能就会导致内存溢出。

但是,我们的App出现内存溢出,不一定就是因为内存泄露,因为本身Android系统分配给每一个的App的空间就是那么一点。

另外,内存泄露也不一定就会出现内存溢出,因为还是泄露的速度比较慢,系统将进程杀死了,也就不会内存溢出咯,不过,发现内存泄露,我们还是要第一时间解决掉这个bug。


技术详情

讲述逻辑如下:

1.什么是内存溢出

2.有些内存里面容易混淆的概念

3.如何解决内存溢出

1.什么是内存溢出

内存溢出,OOM(Out Of Memory),表示当前占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存限制就会抛出的Out Of Memory异常。大部分的OOM的问题,都会与Bitmap的加载有关系

2.内存里面容易混淆的一些概念

主要有三个概念:

1.内存溢出

2.内存抖动

3.内存泄露

其中第一个内存溢出,就是刚刚讲的OOM,第三个内存泄露,可以查看我的上一篇文章。

关于第二个内存抖动,出现的情况是,短时间内,大量的对象被创建,然后又马上被释放,瞬间产生的对象会严重占用内存区域,这个区域就是我们之前接触的那个年轻代区域,到达这个区域的阈值时就会触发minor gc,当出现频繁的minor gc的时候,就会出现内存抖动,我们能够通过我们的Android Studio的Memory Monitor能够非常直观的看到内存抖动

内存抖动

出现内存抖动的现象,可根据当前app处理的实际业务结合Memory Monitor中的现象来进行判断,然后有针对性的进行优化。

它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动

内存溢出对我们的App来说,影响是非常大的,整得不好,就有可能导致程序闪退,无响应等现象,因此,我们一定要优先解决OOM的问题。

3.如何解决内存溢出

如何解决OOM,这个问题范围比较大,我这边大概从两个方面去讲述:

1.关于Bitmap的OOM

2.除了Bitmap之外的OOM

3.1 关于Bitmap的OOM

关于Bitmap的OOM我们有几点需要注意的。

3.1.1 ImageView等控件图片的显示

意思就是加载合适属性的图片,当我们有些场景是可以显示缩略图的时候,就不要调用网络请求加载大图,例如在ListView中,我们在上下滑动的时候,就不要去调用网络请求,当监听到滑动结束的时候,才去加载大图,以免上下滑动的时候产生卡顿现象。

3.1.2 及时释放内存

我们知道,在Android系统中,本身就有自己的垃圾回收机制,系统会不定期进行垃圾回收的。但是,这个只是针对Java那一块的内存,但是我们需要知道Bitmap实例化的时候,是通过JNI的方式,所以还有一部分的内存是C那一块的,我们的GC没有办法回收,所以,我们在不用的时候,还是需要调用recycle()方法,源码里面,recycle()方法其实就是调用的JNI的函数,然后释放C那一块的内存。

3.1.3 把图片进行压缩

我们在实际开发过程当中,可能因为业务需要,需要加载一张很大的图片,大到直接可以超过系统分配给我们App的内存大小,这样,就会直接导致内存溢出,那么,这个时候,我们就应当控制图片的大小,那么就应该将bitmap进行压缩了。

下面大概讲一下对一张图片进行压缩的一个过程。

第一步:计算实际采样率

/**
 * 计算压缩比例值
 * @param options             解析图片的配置信息
 * @param reqWidth            所需图片压缩尺寸最小宽度
 * @param reqHeight           所需图片压缩尺寸最小高度
 * @return
 */
public static int calculateInSampleSize(BitmapFactory.Options options,
         int reqWidth, int reqHeight) {
   //保存图片原宽高值
   final int height = options. outHeight;
   final int width = options. outWidth;

   //初始化压缩比例为1
   int inSampleSize = 1;

   //当图片宽高值任何一个大于所需压缩图片宽高值时,进入循环计算系统
   if (height > reqHeight || width > reqWidth) {

         final int halfHeight = height / 2;
         final int halfWidth = width / 2;

         //压缩比例值每次循环两倍增加,
         //直到原图宽高值的一半除以压缩值后都~大于所需宽高值为止
         while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
              inSampleSize *= 2;
        }
   }
   return inSampleSize;
}

第二步:根据得到的采样率对图片进行解析

/**
 * 获取压缩后的图片
 * @param res
 * @param resId
 * @param reqWidth            所需图片压缩尺寸最小宽度
 * @param reqHeight           所需图片压缩尺寸最小高度
 * @return
 */
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
    int reqWidth, int reqHeight) {

    //首先不加载图片,仅获取图片尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();

    //当inJustDecodeBounds设为true时,不会加载图片仅获取图片尺寸信息
    options.inJustDecodeBounds = true;

    //此时仅会将图片信息会保存至options对象内,decode方法不会返回bitmap对象
    BitmapFactory.decodeResource(res, resId, options);

    //计算压缩比例,如inSampleSize=4时,图片会压缩成原图的1/4
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    //当inJustDecodeBounds设为false时,BitmapFactory.decode...就会返回图片对象了
    options. inJustDecodeBounds = false;

    //利用计算的比例值获取压缩后的图片对象
    return BitmapFactory.decodeResource(res, resId, options);
}

3.1.4 使用Bitmap的高级属性inBitmap

Bitmap的inBitmap高级属性主要是值复用内存块,不需要在重新给新的bitmap对象申请一块新的内存,避免了一次内存的分配和回收,从而提供了我们程序运行的效率。

不过这个属性还是有一些坑的,对于适配Android3.0以上 。而且,这个功能,google一直在优化当中,在Android4.4以前,只能复用相同大小的bitmap内存,而4.4之后,则只要比之前的内存小,就可以了。以下贴出inBitmap的简单使用方法:

第一步:首先判断当前图片是否能够使用inBitmap

/**
 * 判断是否能够使用inBigmap
 * @param candidate         比较标准
 * @param targetOptions     判断目标对象属性
 * @return
 */
public static boolean canUseForInBitmap(
    Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use
        // if the byte size of the new bitmap is smaller than
        // the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height =
            targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height
            * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions,
    // the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
        && candidate.getHeight() == targetOptions.outHeight
        && targetOptions.inSampleSize == 1;
}

第二步:从缓存里面拿出bitmap,将此Bitmap赋值给inBitmap。

/**
 * 将Bitmap赋值给inBitmap
 * @param options             图片的配置信息
 * @param cache               图片缓存
 */
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {

    //inBitmap only works with mutable bitmaps, so force the decoder to
    //return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found,
            // set it as the value of inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

第三步:调用刚刚图片压缩时候的decode方法,把options参数传入

3.1.5 捕获异常

很多时候,当内存确实很吃紧的时候,难免还是会出现OOM,所以,根据经验之谈,我们在开发过程中,实例化Bitmap的时候,最好还是添加try catch,进行异常捕获。

需要注意,平常的Exception异常是捕获不到OOM Erro的,因为OOM是一个错误,我们编码的时候需要捕获错误,具体给出以下示例代码:

public static Bitmap createBitmap(int width, int height, Bitmap.Config config) {  
    Bitmap bitmap = null;  
    try {  
        bitmap = Bitmap.createBitmap(width, height, config);  
    } catch (OutOfMemoryError e) {  
        while(bitmap == null) {  
            System.gc();  
            System.runFinalization();  
            bitmap = createBitmap(width, height, config);  
        }  
    }  
} 

3.2 除了Bitmap之外的OOM

3.2.1 listview

这个listview确实提到了好多次,毕竟我们实际开发当中,用它来呈现一些数据确实的频率也蛮高,还是需要讲述一下。

使用listview的时候,一定要记得复用convertView

同时,在listview当中,如果需要显示大图的控件,记得使用LRU(最近最少使用,三级缓存)机制进行缓存图片

3.2.2 onDraw方法当中,尽量避免对象的创建

如果在onDraw方法中创建对象,会触发频繁的GC,也就是之前提到的内存抖动,当内存抖动积累到一定的程度,也会出现内存溢出。

3.2.3 使用多进程,一定要小心小心再小心

我们有的时候需要将一些服务,或者主件放到另外一个进程去运行,例如一些定位,推送等,这样确实可以分担主进程的内存压力。

但是,多进程中的一些通信真心没有那么简单。很多机制可能失效,从而影响业务的基本功能。可能会出现一些莫名其妙的crash.

所以,如果我们的App实际业务没有达到一定程度,真心不要使用多进程。


干货总结

此篇文章根据OOM是什么,了解一些容易混淆的概念,然后熟悉一些OOM的解决方案这个逻辑,再结合实际开发可能遇到的问题,讲述了内存溢出的相关知识。其实,大篇幅都是在讲述Bitmap的处理方案,因为,我们这个Bitmap确实在实际开发当中引发OOM的概率还是相当大的。

希望通过以上的讲述,我们能够对于OOM有一个清晰的了解,从而根据我们实际开发当中自己的业务,进行OOM的优化。

其实有的时候,我们在解决OOM的时候需要有一个权衡,因为如果考虑到了OOM的情况而频繁触发GC,可能会导致UI卡顿的现象,跟严重的可能出现ANR的问题,需要我们在实际开发过程中具体场景具体分析。

好了,内存的泄露的知识就先更新到这了,如果觉得本篇文章对大家有益,请给予一个赞和喜欢,这样我才更有动力一直更新下去,如果想和我一起探讨的,可以关注一波。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容