“ 三角形是最稳定的结构,NR 是网易新闻的缩写,Tripod 意为三脚架,NRTripod 是一款App线上风险控制系统,旨在保障移动应用的线上稳定性。”
每当我们提及线上风险,可能首先想到的就是崩溃问题。新闻客户端目前的崩溃率是万分之一,在我们看来,这个崩溃数据是比较理想的,而其中一部分稳定性的维系,是通过 NRTripod。
图1 Free Session 占比
从最近24周的 Session 崩溃数据统计上来看,在进行了多方面包括 NRTripod 在内的整合处理后,崩溃整体曲线逐步下降,在一些发版日前后,Session 崩溃数据略有震动,但震动范围不大。
我们将时间维度转换为月份来看,Session 崩溃整体下降曲线会更加平滑。
图3 近6月 Session 崩溃数据曲线图
在 NRTripod 中,我们简单把线上风险问题大致分为四类:
- 兼容性风险:主要指系统的兼容性问题,安卓的碎片化尤其严重;
- 突发事故:指某些业务场景由于异常情况引发了线上事故,需要及时止损;
- 开发隐患:指开发过程中带来的隐患问题,这样的风险可能有比较长的潜伏期。
NRTripod 包括前中和准后端,主要为应对上述各类风险而生。NRTripod 基于新闻团队自研方案,在系统架构划分上分为开发环境中间层、生产环境风控层、系统数据收集层和后台服务集中层四层,架构图如下图所示。
静态预置风控代码,提供开发环境需要使用到的风控调试工具等。在开发环境中间层,主要提供一些便于在日常开发测试中进行调试的工具,包括补丁下发测试工具、动态配置系统(哈雷)测试工具和动态上下线调试工具。静态风险保护主要用于在开发阶段来静态预置风控代码,避免稳定频发的风险问题。生产环境风控层包括动态配置系统(哈雷)、保证动态配置系统数据到达前后的一致性的缓存命中器、可以动态开闭存在风险的第三方SDK的SDK动态上下线和动态处理APP异常崩溃情况的Crash白名单等。主要负责配置和下发数据,其中包括数据后台服务、崩溃收集服务等。
3.1 技术背景
动态配置系统是实现 NRTripod 线上风控动态配置的核心基础。其基本实现原理为 Feature Flag,即功能开关。 功能开关,可以用来控制线上功能的开启或关闭,相对应的方式为 Feature Branches。Feature Flag 允许功能在主干上进行开发,从而实现持续交付。Feature Flag 已经在诸如 FaceBook、Google 等公司中使用来持续集成开发。通过开关配置文件,下发功能的开关状态,这是其一种实现方式。对于不同的开关状态,通过条件判断语句来执行代码,以实现两种展现形式。3.2 系统组成
- 数据模型:包含动态配置系统预置支持的几种数据类型;
- 数据控制:包含本地和网络数据的加载控制,和数据一致性的控制;
- 调试器:用于在开发环境中使用 UI 界面操作动态配置数据。
图5 动态配置系统数据控制流程
由于在开发和测试过程中调试动态配置系统,mock数据的步骤较为繁琐,人工成本高,所以我们开发了动态配置系统的调试工具。通过调试工具,可以为动态配置系统提供便利,降低调试成本,动态配置系统调试工具 UI 如下图所示。
图6 动态配置系统调试工具
崩溃风控系统通过接管 Android Looper,可以动态捕获 Android 产生的线上崩溃,用最小的成本,实时处理线上大量的棘手崩溃问题,将用户损失降到最低。通过与动态配置系统的协作,我们也可以实现动态下发崩溃捕获信息。4.1 技术背景
当线程执行完可执行的代码段后,就会停止生命周期,而安卓中的主线程有 Looper.loop() 可以开启一个死循环,保证主线程的存活。通过动态配置系统,我们也可以实现动态下发崩溃捕获信息。4.2 线上数据
通过崩溃风控系统来遏制系统兼容性顽疾在线上已经有一定成效。如下代码所示的 OPPO R9 的 NPE 问题,占新闻某一版本崩溃量 10%,通过使用崩溃风控系统可以处理这一类 ROM 问题。Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference at android.os.Message.toString + 574(Message.java:574) at android.os.Message.toString + 480(Message.java:480) at android.os.Looper.loop + 187(Looper.java:187) at android.app.ActivityThread.main + 5509(ActivityThread.java:5509) at java.lang.reflect.Method.invoke(Method.java) at java.lang.reflect.Method.invoke + 372(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run + 961(ZygoteInit.java:961) at com.android.internal.os.ZygoteInit.main + 756(ZygoteInit.java:756)在预置了该部分信息捕获后,从崩溃曲线上可以看到,崩溃有明显下降,剩余少量旧版本应用还有存留崩溃。
图7 ROM 崩溃问题治理表现图
第三方SDK代码的管理和风险控制一直是每一个开发者不得不面临的棘手问题。SDK 风控系统实现基于SDK 动态上下线功能,帮助开发者解决第三方SDK管理棘手的问题。5.1 技术背景
在通过 Google ClassyShark 对网易新闻v67.0 APK 进行 Method 统计分析后,我们发现,安装包内网易新闻的 Method 占比只有 33%,而有 67% 的 Method 都是第三方 SDK 的,即使抛除 Google 官方相关 API,第三方 SDK Method 占比仍有44%,这些没有经过严格 Review 的代码,在线上存在未知的隐患问题。
图8 网易新闻 v67.0 Method 来源分析华夫饼图
5.2 实现原理
动态上下线基于功能开关和 NullObject 模式来封装第三方 SDK 的所有调用代码,动态控制 SDK 的开闭。当开启 SDK 时,执行真实 SDK 的方法,当关闭 SDK 时,执行空对象的方法。Wikipedia 对 NullObject 模式的说明如下。In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof).
NullObject模式首次发表在“ 程序设计模式语言 ”系列丛书中。一般的,在面向对象语言中,对对象的调用前需要使用判空检查,来判断这些对象是否为空,因为在空引用上无法调用所需方法。
依赖于NullObject设计模式的基础,根据Feature Flag的思想,将每个SDK的方法调用统一封装,使用NullObject设计Null SDK类,负责处理SDK下线后的操作,使用Real SDK类来处理真实的SDK方法调用,抽象接口,提供工厂类对SDK的调用对象进行创建。这种设计思路集合三种模式的特性,通过Null Object与Feature Flag的组合来实现SDK的动态上下线控制特性,而又不损失代码分支管理的功能。在保证三种方式可以共同协作的情况下,以达到持续交付、持续集成和持续部署以及动态管理SDK的目标。
图9 SDK 动态上下线系统设计思路
5.3 调试工具
SDK风控系统配有调试工具,如图所示,调试者可以通过点击开关来关闭客户端的SDK。例如,动态下线支付宝支付SDK,这样客户端就不能再跳转到支付宝支付,同时可以配置相关Toast提示信息等,如提示用户,该功能暂未开放等。或者动态下线 Fabric 监测,这样客户端就不会再向崩溃系统上报崩溃信息。具体的SDK上下线配置就是通过动态配置系统来下发的。
图10 SDK 动态上下线系统调试工具
5.4 开发工具
因为SDK动态上下线的代码可以模板化,而基于APT处理模板代码的话,对编译效率在一定程度上有折损,源码处理也不是很明了。所以,我们还发布了一款 Android Stuido 插件,用于一键自动生成动态SDK上下线的代码,以降低开发者的学习成本。通过在IDEA的插件仓库中,搜索“NR”即可找到新闻团队发布的相关插件,其中的 .NR Feature Toggle 就是 SDK动态上下线的处理插件。
图11 .NR Feature Toggle 插件
在IDE中,在需要实现SDK动态上下线功能的SDK包装类代码中,右键选择相应选项,可以一键实现SDK动态上下线代码封装。
图12 .NR Feature Toggle 插件使用
由于移动终端的碎片化等问题,导致很多历史性的崩溃让每个开发者多次重蹈覆辙,经过各种洗礼后才能规避风险存在,静态风险保护系统旨在整理出这些总是能绊倒人的石头,避免更多开发者走前人踩过的坑。6.1 技术背景
以90天为周期,通过对Android的线上风险问题统计分析,我们发现,绝大多数用户使用的系统都是 Android 9 或 Android 10,但发生在 Android 9 或 Android 10 上的风险问题占比却很低,绝大多数风险问题仍然存留在 Android 6.0 和 Android 4.4 等较低版本系统上。由此可见,较低版本的系统兼容性问题是处理线上风险的核心关注点,其中绝大多数兼容性崩溃问题的调用栈都并未出现新闻业务代码,而很多兼容性问题在网上有一部分解决方案,这说明大部分开发者对这类问题在重复踩坑。
图13 兼容性问题系统占比分析
所以,基于新闻和业界资料,我们整理了高发的风险问题,基于崩溃风控系统,我们将高风险问题统一封装成库,其核心实现机制为Crash白名单,通过Crash白名单的分级管理,可以良好地降低系统兼容性问题对应用的影响。
除此之外,其静态风险保护系统组成内还包括字节码处理器和Manifest文件保护器。6.2 Crash白名单
Crash白名单基本实现原理即为在程序中预置部分风险捕获信息,静态写在崩溃风控系统的工具库层,其整体设计结构图如下图所示。
图14 Crash 白名单设计结构图
移动端的系统中的消息处理机制如下图所示,Looper在主线程通过loop开启一个死循环,Looper这个水泵开始不断从Message队列中获取消息,而崩溃风控系统则又调用 loop 开启了一个死循环,抢夺了消息队列的操作权。主线程的崩溃都会从loop中抛出,而崩溃风控系统抢夺了原有Looper的控制器,崩溃则会从崩溃风控系统的loop调用处抛出。当我们在崩溃风控系统的loop处捕获异常,则可有目的性的控制部分崩溃对用户无感知。
图15 Crash 白名单基本原理图
在风控系统中加入一套白名单,静态写入部分多发性的崩溃问题,通过崩溃系统的筛选器,可以有目的性的选取一部分机型开启静态预置风险区。
图16 Crash 白名单核心设计图
- 第一级:ROM级,包含部分ROM频发的崩溃,覆盖面较小,适用于成熟且量级较大的产品;
- 第二级:包含普遍多发性的崩溃问题,和ROM级的碎片化问题,最具有通用性的级别,使用中高量级产品;
- 第三级:包含所有可能引发风险的量级较大的问题,适用于量级中小型的产品。
6.3 字节码处理器
即在transfrom阶段操作Java字节码插入代码,降低业务入侵,并兼容控制第三方依赖带来的风险。在字节码处理器中,我们预处理了Toast崩溃、序列化等问题。6.4 Manifest保护器
通过对 网易新闻 v67 的 AndroidManifest.xml 中的四大组件进行分析,我们发现,合并后的AndroidManifest.xml中,有58%的四大组件声明都来自第三方SDK。当第三方SDK进行变更时,AndroidManifest.xml 很有可能产生开发人员难以注意到的冗余配置,导致产生线上风险。
图17 AndroidManifest 配置来源分析
Manifest 保护器是用来检查系统清单文件中是否有失效冗余配置的工具,避免在部分ROM上,由于 清单文件中的失效配置而导致崩溃等风险问题。其实现原理主要是通过字节码处理器,在transfrom阶段,获取到合并完成的Manifest文件,通过XML解析,检测出冗余配置信息,对开发人员进行提醒,以规避风险性问题。
Manifest 保护器在实际应用场景的一个案例是,在我们处理 Android P 适配时,针对一部分 SDK 有进行升级,在升级后,部分 SDK 移除了某些组件,而由于组件化的多 Module 结构,SDK 移除的这部分组件声明还冗余在AndroidManifest.xml 中,没有被删除,上线后与部分 ROM 产生了兼容性问题,进而引发了大面积的 ClassNotFoundException 的产生,Manifest 保护器可以检测并提醒移除这些冗余无效配置,避免 SDK 升级或多种场景下带来的与 Manifest 相关的隐患问题。数据收集系统负责收集各个组件统一输出的信息,存储在本地,通过后台服务上传到服务器中,可由用户手动触发,也可以通过Push等消息服务触发数据上传,其组件结构如下所示。
李云鹏 2016年加入网易传媒,目前主要负责架构和性能优化相关工作。QCon 大会讲师,著有《移动开发架构设计实战》等书籍。