Android OOM案例分析

在Android(Java)开发中,基本都会遇到java.lang.OutOfMemoryError(本文简称OOM),这种错误解决起来相对于一般的Exception或者Error都要难一些,主要是由于错误产生的root cause不是很显而易见。由于没有办法能够直接拿到用户的内存dump文件,如果错误发生在线上的版本,分析起来就会更加困难。本文从一个具体的案例切入,介绍OOM分析的思路及相关工具的使用。

案例背景

在美团App 7.4~7.7版本期间,美食业务的OOM数量居高不下,远高于历史水平,主要都是DECODE本地的资源出错。

图中OOM数量为各版本发版后第一个月的统计量,包含新发版本及历史版本。对比了同时期其他业务的情况,也有类似OOM。由于美食业务的访问量占美团App的比重较大,因此,OOM的数量相对其他业务也多一些。

思路方案

在问题较为严重的7.6~7.7版本期间,团队对OOM频现的原因有过各种猜测。笔者怀疑过是否是业务上某些修改引起的,例如头图尺寸变大,或者是由页面模块加载方式引起的等等。但这些与OOM问题出现的时间并不吻合。其次也怀疑过是否由某些ROM的Bug导致,但此推断缺乏有力的证据支撑。因此,要找到OOM的root cause,根本途径还是找到谁占的内存最多,然后再根据具体case具体分析,为什么占了这么多。

采集用户手机内存信息

要分析内存的占用,需要内存的dump文件,但是dump文件一般都比较大,让用户配合上传dump文件不合适。所以希望能够运行时采集一些内存的特征然后随着crash日志上报上来。当用户发生OOM时,dump出用户的内存,然后基于com.squareup.haha:haha:2.0.3分析,得到一些关键数据(内存占用最多的实例及所占比例等)。但这个方案很快就被证明是不可行的。主要基于下面几个原因:

  • 需要引入新的库。
  • dump和分析内存都很耗时,效率难以接受。
  • OOM时内存已经几乎耗尽,再加载内存dump文件并分析会导致二次OOM,得不偿失。

模拟复现OOM

采集用户手机内存信息的方案不可行,那么只能采取复现用户场景的方式。由于发生OOM时,用户操作路径的不确定性,无法精确复现线上的OOM,因此采取模拟复现的方式,最终发生OOM时的栈信息基本一致即可。为了能够尽量模拟用户发生OOM的场景,需要基本条件基本一致,即用户使用的手机的各种相关参数。

挖掘OOM特征

分析7.4以来的OOM,列出发生OOM的机器的特征,主要是内存和分辨率,适当考虑其它因素例如系统版本。

机型 内存 分辨率 OS stack log
OPPO N1(T/W) 2G 1920*1080 4.2.2 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
HM 2LTE-CMCC 1G 1280*720 4.4.4 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
Newman CM810 2G 1920*1080 4.4.4 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
LGL22 2G 1830*1080 4.2.2 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
OPPO X909 2G 1920*1080 4.2.2 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
Lenovo K900 2G 1920*1080 4.2.2 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
GiONEE E6 2G 1920*1080 4.2.1 java.lang.OutOfMemoryError 
at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)

这些特征可以总结为:内存一般,分辨率偏高,OOM的堆栈log基本一致。其中,OPPO N1(T/W)上所发生的OOM比重较高,约为65%,因此选定这款机器作为复现OOM的机器。

关键数据(内存dump文件)

需要复现OOM然后获取内存dump。思路是采取内存压力测试,让问题暴露的快速且充分。具体方案为:

  • 选取图片资源多且较为复杂的页面,比如美食的POI详情页。
  • 加载30次该页面,为了增加OOM的几率,30个POI页面的ID是不同的。

OOM发生后,使用Android Studio自带的Android Monitor dump出HPROF文件,然后使用SDK中的hprof-conv(位于sdk_root/platform-tools)工具转换为标准的Java堆转储文件格式,这样可以使用MAT(Eclipse Memory Analyzer)继续分析。

切到histogram视图,按shadow heap降序排列。

选取byte数组,右击->list objects->with incoming references,降序排列可以看到有很多大小一致的byte[]实例。

右击其中一个数组->Path to GC Roots-> exclude xxx references

如上图所示,这些byte[]都是系统的EdgeEffect的drawable所持有,drawable对应的bitmap占用的空间为1566 * 406 * 4 = 2543184,与byte数组的大小一致。

再看另外一个:

这些byte[]是被App的一个背景图所持有,如下图:

通过ImageView的ID(如图)及build目录下的R.txt反查可知该ImageView的ID名称,即可知其设置的背景图的大小为720 * 200(xhdpi),加载到内存并考虑density,size刚好是1080 * 300 * 4 = 1296000,与byte数组大小一致。

数据分析

为什么会出现这些大小一致的byte数组,或者说,为什么会创建多份EdgeEffect的drawable?查看EdgeEffect的源码(4.2.2)可知,其drawable成员也是通过Resources.getDrawable系统调用获取的。

/*** Construct a new EdgeEffect with a theme appropriate for the provided context.* @param context Context used to provide theming and resource information for the EdgeEffect*/
public EdgeEffect(Context context) {final Resources res = context.getResources();mEdge = res.getDrawable(R.drawable.overscroll_edge);mGlow = res.getDrawable(R.drawable.overscroll_glow);******mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f);mInterpolator = new DecelerateInterpolator();
}

ImageView(View)获取background对应的drawable的过程类似。

for (int i = 0; i < N; i++) {int attr = a.getIndex(i);switch (attr) {case com.android.internal.R.styleable.View_background:background = a.getDrawable(attr); // TypedArray.getDrawablebreak;******}
}

不论是Resources.getDrawable还是TypedArray.getDrawable,最终都会调用Resources.loadDrawable。继续看Resources.loadDrawable的源码,发现的确是使用了缓存。对于同一个drawable资源,系统只会加载一次,之后都会从缓存去取。

既然drawable的加载机制并没有问题,那么drawable所在的缓存实例或者获取drawable的Resources实例是否是同一个呢?通过下面的代码,打印出每个Activity的Resources实例及Resources实例的drawable cache。

//noinspection unchecked
LongSparseArray<WeakReference<Drawable.ConstantState>> cache = (LongSparseArray<WeakReference<Drawable.ConstantState>>) Hack.into(Resources.class).field("mDrawableCache").get(getResources());
Object appCache = Hack.into(Resources.class).field("mDrawableCache").get(getApplication().getResources());
Log.e("oom", "Resources: {application=" + getApplication().getResources() + ", activity=" + getResources() + "}");
Log.e("oom", "Resources.mDrawableCache: {application=" + appCache + ", activity=" + cache + "}");

这也进一步解释了另外一个现象,即这些大小相同的数组的个数基本和启动Activity的数量成正比。

通过数据分析可知,这些drawable之所以存在多份,是因为其所在的Resources实例并不是同一个。进一步debug可知,Resources实例存在多个的原因是开启了标志位sCompatVectorFromResourcesEnabled。
虽然最终造成OOM突然增多的原因只是开启一个标志位,但是这也告诫大家阅读API文档的重要性,其实很多时候API的使用说明已经明确告知了使用的限制条件甚至风险。

7.8版本关闭了此标志,发版后第一个月的OOM数量(包含历史版本)为153,如下图。

其中新版本发生的OOM数量为22。

总结

对于线上出现的OOM,如何分析和解决可以大致分为三个步骤:

  1. 充分挖掘特征。在挖掘特征时,需要多方面考虑,此过程更多的是猜测怀疑,所以可能的方面都要考虑到,包括但不限于代码改动、机器特征、时间特征等,必要时还需要做一定的统计分析。
  2. 根据掌握的特征寻找稳定的复现的途径。一般需要做内存压力测试,这样比较容易达到OOM的临界值,只是简单的一些正常操作难以触发OOM。
  3. 获取可分析的数据(内存dump文件)。利用MAT分析dump文件,MAT可以方便的按照大小排序实例,可以查看某些实例到GC ROOT的路径。

Android 系统性能优化(43)---Android OOM案例分析相关推荐

  1. Android 系统性能优化(14)---Android性能优化典范 - 第2季

    1)Battery Drain and Networking 对于手机程序,网络操作相对来说是比较耗电的行为.优化网络操作能够显著节约电量的消耗.在性能优化第1季里面有提到过,手机硬件的各个模块的耗电 ...

  2. Android 系统性能优化(15)---Android性能优化典范 - 第3季

    Android性能优化典范的课程最近更新到第三季了,这次一共12个短视频课程,包括的内容大致有:更高效的ArrayMap容器,使用Android系统提供的特殊容器来避免自动装箱,避免使用枚举类型,注意 ...

  3. Android 系统性能优化(27)---内存分析工具

    1.Memory Monitor 在Android Studio(以下简称AS)中Android Monitor是一个主窗口,它包含了Logcat,.Memory Monitor.CPU Monito ...

  4. 【书评】一本Android系统性能优化的新书

    Android性能优化,是一个合格的Android程序员必备的技能,现如今几乎所有的Android面试内容都会或多或少涉及性能优化方面的话题. 学习Android性能优化可以让我们在简历上展示自己的专 ...

  5. 那些年,我们一起经历过的 Android 系统性能优化

    Android 系统性能优化,最近几年 ,Google 之外,绝对是中国的手机厂商参与得最深最广.不光深挖系统.场景,还要和 Top 应用合作优化,和各种流氓应用勾心斗角.同时 Google 也在不断 ...

  6. 【Android CPU 优化】Android CPU 调优 ( Trace 文件分析 | Android Profiler 工具 | CPU Profiler 工具 )

    文章目录 一.Android CPU 优化 二.CPU Profiler 工具 三.相关资源 一.Android CPU 优化 在 Android 中 , 出现 动画掉帧 , 页面切换白屏 , 卡顿 ...

  7. 【Android 内存优化】Android 工程中使用 libjpeg-turbo 压缩图片 ( 初始化压缩对象 | 打开文件 | 设置压缩参数 | 写入压缩图像数据 | 完成压缩 | 释放资源 )

    文章目录 一.使用 libjpeg-turbo 压缩图片流程 二.初始化 JPEG 压缩对象 三.打开文件 四.设置压缩参数 五.开始压缩 六.循环写入压缩数据 七.完成图片压缩及收尾 八.libjp ...

  8. 【Android 内存优化】Android 原生 API 图片压缩原理 ( 哈夫曼编码开关 | 哈夫曼编码原理 | libjpeg-turbo 函数库 )

    文章目录 一. 哈夫曼编码开关 二. 哈夫曼编码原理 三. libjpeg-turbo 函数库 四. libjpeg-turbo 函数库下载 [Android 内存优化]图片文件压缩 ( Androi ...

  9. 【Android 内存优化】Android 原生 API 图片压缩原理 ( Bitmap_compress 方法解析 | Skia 二维图形库 | libjpeg 函数库 | libpng 函数库 )

    文章目录 一. 图片质量压缩方法 二. Skia 二维图形库 三. libjpeg.libpng 函数库引入 在博客 [Android 内存优化]图片文件压缩 ( Android 原生 API 提供的 ...

最新文章

  1. tabs选项卡切换效果(jquery版)
  2. 彻底卸载WinStdup
  3. [java设计模式简记] 观察者模式(Observer-Pattern)
  4. 机票分享第一篇 机票由何而来
  5. OptionRoom推出通缩NFT质押活动,销毁15万代币
  6. what?传统风控策略,无法cover到以下风控场景
  7. mySAP标准培训教材全套列表
  8. 驗證類javascript
  9. 15. Provide access to raw resources in resouce-managing classes
  10. http接口开发与调用案例
  11. Java并发编程系列文章目录帖及源码
  12. 通信协议之一线协议(1-Wire)解析
  13. 给 Vuepress 和 ElementUI 添加夜间暗色模式
  14. iOS端屏幕录制ReplayKit
  15. API ,批量添加
  16. 中图杯获奖作品计算机组,“中图杯”第十四届环境地图大赛召开 地图慧成指定制图软件...
  17. 图片查看器 Viewer.js
  18. springboot毕设项目电信CRM系统的设计与实现3z7u5(java+VUE+Mybatis+Maven+Mysql)
  19. 【Android】Unable to get provider com.crashlytics.android.CrashlyticsInitProvider: io.fabric.sdk.andro
  20. linux系统是什么意思

热门文章

  1. linux input子系统分析--主要函数
  2. Particle Filter Tutorial 粒子滤波:从推导到应用(二)
  3. mysql中merge的用法_mysql中merge表存儲引擎用法介紹
  4. Java面试23种设计模式之单例模式的8种实现方式
  5. 如果对象的引用被置为null,;垃圾回收器是否会立即释放对象占用的内存?
  6. java并发(二):初探syncronized
  7. 工作绩效数据、工作绩效信息和工作绩效报告的区别
  8. linux下不同tomcat使用不同的jdk版本
  9. UI自动化测试POM设计之-maven工程
  10. 容易被忽略的label标签