基于端侧的Android内存分析SDK-MemoryProphet

1 背景

在Android端App的性能优化过程中,我们发现应用的内存问题会对很多我们关心的指标产生负面影响,这其中包括:

  • UI流畅度
  • 后台存活时间
  • OutOfMemoryError崩溃

大部分内存使用不当并不会导致App进程立即崩溃,而具有较长的潜伏周期;很多内存问题由于路径深,依赖交互方式和使用上下文,相比语言崩溃来说,更加难以定位。

2 方案思路

  • 真实环境捕捉异常镜像,而非实验室模拟
  • 避免低效重复劳动,自动化问题分析
  • 基于设备端侧实现高性能解析引擎
  • 萃取原始镜像中的结论性有用信息
  • 长效监控机制,问题跟踪闭环机制和异常判定规则机制

2.1 异常镜像的采集

实验室环境毕竟难以模拟用户实际使用场景/硬件场景/数据场景,我们希望在真实环境捕捉异常镜像,而非实验室模拟。类似LeakCanary方案只能结合自动化测试机制在实验室里进行测试。
我们希望采集之后的分析不需要传给服务端去做,因为内存镜像文件特别大,一般一个异常镜像能达到200MB,压缩后的size也是50到100MB,这么大的文件传给服务端(必定只有wifi环境才能上传),对于网络环境约束较高,同时上传成功率较低。上传服务端的方案必然存在一个较低的转化率,客户端捕捉到的镜像中只会有很少一部分成功发给了服务端,这样,服务端分析的样本就会少很多,低概率问题会难以发现。

2.2 异常问题的分析

内存镜像分析有套路。如特大对象,如实例数超过合理值,如被不合理持有引用的对象等等。我们希望将套路用代码实现,固化规则,开放参数配置,避免低效重复劳动,自动化问题分析

内存异常问题存在概率性/平台依赖性,因此我们希望分析到所有的异常内存镜像,这就要求我们开发出基于设备端侧实现高性能解析引擎。LeakCanary方案里面的解析引擎是Java语言实现的haha库,解析镜像发生OOM概率高(受限于JVM的最大堆大小的限制),同时解析速度比较慢,很多情况下3分钟都无法解析完成一个镜像。

解析过程是用固化套路进行规则分析的过程,我们最终希望萃取原始镜像中的结论性有用信息,得到一个体积较小的结论性分析报告,以便发给服务端做报告的聚合。LeakCanary方案里面其实就一条规则:onDestory了的Activity不应该出现,我们应该更丰富一些。

我们希望对内存异常问题做线上的版本间的长期跟踪,需要做好问题的跟踪和闭环。其中,问题判定的规则非常重要,开发同学要做好运行参数设定和对象存在合理性规则,才能做具体对象合理性的最终判断。总结就是长效监控机制,问题跟踪闭环机制和异常判定规则机制

3 解决方案特色

3.1 Native高性能解析引擎

引擎使用了C++的STL标准库(static编译的gunstl),因此Size相对比较大,但是编译进了APK中会有大概190KB的增量。我们推荐在发布前灰度版本中开启这块功能,能发现问题还不会增大正式包的size。

为了防止镜像的解析会污染原有进程的数据环境或崩溃导致App进程挂掉,镜像分析引擎运行在单独开
出的另外一个进程里,十几秒的解析结束后会自动杀掉自身进程。

镜像解析处理引擎的具体性能参数如下表所示:


性能表格.PNG

较小的时间消耗和内存消耗保证了在客户端设备上进行分析的可行性,也保证了镜像采集和镜像分析之间百分百的转化率。只要所有的异常镜像都得到分析,那么即便只出现过1次的问题也能被检测出来。

3.2 可配置可扩展的规则匹配体系

比如特大对象规则,实例数量合理阈值规则,服务端侧的运行环境参数判定规则,以及Finalize队列同类对象数量和文件句柄数量规则等。规则中的重要参数对业务方开放配置。

3.3 低Size的Json格式的泄漏点引用链分析报告

Json文件的内容:

  • 全局软硬件静态参数
  • 镜像抓取时间点的运行时参数
  • 基于各种异常规则的分析报告
  • 文件句柄和finalize队列情况

3.4 低网络带宽和服务器端资源消耗

将100-200MB的文件萃取为3-8KB的Json之后(随着镜像内容和规则的丰富会有增加),直接上传给服务端也无压力。 服务端不需要进行镜像分析,需要进行反混淆,分析报告聚合入库,结合对象存在规则进行合理性判断,提供RESTFUL API给前端。

4 解决方案技术架构

逻辑上有三部分,部署上有两部分。

我们目前的条件判断是切后台(所有Activity都后台)和锁屏状态,同时Java堆大小或Pss大小满足我们的阈值标准。切后台和锁屏状态的限制是可配置的,

重点需要解释一下镜像分析引擎和规则匹配引擎。

技术架构.PNG

4.1 处理引擎

4.1.1 HPROF文件的反序列化

HPROF文件是个格式化了的二进制文件,它包含了某时间点下JVM堆里面的一切信息,也就是所有对象的信息,这些信息需要映射到我们的进程中。
Hprof文件的格式详情可以参阅: <a href="https://docs.oracle.com/javase/7/docs/technotes/samples/hprof.html?cm_mc_uid=42001984893514816092709&cm_mc_sid_50200000=1493115298">HPROF文件的格式</a>

在JVM眼里,所有对象都是下面四种角色之一:


对象类型.png

其中GC Root也就是GC算法中的根节点,如果一个对象无法被GC释放掉,它肯定是GC Root或者处于GC Root的引用链上。而我们在最后的Json报告中,描述对象就一定要描述它的引用链。

处理引擎的核心是构建对象关系图构建可回溯支配树两个步骤,这两个步骤是后续规则引擎运行的基础。

4.1.2 构建对象关系图

实例之间的关系非常复杂,有引用关系和索引关系。引用关系是GC算法所关切的核心,索引关系是分析引用关系以及运行规则匹配引擎而必须建立的关系。
下表是一部分对象之间的关系:


对象关系.PNG

对象关系图的构建流程:


对象关系图构建步骤.png

4.1.3 构建可回溯支配树

为了定位每个对象实例,我们需要知道某个对象的所处的引用链。从该对象出发,找到该对象的支配者(Dominator),从而回溯到某个GC Root节点。JVM只要可以明确哪些对象是GC Root,加上可达性分析算法,就得出了哪些对象该被回收。

每一个GC Root的对象都有不可被回收的理由,由JVM进行判定,比如:

  • 被系统ClassLoader加载的Class
  • 正在运行的Thread
  • Java栈中的局部变量或参数(未退栈)
  • JNI栈中的局部变量或参数
  • JNI的全局引用
  • 由JVM定义的特殊类


    gcroot.png

支配树的计算关键在于将错综复杂的有向图梳理成树(以每个GC Root为根对应一个树)。在非递归基于队列的遍历算法中,被多个对象引用的对象只计算一次。


支配树计算.png

以上工作做好之后,就为规则引擎的运行打下了良好的基础,可以做以下操作:

  • 由类型字符串索引到类型;
  • 由类型索引实例数组,由类型索引到静态Field值和类型;
  • 由实例可以查阅实例的Field值和类型;
  • 任意一个对象可以找到他的上级支配者对象

4.2 规则匹配引擎

规则匹配引擎是分析镜像的关键,我们将一些通用问题的分析套路固化下来;同时,非通用的套路固化框架,注入参数。

4.2.1 通用规则

  • 大byte[] / char[]规则,长度超过X的
  • 大Bitmap规则,mHeight * mWidth的结果超过Y的
  • 进程持有大量文件句柄(最大1024个)
  • Finalize队列中存在大量同类对象

4.2.2 需要运行时注入参数的规则

  • 功能相关运行参数,比如用户在使用某功能或是已经退出使用,服务端需要根据这些参数辅助进行引用链合理性判定,如大Bitmap的合理性
  • 对象数量合理阈值,比如浏览器开了几个网页就应该对应几个WebWindow实例,多出来的话一定存在不合理的引用;或是ListView用到的ItemView,因为存在回收重用机制,如果对象数量超过50个,可能就存在问题,比如被操作回调所引用等。

规则是问题发现的核心,我们也在不停的探索和完善这些规则。

举个发现的简单的特大Bitmap的例子(例子来源于UC实现的线上后台系统),这个图片是APK的icon,获取到的icon特别特别大达到60MB,出现次数只有1个pv:

a39cfa0f9581f779a4a9f29723453609.png

5 SDK的使用

5.1 配置指引

初始化,需要配置BasicConfig和AdvancedConfig,分别是基本配置和全局性质的配置:

    MemoryAnalyzerSDK.initAnalyzer(new BasicConfig(), new AdvancedConfig());

如需设置byte[]和Bitmap对象的监控阈值,如在AdvancedConfig配置,缺省分别为1024512 / 102464:

    @Override
    public long getByteArrayLengthThreshold() {
        return 1024 *256;
    }

    @Override
    public long getBitmapAreaThreshold() {
        return 1024 * 128;
    }

如需设置需要配置合理阈值,请实现AdvancedConfig,在镜像截取前设置预期实例数:

    @Override
    public void onBeforeDumpHprof() {
        MemoryAnalyzerSDK.setBaseClassInstancesThreshold(BaseClassTest.class.getName(), BaseTestManager.getInstance().getReasonalCount());
        MemoryAnalyzerSDK.setBaseClassInstancesThreshold(WebWindow.class.getName(), WebWindowManager.getInstance().getReasonalCount());
    }

5.2 线上后台系统

为了配合端侧SDK的工作,我们需要搭建一个线上后台系统相配合。线上系统需要接收SDK上传的JSON格式文件并做必要的处理和分析,除此外,后台系统需要做一部分规则匹配的工作,主要是根据运行参数计算某引用链对某种对象持有的合理性。

我们将来会提供配套的后台系统(UC内部已经实现),但目前需要自己去做相应的实现。SDK和后台系统有着明确的数据协议边界,也就是JSON格式的文件。

我们还需要配套的前端完成数据展示和规则配置界面的开发,UC内部已有实现,将来会提供服务开放。

5.2.1 JSON格式文件的处理和分析

上报的引用链是混淆过的,我们需要打包流水号来进行代码的反混淆;反混淆出来的引用链适合存放在NoSql数据库里。实现上,为引用链取唯一hash表示一类引用链,将引用链和异常对象进行关联,然后将异常对象和引用链类型与运行时环境关联起来。
数据的核心是异常对象的引用链类型,有多维数据可以进行索引,各个维度的条件数据聚合是基本的数据查询方式。

5.2.2 异常规则匹配系统后台

当某种情况下,某些类的实例不该出现在内存中,否则就是有问题,且需要根据引用链来分析问题出在哪儿。
当我们收到了某个对象出现在内存中时,需要判断它是否应该存在,客户端会有运行参数上传,如是否正在看视频,是否正在刷信息流等等。
线上后台系统需要让开发工程师来针对引用链类型进行规则的设定,同时开发工程师还要在SDK的接入配置上作相应的设置。比如用户没有停留在截屏编写的场景下,那么XXXScreenShotView类所持有的Bitmap就不应该存在,如果存在就是不合理;这时候开发工程师还需要在代码里加入类似代码,镜像分析后,参数isScreenShotFeatureActive就会发到服务端,服务端根据isScreenShotFeatureActive的值来判断XXXScreenShotView类持有的Bitmap存在的合理性:

    public void onScreenShotFeatureActive() {
        MemoryAnalyzerSDK.putStatusParams("isScreenShotFeatureActive", "true");
    }

    public void onScreenShotFeatureInactive() {
        MemoryAnalyzerSDK.putStatusParams("isScreenShotFeatureActive", "false");
    }

6 功能缺点和未来发展方向

6.1 缺点

  • 无法监控并分析动态内存申请记录。目前ART虚拟机上的兼容性问题比较严重。
  • 不支持纯Native内存的内存管理。目前还是有很多App,尤其是很多影音视频相关的App都用到了大量Native内存,Native内存同样重要。

6.2 未来发展方向

  • 完善异常匹配规则体系
  • 探索Native内存分析机制
  • 探索建立实验室环境下基于自动化测试场景的多切面镜像diff异常判断体系

7 附录

上报的JSON片段示例。这个引用链表示了一个浏览器内核的JNI对象WebViewU3Adapter间接持有了一个父UI类ShareLoginWindow,又持有了一个Bitmap。原因是WebView没有被destory掉,果然,代码实现中少了一个判断释放的时机点。

{
        "type": "com.xxx.webkit.il", // WebViewU3Adapter
        "field": "mParent",    
        "id": 319660032,
        "obj_size": 1516
}, {
        "type": "android.widget.FrameLayout",
        "field": "mParent",
        "id": 319921152,
        "obj_size": 581
}, {
        "type": "android.widget.LinearLayout",
        "field": "mParent",
        "id": 319920128,
        "obj_size": 612
}, {
        "type": "com.xx.framework.ap", // DefaultWindow$1
        "field": "a",
        "id": 330037248,
        "obj_size": 580
}, {
        "type": "com.xx.browser.business.sharesend.k", // ShareLoginWindow
        "field": "a",
        "id": 330036224,
        "obj_size": 682
},
//......................................................
 {
        "type": "com.xx.browser.business.picview.ag", // PicViewerContanier
        "field": "mChildren",
        "id": 328859648,
        "obj_size": 676
},{
        "type": "android.graphics.drawable.BitmapDrawable",
        "field": "mBitmapState",
        "id": 318958016,
        "obj_size": 68
}, {
        "type": "android.graphics.drawable.BitmapDrawable$BitmapState",
        "field": "mBitmap",
        "id": 319144704,
        "obj_size": 54
}, {
        "type": "android.graphics.Bitmap",
        "field": "",
        "id": 315224608,
        "obj_size": 47,
        "ext_size": 8294400,
        "inner_value": [{
                            "name": "mBuffer",
                            "value": "null"
                   }, {
                            "name": "mHeight",
                            "value": "1920"
                   }, {
                            "name": "mWidth",
                            "value": "1080"
                   }
        ]
}

7 结语:

因为该方案是笔者在供职阿里时开发,属于阿里内部开源项目。源码无法对外公开,如有疑问请联系hjw3156@126.com

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 153,677评论 23 675
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 117,115评论 15 132
  • 再一次夜渡迷津, 听见那尘世间花开的独吟, 仿佛是裂帛的声音, 然后浮沉起执着的心, 直到生命殆尽。
    隐花枝阅读 64评论 0 2
  • 做个有温度的女子,知世故而不世故。
    帅萌_ccaf阅读 57评论 1 1
  • 推开一扇窗 感受着茶香 凝聚千年的精华 暖涌我心房 你制茶的身影 斜射一缕阳光 高大身影渐渐没入脑海流淌 谱了一首...
    清梦飞扬阅读 129评论 4 4