monkey 离线脚本这个标题,放在多数人的聊天框里可能第一反应是某个安卓工具的偷懒玩法,但真正做稳定性测试的人看到应该会心一笑。这里说的其实就是一套可以脱离人工盯屏、在没有交互式操作的情况下,自动完成压力测试注入、运行监控、日志收集和结果归类的完整流程。我最早接触 monkey 是在几年前刚上手 Android 测试的时候,那时候跑个压力测试就是直接敲一行adb shell monkey 50000,然后盯着终端滚屏,滚完数一下有没有崩溃,再手动去设备里拉日志。这种玩法放到版本回归、发布前的稳定性验证、甚至 CI 流水线里完全没法用,人不可能陪着设备跑三五个小时,更不可能同时盯着几十台机器。真正把这件事做好,靠的就是把 monkey 跑法和日志策略封装成一套离线脚本,让设备脱离人工以后自己完成一切。
这篇文章就按照我从需求拆解、命令研究、脚本架构、实操落地到踩坑记录这条线,把一套完整的 monkey 离线脚本怎么设计、怎么写、怎么排查问题都展开聊聊。适合刚入行的测试新人、被重复回归折磨的自动化测试同学,以及想在持续集成体系里加入稳定性测试能力的团队参考。
1. 项目解读:离线跑 monkey 到底在解决什么问题
1.1 为什么非要把 monkey 变成“离线”的
monkey 是 Android 系统自带的一个命令行工具,全名叫 UI/Application Exerciser Monkey,作用是向设备发送大量的伪随机事件流,比如点击、滑动、旋转、缩放、系统按键等,用来验证应用长时间运行会不会崩溃、会不会无响应。这个工具本身的用法非常简单,但真正投入到实际项目中,你会发现几个绕不开的痛点。
第一个痛点是时间成本。常规回归测试可能要求跑 5 万、10 万甚至 20 万条随机事件,按一般设备的速度跑下来,至少需要两三个小时到一晚上。你不可能一直在工位上盯着屏幕,更不可能在半夜爬起来做日志归档。只要把执行和日志收集做成了自动离线脚本,下班前挂上任务,第二天早上直接看报告就行,设备在跑,人在睡觉,互不干扰。
第二个痛点是日志的有效性。手动跑 monkey 最容易犯的一个错误是崩溃发生时没有抓到现场日志。monkey 崩溃通过 logcat 可以看到堆栈信息,但 logcat 缓冲区是滚动的,如果你没有提前做好过滤和归档,等到跑完再去翻日志,最早的崩溃信息早就被冲掉了。离线脚本的好处在于,它能实时监听崩溃事件,一旦发现异常就立刻抓取adb bugreport、logcat以及/data/anr/目录下的 trace 文件,把现场固定下来。
第三个痛点是可复现性。monkey 最大的特点是随机,但随机不代表无法控制。每条随机事件序列都基于一个 seed 值生成,只要使用相同的 seed 和相同的命令参数,就能在另一台设备上复现同样的操作序列。离线脚本的核心价值之一,就是把 seed、事件数、事件百分比固定下来,保证每次回归的随机序列可追溯、可复现。不然这次的崩溃是随机事件击中某个深藏 bug 导致的,你下次随便跑跑不出来,这个 bug 就会被漏掉。
1.2 离线脚本适合哪些场景
从场景划分来看,monkey 离线脚本最常用的有三类用途。
一类是版本发布前的稳定性验证。新版本提测后,测试团队拿一台或一组设备,在构建产物上跑一整夜的高强度 monkey,观察有没有内存泄漏崩、ANR 频发、IPC 异常这类问题。这类场景对日志完整度和结果分类要求很高,因为需要快速定位是哪个模块引发的异常。
另一类是竞品对比和回归监控。有的团队会把某应用的历史版本、竞品版本放到同一批设备上,用相同 seed、相同事件流跑同样的时间,然后对比各自的崩溃率、ANR 次数、内存占用曲线。这种对比测试如果靠人肉操作几乎不可能完成,而脚本化之后只需要在配置里切换 APK 路径和包名即可。
再有一类是 CI/CD 流水线的集成。很多团队已经把 monkey 离线脚本作为自动化测试任务的一部分,打包阶段完成后自动触发一轮烟雾级的 monkey 测试,例如跑 1 万事件,如果崩溃率超过阈值,流水线直接标红。这种场景下脚本不需要交互,只需要一个命令行入口、一个固定的退出码和一份机器可读的报告。
2. 技术底座:把 monkey 命令参数吃透
2.1 参数不是随便填的,先理解玩法的可组合性
monkey 的命令格式虽然简单,但参数组合非常多,不同组合的测试目的完全不同。很多新手喜欢直接adb shell monkey 50000,把默认参数跑满,最后崩溃一堆,也不知道是哪个动作触发的。我建议在做离线脚本之前,先把下面这张常用参数表过一遍,按真实需求去选。
| 参数 | 作用 | 实际用法 |
|---|---|---|
-p | 指定包名 | -p com.demo.app限定只测试目标应用,不写会整个系统乱跑 |
-s | 指定种子值 | -s 1687保证随机序列可复现 |
--throttle | 事件间隔毫秒 | --throttle 300表示每个事件之间停顿 300ms |
-v | 日志详细级别 | -v -v -v输出最详细的事件、崩溃信息 |
--pct-touch | 触摸事件占比 | --pct-touch 40设置触摸事件占 40% |
--pct-motion | 滑动事件占比 | --pct-motion 25 |
--pct-appswitch | 应用切换占比 | --pct-appswitch 10模拟用户后台切换 |
--pct-syskeys | 系统按键占比 | --pct-syskeys 5模拟 Home、返回等按键 |
--ignore-crashes | 崩溃后继续运行 | 遇到崩溃不中止,跑完整个事件流 |
--ignore-timeouts | ANR 后继续运行 | 遇到 ANR 不中止 |
--kill-process-after-error | 出错后杀死应用进程 | 设置后下次事件从新进程开始 |
--monitor-native-crashes | 监听原生层崩溃 | 监控 so 层崩溃需要加上 |
--bugreport | 崩溃时自动抓取报告 | 每次崩溃自动尝试抓取adb bugreport |
--dbg-no-events | 不发送事件,仅启动应用 | 可用于初始化安装验证 |
这些参数看起来零散,但是组合起来就是测试策略。比如我需要做一次偏向用户操作习惯的标准回归,我会把触摸和滑动压到 60% 以上,同时留一部分给应用切换和系统按键,来模拟真实用户频繁切换页面的过程。如果我是偏向做异常状态验证,比如来电打断、低电量提示这类场景,就要调高--pct-syskeys和--pct-appswitch的占比,同时配合其他指令。
2.2 用 seed 锁定随机序列,把概率变成可复现
seed 可以说是 monkey 测试里最容易被忽略但是最重要的参数。我的经验是,任何一次正式测试都要显式指定 seed。为什么要这么做?因为 monkey 的随机事件发生器是一个伪随机算法,给定相同的 seed,它生成的事件序列是相同的。种子不同,跑的路径完全不同,很多崩溃只有特定的种子序列才能触发。
举个例子。假设当前版本在手势滑动某个页面时偶发崩溃,但你随机跑 10 万事件没有复现,这时候最好的做法不是加大数量盲目撞概率,而是保住当前现场的 seed。现场日志里会记录当时用的 seed 值,下次直接用这个 seed 搭配相同的参数和包名版本重新跑,大概率可以复现问题,然后交给开发定位。离线脚本里必须把这个 seed 自动化落盘,并且报告里带上有用的现场索引。
我在工作里习惯约定 seed 的生成策略:如果是日常轮询回归,用固定的常用 seed,比如 10086,保证每轮测试事件序列一致,方便横向对比;如果是独立验证测试,使用时间戳生成 seed,避免每次跑的都是同一条路径,发现问题的覆盖面更广。无论哪种策略,最后的报告里一定要能追溯到 seed 和完整命令。
2.3 事件百分比设置的思路,不是拍脑袋
事件百分比直接决定测试压力作用在哪些交互行为上。很多人偷懒直接不写--pct-*,结果系统默认的事件分布跟你期望的完全不一样。默认 monkey 的触摸、滑动等事件占比是各占一部分,但系统按键和导航事件也占有相当比例,实际体验下来会感觉操作很“乱”,容易误触一些系统界面。
我的建议是先用执行任务表把期望的事件分布写清楚,再换算成百分比。假设我要做一次电商类应用的稳定性验证,用户行为主要集中在浏览商品、滑动列表、点击加购、切换 Tab、偶尔退出到桌面。那么对应的事件模型,我会设置触摸 45%、滑动 30%、应用切换 10%、系统按键 5%、其他事件(缩放、旋转等)10%。这样跑出来的路径比较接近真实用户的操作节奏,而不是像无头苍蝇一样在系统各个页面乱跳。
如果目标是验证多进程或者后台服务稳定性,我会降低触摸和滑动的占比,提高系统按键和应用切换占比,模拟用户反复切后台、唤起其他应用,观察目标进程是否被杀、是否产生 ANR。不同测试目标要匹配不同的参数模板,这套模板要提前沉淀在离线脚本的配置文件中,而不是每次临场手敲。
3. 脚本设计:拆成模块之后,逻辑一下就清楚了
3.1 离线脚本应该具备哪些能力
很多人以为一个 monkey 离线脚本就是一行adb shell monkey加一个日志重定向,这个理解太浅了。真正能上生产的脚本,至少应该具备下面几项能力。
第一是设备连接管理。要能自动检测连接的设备,支持指定序列号、多设备并发、设备断开后的重试逻辑。第二是前置条件处理。安装指定 APK、设置权限、清理应用数据、关闭系统自动更新等干扰因素。第三是 monkey 执行控制。包括命令构造、事件数量、seed、超时控制、执行状态统计。第四是日志采集与归档。实时抓取 logcat、崩溃栈、ANR trace、bugreport等,按时间戳和场景目录归档。第五是报告生成。用统一格式输出执行概要、崩溃列表、崩溃详细信息、内存趋势等,方便后续人工判断和团队周报。
基于这个清单,我在实际项目里通常把离线脚本拆成两个大的部分:负责跑流程的外层控制脚本,和负责数据后处理的 Python 分析模块。外层控制脚本用 shell 或者批处理编写,在 Linux 执行机上负责多设备调度、超时控制、简单轮询、日志粗分类;Python 部分负责解析logcat里的崩溃关键字、聚合 ANR 信息、生成可读的 HTML 文本报告。这套结构的好处是逻辑清晰,外壳坏了可以直接改壳,分析逻辑变更不需要动执行层。
3.2 日志目录设计和文件名规则的讲究
日志目录是我踩过最多坑的地方。没有一套规范的目录,日志采集回来以后根本没法快速定位问题。我现在的标准做法是,在每个设备的测试根目录下按设备序列号_日期_测试轮次建目录,内部再细分子目录。
output/ 192.168.1.10_20250614_001/ installd.log # 安装与前置步骤日志 monkey.log # monkey 标准输出 logcat_main.log # 主 logcat 全量日志 logcat_crash.log # 实时过滤的崩溃日志 events.log # monkey --bugreport 生成的事件细节 traces/ # ANR trace 文件 001_crash_com.demo.app.log 001_anr_com.demo.app.log report.txt这个结构的好处是,后续无论是人工排查还是写自动化分析工具,都能基于确定的路径找到对应文件。文件名加了序号后,崩溃发生的顺序一目了然。我曾经遇到一个团队把崩溃日志全部堆到一个目录里,文件名又是纯时间戳,排查问题时翻了整整一小时,效率极低。
3.3 崩溃监听和 ANR 采集要主动抓,别只依赖重定向
这是整个离线脚本里技术含量最高的部分。直接把adb shell monkey的输出重定向到文件只是最基础的日志保存,真正关键的是实时日志采集。我通常会在脚本里同时开一个后台循环,持续执行adb logcat -v threadtime抓取实时日志,并且在抓取的同时用关键词过滤崩溃信息。
常见的关键词包括FATAL EXCEPTION、ANR in、am_anr、am_crash、嵌入式场景还要看SIGSEGV、SIGABRT这类信号量。一旦检测到崩溃,立即复制当前logcat缓冲区的关键片段,同时尝试从设备上拉取/data/anr/目录下新的 trace 文件。这里要注意,抓取 ANR 文件需要 root 权限或者使用adb bugreport来间接获取,直接在 shell 里adb pull /data/anr/经常会失败,这也是后面问题排查要展开的部分。
这样的主动监听方式比等 monkey 跑完再统一分析要好得多,因为崩溃现场有大量瞬时信息,比如事件序列、内存快照、CPU 状态,过了这个时机就再也拿不到了。
4. 实操过程:一份能直接上手的 monkey 离线脚本
4.1 前置准备与运行环境
动手写脚本之前,先把执行环境准备好。macOS 或者 Linux 上安装好 adb 工具,Windows 上建议在 PowerShell 或者转换为 shell 兼容环境运行,不过我当时直接用的 Ubuntu 执行机,配合多台 Android 设备通过 USB 或者 Wi-Fi adb 连接管理。执行前确认设备开启开发者模式,并且已授权adb调试,运行adb devices能看到所有设备。
为了让脚本可以灵活切换测试目标,我在脚本目录下放了一个config.env配置文件,集中管理包名、APK 路径、事件数、seed、事件百分比、执行时长上限这些参数。离线脚本的灵活性全部归功于把可变项收敛到配置文件中,不用为了每类测试都去改代码本体。
PACKAGE=com.demo.app APK_PATH=./app-release.apk EVENT_COUNT=50000 SEED=10086 THROTTLE=300 TOUCH_PCT=45 MOTION_PCT=30 APP_SWITCH_PCT=10 SYSKEYS_PCT=5 TIMEOUT_SEC=72004.2 脚本主体:shell 部分的管理与控制逻辑
外层脚本主要完成设备选择、APK 安装、日志目录准备、monkey 命令构造、超时控制和基础日志轮转。这里有一个关键点是,执行 monkey 命令时要使用timeout命令包裹,防止因为某些异常事件导致进程挂起而无限等待。monkey 自身有事件数量限制,但事件之间如果某个--throttle值设置得太小,遇到页面动画卡顿,事件注入速度会受影响,实际耗时会比你预想的长很多。超时控制必须要有,否则 CI 任务会把执行机长时间占住。
#!/bin/bash source ./config.env ADB="adb" DEVICE_ID=$1 if [ -z "$DEVICE_ID" ]; then echo "Usage: $0 <device-id>" exit 1 fi # 输出目录构建 BASE_DIR="./output/${DEVICE_ID}_$(date +%Y%m%d_%H%M%S)_${SEED}" mkdir -p "$BASE_DIR/traces" run_cmd() { "$ADB" -s "$DEVICE_ID" $@ } # 安装与系统预处理 run_cmd install -r "$APK_PATH" > "$BASE_DIR/installd.log" 2>&1 run_cmd shell am force-stop "$PACKAGE" run_cmd shell pm clear "$PACKAGE" # 构造 monkey 命令参数 CMD="$ADB -s $DEVICE_ID shell monkey -p $PACKAGE -s $SEED \ --throttle $THROTTLE \ --pct-touch $TOUCH_PCT \ --pct-motion $MOTION_PCT \ --pct-appswitch $APP_SWITCH_PCT \ --pct-syskeys $SYSKEYS_PCT \ --ignore-crashes \ --ignore-timeouts \ --monitor-native-crashes \ -v -v -v \ $EVENT_COUNT" # 启动实时 logcat 采集 run_cmd logcat -c run_cmd logcat -v threadtime > "$BASE_DIR/logcat_main.log" 2>&1 & LOGCAT_PID=$! # 执行 monkey,加超时控制 timeout "$TIMEOUT_SEC" $CMD > "$BASE_DIR/monkey.log" 2>&1 MONKEY_EXIT=$? # 停止 logcat 采集 kill $LOGCAT_PID 2>/dev/null # 基础崩溃关键词提取 grep -n -E "FATAL EXCEPTION|ANR in" "$BASE_DIR/monkey.log" > "$BASE_DIR/crash_hint.log" echo "$MONKEY_EXIT" > "$BASE_DIR/exit_code.txt" exit $MONKEY_EXIT这段 shell 脚本的思路很直接,安装、清数据、构造命令、后台日志、超时执行、崩溃关键词初筛。执行完以后,立即把退出码和崩溃摘要存到固定位置,方便后续汇总。这里--ignore-crashes和--ignore-timeouts我刻意加上,因为目标不是遇到一次崩溃立刻中止,而是让 monkey 持续跑完整个压力周期,把所有问题都暴露出来。如果你想尽早暴露并停止,可以把这两个参数去掉。
4.3 Python 后处理:从日志到可读报告
Shell 负责跑,Python 负责分析和归档。这部分我写了一个简单的日志解析器,读取crash_hint.log、monkey.log和logcat_main.log,按崩溃类型分类,并尝试提取崩溃发生的具体堆栈片段。核心逻辑是,在logcat_main.log里定位FATAL EXCEPTION出现的位置,然后从该行往后找一段包含at com.demo.app的调用栈,输出到独立文件。
import os import re import sys from collections import defaultdict def parse_monkey_log(monkey_log_path): lines = open(monkey_log_path, encoding="utf-8", errors="ignore").readlines() result = {"crashes": 0, "anr": 0, "finished": 0, "events": 0} for idx, line in enumerate(lines): if "FATAL EXCEPTION" in line or "Process:" in line: result["crashes"] += 1 if "ANR in" in line: result["anr"] += 1 if "Events injected" in line: m = re.search(r"Events injected: (\d+)", line) if m: result["events"] = int(m.group(1)) if "Monkey finished" in line: result["finished"] = 1 return result def extract_crash_stack(logcat_path, output_dir): lines = open(logcat_path, encoding="utf-8", errors="ignore").readlines() count = 0 for idx, line in enumerate(lines): if "FATAL EXCEPTION" in line or "ANR in" in line: count += 1 file_name = f"{count:03d}_crash.txt" target = os.path.join(output_dir, file_name) with open(target, "w", encoding="utf-8") as f: end = min(idx + 80, len(lines)) f.writelines(lines[idx:end]) print(f"[extract] {file_name}") return count if __name__ == "__main__": base = sys.argv[1] stats = parse_monkey_log(os.path.join(base, "monkey.log")) crash_files = extract_crash_stack(os.path.join(base, "logcat_main.log"), os.path.join(base, "traces")) with open(os.path.join(base, "report.txt"), "w", encoding="utf-8") as f: f.write(f"crashes: {stats['crashes']}\n") f.write(f"anr: {stats['anr']}\n") f.write(f"events: {stats['events']}\n") f.write(f"finished: {stats['finished']}\n") f.write(f"extracted_crash_files: {crash_files}\n")这段代码有个细节:崩溃栈片段提取不是简单地把整段日志都复制到崩溃文件,而是只取崩溃点之后的 80 行。因为 logcat 在崩溃发生后往往紧接着出现大量新事件,太多无关日志会干扰定位。80 行是我在实际使用中觉得比较合适的窗口,基本覆盖从FATAL EXCEPTION到at com.demo.app的完整 Java 堆栈,又不至于把后续无关日志带进来。
4.4 多设备并发的调度思路
生产环境里很少只跑一台设备,多设备并发是标配。最简单可靠的并发方案是循环逐个设备执行上面那个 shell 脚本,但这里有个问题:adb 多设备命令不加-s参数时,它会报错或者默认选择第一台设备,造成执行混乱。需要在最外层写一个调度脚本来遍历adb devices输出的设备列表,并为每个设备启动一个后台任务。
#!/bin/bash adb devices | awk 'NR>1 {print $1}' | while read dev; do if [ "$dev" != "" ]; then echo "start monkey on $dev" ./run_monkey.sh "$dev" & fi done wait echo "all device tasks completed"多设备并发时要额外注意执行机的串口带宽和 USB 资源。如果设备数量较多,建议给每台设备的 adb 连接加一个server级配置,同时限制同时拉取日志的数量。日志拉取是 IO 密集操作,我曾经一次并发 16 台设备拉取 bugreport,结果把执行机的磁盘 IO 打满,所有任务的写入速度都明显下降。后来我在调度脚本里对设备分批处理,同一批次最多 4 台进入日志拉取流程,其他设备先等待,整体效率反而更高。
5. 常见问题与排查技巧实录
5.1 monkey 跑完了,日志目录里却什么都没有
遇到这个问题,第一反应不是脚本出问题了,而是先看设备是否真的执行了命令。很多情况下是因为安装失败或者包名不匹配导致 monkey 根本启动不了目标应用。检查installd.log是否有Success,再手动执行一下adb shell monkey -p 包名 1看返回输出。我遇到过一次比较隐蔽的问题是,测试 APK 和当前设备 Android 版本的 targetSdk 冲突,安装能够成功,但启动时直接崩溃,monkey 还没开始注入事件应用就已经死了。这时候 monkey.log 里通常会有Error launching activity的字样。
另一个常见原因是 logcat 后台进程没有正确启动。你要确认脚本里adb logcat是否用了后台&符号,以及执行完 monkey 后是否把该进程 kill 掉。如果上一轮的 logcat 进程没清理,当前轮的日志会写到上一个进程对应的文件,你打开当轮目录自然什么都看不到。
5.2 崩溃日志抓到了,但 ANR trace 始终拉取失败
这是没有 root 的设备上最常见的坑。Android 系统的/data/anr/目录权限受限,普通 adb 权限无法直接访问。很多测试员在脚本里写adb pull /data/anr/然后发现 Permission denied,就以为拿不到 ANR 现场了。
其实可以用两个替代方案。第一个是在 monkey 命令里带--bugreport参数,崩溃或 ANR 时可以自动触发 bugreport 收集/data/anr/下的信息。第二个是升级为adb bugreport手动采集,它能把系统所有诊断信息打包到执行机,里面通常包含traces.txt。我的离线脚本在检测到 ANR 关键词时,会额外调用一次adb bugreport来导出系统 snapshot,虽然耗时较长,但采集到的信息量比单独拉 ANR 目录全面得多。要注意的是,adb bugreport本身耗时可能超过一分钟,如果 ANR 触发频繁,采集计划要做好超时和频率控制,避免日志还没拉完设备已经被下一轮事件继续冲击。
5.3 monkey 命令卡住不动,超时后杀不掉进程
有时候timeout虽然能杀掉 adb 命令,但设备里的 monkey 进程可能还在跑,继续消耗系统资源。此时你要在 shell 脚本的超时处理分支里补一步清理设备端进程:
adb shell pkill -f monkey
但注意,pkill不是所有 Android 设备都内置。更通用的做法是,先查 monkey 进程的 PID,再adb shell kill -9 PID。我一般会在脚本里封装一个函数,在timeout返回非零退出码时,主动执行一次进程清理。这样下一轮测试开始前设备是干净的,不会出现前一个 monkey 的事件流还留在系统里,干扰新一轮测试的情况。
5.4 崩溃率很高,但人工复看日志发现都是同一个误报
monkey 崩溃日志里的内容不都是应用代码的 bug,也可能来自系统组件。比如某些设备在低内存条件下会触发系统桌面重启,日志里会看到systemui的 FATAL EXCEPTION,但它跟目标应用无关。离线脚本的分析模块一定不能被所有崩溃关键词骗到,要在报告里按进程名分类统计。
我的做法是在extract_crash_stack里把崩溃进程名也解析出来,与目标包名做匹配。只把与目标包名相关的崩溃标记成有效崩溃,系统级崩溃单独列一份统计便于处理。这样做能大幅减少报告噪音。实际项目里我见过有团队因为统计了系统桌面崩溃,误报率高达 60%,开发复查后全是无效单,后面自动化可信度受到很大影响。
| 现象 | 排查方向 | 处理建议 |
|---|---|---|
| 日志目录为空 | adb 连接、安装失败、包名不匹配 | 检查 installd.log、单步手动执行 |
| ANR 无法拉取 | /data/anr 权限不足 | 使用 --bugreport 或 adb bugreport 兜底 |
| monkey 卡死 | 事件流异常、低内存 | timeout 包裹,pkill 进程清理 |
| 崩溃率虚高 | 系统进程崩溃被统计 | 按进程名过滤,只统计目标包 |
| 多设备并发拉日志慢 | USB/磁盘 IO 瓶颈 | 分批拉取、限制同批设备数量 |
5.5 报告里的退出码含义要准确解读
monkey 命令退出码不是随随便便看的。按照我的经验,退出码 0 表示 monkey 执行完毕,退出码非 0 则多半是发生了中断或者错误,但这里有个细节:即使加了--ignore-crashes,monkey 仍然可能因为其他异常返回非零退出码。例如设备存储满导致日志写不进去、adb 连接断开、设备进入休眠锁屏等,都会让命令提前结束。你不能简单认为退出码非 0 就是被测应用出现了崩溃,应该结合monkey.log里的Monkey aborted due to error这类文本做二次确认。
这里分享一个排查技巧:在日志里搜索** Monkey aborted due to error后面紧跟的原因字段,它通常直接告诉你是什么触发了中断。我之前遇到过几次设备屏保导致事件无法注入,就是通过这个字段定位到的。处理方法是提前设置设备亮屏时间无限长,并在脚本执行前通过svc power stayon true强制保持唤醒。
5.6 顺手总结三个独家小技巧
脚本里所有临时文件命名都要带设备和 seed,别只带时间戳。多设备并发状态下,不同设备的日志如果文件名重复,输出目录一合并直接互相覆盖,这类事故发生过不止一次。
monkey 的阈值和判定标准要固化,不要每次跑完再人肉定标准。最好的方式是在配置里写清楚“崩溃次数大于等于 1 就标红”,同时允许人工在后处理报告里备注无效崩溃。自动化结果如果不带人工复核通道,时间长了大家会对红色告警麻木,最终这个工具会被团队弃用。
日志分析时不要忽略Events injected这个字段。它显示实际注入的事件数量。如果配置问 5 万事件,但它只注入了 1 万就收到系统中断,说明测试并没有达到预期压力强度,即使是退出码 0 也要引起注意。
6. 这套脚本后续还能往哪个方向扩展
monkey 离线脚本做到一定程度之后,瓶颈往往不在脚本本身,而在如何把结果和研发流程打通。我目前比较喜欢的方向是把离线脚本的产出标准化为 JSON 格式,然后对接团队内部的看板系统,每次测试完成自动推送一条消息,包含崩溃摘要、趋势变化、复跑入口。这样稳定性测试的结果不只是跑完自己看看,而是能实时暴露给整个团队。
另一个扩展方向是接入内存监控。压力测试如果只看崩溃和 ANR,很多内存泄漏问题会被漏掉,因为这些泄漏往往不会在短时间内触发崩溃。可以在外部脚本里周期性执行adb shell dumpsys meminfo 包名,记录内存曲线,跑完以后对照事件流定位是哪个阶段导致的内存激增。这块我当时没有写进第一版脚本,后面单独花了一个迭代周期补上,效果很好。
如果你所在的团队已经有统一调度平台,还可以把这套 shell 脚本封装成可执行任务,做成参数化驱动。比如传入 APK 下载地址、包名、测试时长,平台自动分配设备和存储空间。脚本本身保持简单,把复杂调度交给平台,脚本只负责“跑设备相关的脏活累活”。
回到最开始的那个问题。monkey 离线脚本本质上是把测试员从重复劳动里解放出来的工具,但工具能发挥多大价值,取决于使用者对 monkey 参数、日志语义和设备行为的理解深度。能跑通一条命令很容易,能把崩溃现场完整地带回来并在团队协作中形成闭环,才算真的把离线脚本做成了项目里靠谱的基础设施。