第4章:动态交付——CI/CD与自动化测试在ANR治理中的应用

说实话,很多人觉得ANR治理就是出了问题时,拿个trace文件分析一下,改改代码。但我在几个大厂项目里待过之后,发现事情没那么简单。

你想想看,一个功能团队几十人,每天提交上百次代码。等用户反馈ANR再处理?黄花菜都凉了。所以我们必须把ANR治理往前移,移到代码还没合并、还没发布的时候就把它拦住。

这就引出了我们今天要聊的核心——动态交付。说白了,就是用持续集成(CI)、持续交付(CD)和自动化测试,打造一条ANR的“检测-预防-快速修复”流水线。

为什么动态交付能治ANR?

传统开发流程里,ANR往往等到提测甚至灰度后才暴露。修复成本高、周期长。而动态交付的核心是:

  • 每一次代码提交都做质量门禁
  • 每一次构建都自动执行ANR相关检查
  • 每一次发布都有自动化测试兜底

我个人习惯把这条流水线叫做“ANR防火墙”。下面我们一个个拆开来看。

持续集成(CI)——把ANR扼杀在萌芽

CI要做的事很简单:代码一提交,服务器立刻拉下来编译、跑检查。在ANR治理方面,我们主要做三件事:

检查项工具/方法拦截目标
静态代码分析Android Lint + 自定义Detector在主线程做IO、网络请求等耗时操作
线程模型检查自定义Gradle插件Handler使用不当、锁持有时间过长
依赖分析Dex分包、ProGuard R8规则方法数超限导致编译卡顿(间接引发ANR)
我的一点经验:别只依赖官方Lint。我在项目里写了一个Detector,专门检查在BroadcastReceiver的onReceive里有没有startActivity或showDialog,这些操作在主线程干很容易ANR。效果立竿见影。

CI Pipeline示例(伪代码):

pipeline {
    agent any
    stages {
        stage('Checkout') { /* 拉代码 */ }
        stage('Lint') {
            steps {
                script {
                    // 如果检测到主线程耗时操作,直接失败
                    def result = sh(script: './gradlew lint', returnStatus: true)
                    if (result != 0) {
                        error("ANR风险代码被拦截,请修复后重新提交")
                    }
                }
            }
        }
        stage('Build') { /* 编译 */ }
        stage('UnitTest') { /* 跑单测 */ }
    }
}
注意:CI里Lint检查可以设成“警告”级别或“错误”级别。建议关键项(如主线程网络请求)直接报错,阻止合并。次要项(如资源命名规范)做警告,留到Code Review里讨论。

持续交付(CD)——让ANR修复跑得更快

CD的核心是自动化部署。在ANR治理上,CD主要解决两个问题:

  • 快速分发带修复的版本——发现ANR后,从提PR到灰度用户拿到修复包,时间从几天缩短到几小时。
  • 自动化灰度监控——每次发版,CD管道自动开启ANR数据采集,对比基线。如果新版本ANR率超过阈值,自动触发回滚。

我见过一个项目,因为CD配上自动化回滚,硬生生把一次线上严重ANR事故从影响10万用户缩小到只有200人。嗯,这就是CD的魅力。

CD Pipeline关键步骤:

  1. CI通过后,CD自动构建Release包。
  2. 自动签名、混淆、对齐。
  3. 上传到内测分发平台(如蒲公英/自家平台)。
  4. 推送给灰度设备池,自动安装并跑Monkey+ANR监控。
  5. 如果ANR率 > 0.1%,CD自动打回,不进入全量发布。
这里有个坑:我曾经遇到一个情况——CD管道里只监控了主线程ANR,但服务端Hook没包括子线程的ANR(比如Watchdog触发)。结果灰度数据全绿,上线后全面崩盘。所以CD里的ANR监控一定要覆盖“所有线程卡死”的场景,不光是系统ANR。

自动化测试——用机器代替人肉排查

CI/CD管道里,测试是承上启下的关键。针对ANR,我们至少需要三类自动化测试:

测试类型工具检测内容
UI自动化Espresso / UI Automator每步操作后检查UI线程是否响应超时
压力测试Monkey + blockcanary长时间随机操作,捕捉主线程卡顿超过5秒
专项ANR测试自定义脚本 + 系统trace模拟低内存、磁盘IO满、网络慢等异常场景

说个具体的例子。我在做智能家居App时,发现某个页面在切换网络时ANR频发。普通Monkey很难复现。后来我们在测试脚本里加了模拟网络切换的步骤:

// 伪代码:在Espresso测试中模拟网络切换
@Rule
public ActivityScenarioRule<MainActivity> rule = ...;

@Test
public void testNetworkSwitchANR() {
    // 1. 进入联网页面
    onView(withId(R.id.btn_load_data)).perform(click());
    // 2. 模拟网络断开
    toggleWifi(false);
    // 3. 等待2秒后恢复网络
    SystemClock.sleep(2000);
    toggleWifi(true);
    // 4. 立即检查ANR
    assertThat(device.isInCall(), is(false)); // 实际用更精细的ANR检测
}
小建议:把ANR检测做成一个插件,接入CI/CD。比如用BlockCanary监听主线程卡顿,超过阈值时自动截取堆栈上传到CI存档。这样每次构建都能看到是否有潜在的ANR风险。

把流水线串起来:一张图看清全貌

下面这张图汇总了CI/CD/自动化测试在ANR治理中的协作关系。建议你保存下来,画板子或者做汇报都能用上。

动态交付ANR治理流水线 动态交付 ANR 治理流水线 代码提交 CI: Lint+ANR检查 自动化ANR测试 CD发布 灰度ANR率实时监控 超标回滚 全量发布 数据反哺 & 规则优化 说明: 1. 每次代码提交触发CI 2. 测试通过才允许CD 3. 灰度监控自动决策

看懂这张图了吗?从左到右是流水线的主流程:代码提交 → CI检查 → 自动化测试 → CD发布 → 灰度监控。如果灰度ANR率超标,自动回滚;如果正常,才全量发布。最下面那条虚线的反馈回路,把线上数据带回到CI规则里,形成闭环。

落地踩过的坑

最后分享一个实际教训吧。我们曾经把CI里的Lint检查设得太严格,任何warnings都直接阻断构建。结果开发同学为了赶快合并代码,直接加 //noinspection 忽略所有警告。ANR检测形同虚设。

正确的做法是:

  • 对高风险项(如主线程IO、锁死锁)设为error,不可忽略。
  • 对中风险项(如handler泄漏)设为warning,不阻断构建,但记录在CI报告里,最终汇总到每周质量会议。
  • 自动化测试的ANR监测结果,直接关联到CD的门禁,只有通过才能上线。
一句话总结:动态交付不是一套工具,而是一种“左移”的思想。把ANR检测从线上移到线下、从发版移到提交、从人肉移到自动化。做到了,ANR治理就成功了一半。