简介:这份资源是一份关于安卓自动化测试工具 Monkey 的实用文档,适合刚接触安卓测试的开发者、测试工程师,用于掌握 Monkey 压力测试的核心概念与命令行用法。资源为单个 doc 文档,压缩包仅 129KB,轻量易读。文档从 Monkey 的定义出发,说明了它通过发送伪随机用户事件流进行压力测试的机制,归纳了三个基本特征,并给出“adb shell monkey [options]”这一基础语法。在实操部分,以模拟器环境为例,完整记录了从 adb devices 查看设备、进入 shell、查看 data/data 下的应用包,再到指定包名执行 monkey -p 命令发送事件的步骤,同时总结了测试停止的三种情况。内容还提及 Monkey 可结合 JUnit、TestNG、Jenkins 等工具实现自动化与持续集成。已有 704 人学习,对想快速入门安卓稳定性测试、理解 Monkey 工具用法的人来说,是一份简明实用的参考资料。
1. 安卓自动化测试工具 Monkey:它不是点刷器,是崩溃探针
说到安卓自动化测试工具 Monkey,不少同学的认知还停留在"adb shell monkey 加个数字,让它在屏幕上乱点"。真实场景里,Monkey 是 Android SDK 自带的稳定性压测工具,它往目标应用注入的是伪随机事件流——触摸、滑动、按键、应用切换——最值钱的能力,是能在功能测试永远走不到的犄角旮旯里,把崩溃、ANR、内存问题逼出来。
适合谁用?上线前做一轮"没人盯着的随机操作"验证的移动端开发和测试都适合;刚接手一个存量项目的同学,先跑五分钟 Monkey 摸摸底线,也比直接啃代码快。这篇文章不把 Monkey 当自动点屏工具,而是把它当一台可以复现、可以量化、能接进日常回归的小型压测机来讲:从运行机制、参数配置、日志解析一路到避坑和流水线落地。
2. Monkey 的运行机制与最小命令:三分钟让 500 个事件跑起来
2.1 随机事件从哪来:seed、事件类型与盲注入
Monkey 不是一个独立安装的 App,它是设备系统镜像里自带的一个命令行程序。你通过 adb shell 把参数传进去,它就以系统进程身份往窗口系统注入事件。事件来源是一个伪随机数生成器,也就是你在日志里看到的 seed 参数:给定同一个 seed,在同一个环境下生成的"事件序列"是可复现的。这句话是 Monkey 整个使用方法的基石——如果不能复现,那随机测出来的崩溃就永远只是一次性的黑匣子。
事件类型可以分几大类:触摸事件(点击)、手势事件(滑动)、轨迹球事件(真机上基本没有这个硬件)、导航事件(方向键)、主导航事件(确认和返回)、系统按键事件(Home、音量、菜单)、应用切换事件(跳到别的应用再回来)、以及兜底的任意事件。Monkey 会按你给定的百分比,从这些类型里随机挑一种生成具体事件。触摸和手势是最像人操作的;nav 和 syskeys 则很容易把应用带离前台。
关键点是盲注入。Monkey 不去解析当前界面的 UI 层级,它不知道屏幕上是什么控件、什么状态,它只按预设比例往屏幕上丢坐标和按键事件。好处是覆盖够野:弹窗、横竖屏切换、键盘弹出中的点击,它都可能闯进去。代价是它不知道点到了哪里,一个需要输入文本的登录页可能被反复点几百次都进不去。理解这一点,才能理解后面所有参数的真实含义——参数本质上是在"随机"和"像人"之间找平衡。
你可能会问,脚本化的 UI 自动化框架不是也能点吗?区别在于 Monkey 是纯状态无关的注入,它不关心断言成不成功,所以它特别擅长触发那些"开发没料到的操作顺序",比如在 loading 弹窗刚出现的瞬间连点十下,或者在转场动画还没结束时按返回。这类竞态场景在常规用例里几乎不会出现,而在 Monkey 里是家常便饭。顺带纠正一个常见误解:ANR 其实不是 Monkey 自己检测出来的,而是系统服务在输入事件分发超时后记录的,Monkey 只是恰好扮演了那个持续施压的角色。
2.2 一行命令跑通:环境检查、包名确认与最小事件注入
动手前先确认两件事:设备连上了,包名是对的。adb devices 能看到序列号和 device 状态;包名要用应用 ID,不是桌面显示的应用名,在应用商店详情页里看到的那个名字对 Monkey 没有意义。
# 检查设备连接状态,看到 device 字样才算正常 adb devices # 确认目标包已安装,能 grep 到说明没问题 adb shell pm list packages | grep com.example.demo # 最小命令:向目标包注入 500 个随机事件 adb shell monkey -p com.example.demo 500-p 表示只把事件投递给该包,避免随机事件把桌面、设置、其他应用一起点开,这是做单应用压测必须写上的;末尾的 500 是事件总数。这个最小命令能跑完,说明环境没问题。第一次跑我建议加一个 -v:adb shell monkey -p com.example.demo -v 500,日志会打印出 seed、允许的包列表、事件进度和收尾状态,方便对照下一节的内容。尽量别在设备黑屏或锁屏状态下跑,原因放在避坑章节细说。
Monkey 本身不用安装,它在系统镜像里,所以也不存在"升级工具版本"这种操作;不同 Android 版本对参数的支持略有差异,想确认某个参数在当前设备上是否存在,直接 adb shell monkey --help 看输出,以设备实际支持为准,别拿网上旧教程的参数硬套。
2.3 三段式日志:从 seed 回显判断 finished 还是 aborted
Monkey 的日志有三段值得读。开头是配置回显,会打印 :Monkey: seed=... count=...,还有 :AllowPackage: 和 :IncludeCategory: 这类行,用来确认你限定的包和类别生效了。中间是事件进度,默认只有 0% 到 100% 的进度行,-v 级别越高明细越多。结尾最关键:正常结束会看到 :Monkey: 100% done 和 Monkey finished;中途出错会看到 ** Monkey aborted due to error **,后面跟着 Events injected 计数。
:Monkey: seed=1718612345 count=500 :AllowPackage: com.example.demo :IncludeCategory: android.intent.category.LAUNCHER :Monkey: 0% done :Monkey: 50% done :Monkey: 100% done Monkey finished判断一轮跑没跑完,看结尾比看 exit code 可靠,因为 adb shell 的退出码经常被命令行本身吞掉,脚本里直接依赖 $? 会翻车。常见做法是把输出落盘再读,落盘细节第四章讲。这里先记住判断口诀:看到 finished,只能说明事件注入完了,不代表应用没崩——崩溃如果被 --ignore-crashes 吞掉,同样会 finished;看到 aborted,则必须回头找 aborted 前面那一段,崩溃栈往往就在那里。
:Monkey: 35% done // CRASH: com.example.demo (pid 12345) // Short Msg: java.lang.NullPointerException // Long Msg: ... ** Monkey aborted due to error ** Events injected: 178aborted 之前最近的 // CRASH 行就是触发点,Events injected 里的事件序号告诉你崩溃发生在第几个事件附近。配合 seed,你可以在下一次运行时精确地预判崩溃位置,这是 Monkey 排错的基本功。
3. 参数配置:把随机事件调成接近真实手工操作
3.1 事件百分比怎么分:touch、motion、appswitch 推荐配比
不指定百分比时,Monkey 用内置权重混合所有事件类型,其中 nav、syskeys 这类系统级事件占比并不低,直接裸跑很容易把通知栏、最近任务、桌面点出来,测试目标被带偏。所以做正经压测的第一步,是显式给出事件百分比,让配比总和等于 100。
| 参数 | 事件内容 | 说明 |
|---|---|---|
| --pct-touch | 点击(按下+抬起) | 最主要的操作,占比应该最高 |
| --pct-motion | 滑动/手势 | 列表页、轮播图主要靠它 |
| --pct-trackball | 轨迹球 | 真机基本废弃,建议 0 |
| --pct-nav | 方向键 | 模拟器上意义大于真机,给 5 左右 |
| --pct-majornav | 确认/返回键 | 返回行为最容易暴露 Activity 栈问题 |
| --pct-syskeys | Home/音量/菜单 | 会导致应用退后台,普通应用给 0 |
| --pct-appswitch | 切到其他应用再切回 | 模拟恢复现场,建议保留 10 左右 |
| --pct-anyevent | 其他杂项事件 | 兜底,给 5 以内 |
我一般会给一款常规社交或工具类应用配这样一组:--pct-touch 45 --pct-motion 25 --pct-appswitch 10 --pct-majornav 10 --pct-nav 5 --pct-anyevent 5 --pct-syskeys 0。touch 高,是因为大多数崩溃发生在"连续快速点击不同区域"的场景;motion 保留四分之一,是为了覆盖列表滚动到一半时被点击打断的情况;appswitch 保留 10,是为了验证应用从后台恢复时的状态保存逻辑。syskeys 直接给 0,除非你要测的是桌面类或系统工具类应用。
提示:百分比参数的总和不必严格要求等于 100,Monkey 会按权重归一化,但显式凑成 100 更容易自查配比有没有写漏。
3.2 节奏与时长:throttle、seed 和固定时长的换算
不写 --throttle 时,Monkey 相邻事件之间几乎没有间隔,事件会在极短时间内全部注入,消息队列直接被打满。稳定压测我一般从 300ms 起步;想模拟人手的正常操作节奏,用 500ms;想快速跑一遍冒烟,可以用 100ms,但要做好 ANR 误报的心理准备。
# 固定时长 10 分钟:300ms 间隔,约 2000 个事件 adb shell monkey -p com.example.demo \ -s 20240617 \ --throttle 300 \ --pct-touch 45 --pct-motion 25 --pct-appswitch 10 \ --pct-majornav 10 --pct-nav 5 --pct-anyevent 5 --pct-syskeys 0 \ 2000事件数可以用 时长(毫秒) / throttle 来粗算:10 分钟是 600000ms,除以 300 就是 2000。注意这只是事件数,Monkey 每个事件本身的执行还有额外耗时,实际跑完往往比 10 分钟略长,想精确掐时长就在外壳上用 timeout 命令或 CI 超时控制。
-s 后面的数字就是 seed,建议用一个日期或构建号,比如 20240617,这样日志归档时能一眼看出是哪一轮的序列。另外一个实用参数是 --randomize-throttle,它让实际间隔在 0 到 throttle 之间随机抖动,更接近人手,代价是总时长不好精确预估。想跑固定时长时,我一般不用它,宁可节奏均匀一点换取时长可控。
3.3 ignore 三件套:回归快速失败还是压测跑满时长
Monkey 默认遇到第一个未捕获异常就中止,这对回归定位第一个 bug 是好事,但对稳定性压测是灾难——你可能只想跑满 2 小时,结果 1 分钟就停了。所以要根据目的选策略。
| 参数 | 作用 | 适用场景 |
|---|---|---|
| --ignore-crashes | 遇到 Java 崩溃不中止,继续注入 | 长时间压测统计崩溃率 |
| --ignore-timeouts | 遇到 ANR 不中止 | 长时间压测 |
| --ignore-security-exceptions | 遇到权限类异常不中止 | 事件打到受保护窗口时 |
| --kill-process-after-error | 崩溃发生后杀掉目标进程 | 配合 ignore 使用,让后续事件打到干净进程 |
| --monitor-native-crashes | 监控 native 层崩溃 | 有 C/C++ 代码的应用必开 |
回归模式不加 ignore,一崩即停,拿到的第一个堆栈对应的事件序列最短,最好定位。压测模式加 --ignore-crashes --ignore-timeouts,让测试跑满预设时长,最后统计崩溃次数、ANR 次数和对应事件序号。加了 ignore 之后要留意:崩溃过的进程可能处于半异常状态,后续事件继续打进去会产生一连串次生异常,所以压测模式一定要搭配 --kill-process-after-error,让它崩完就把进程杀掉,由系统重新拉起,这样统计出来的才是干净的结果。
4. 日志解析与崩溃定位:从 aborted 摘要到具体堆栈
4.1 两条日志通道:Monkey stdout 与 logcat crash 缓冲
Monkey 自身的输出——seed、进度、aborted 原因——走的是标准输出,直接重定向到文件就行。但 Windows 下 adb shell 会把换行符转成 \r\n,导致 grep "Monkey aborted" 时匹配不上,常见做法是用 tr -d '\r' 清洗一遍,或者用 adb exec-out 代替 adb shell,这个坑在避坑章节会单独展开。
崩溃堆栈不会完整进 Monkey 输出,它只给你一个 // CRASH: 摘要行,完整堆栈要去 logcat 里拿。logcat 有 main、system、crash、events 四个缓冲,崩溃后优先查 crash 缓冲,它专门存崩溃信息,不会被普通日志冲掉。
# 运行前清空所有缓冲,保证只有本轮日志 adb logcat -c # 复现结束后抓 crash 缓冲,threadtime 格式带时间线和线程号 adb logcat -d -b crash -v threadtime > crash.log先清空再抓取是个好习惯,不然 crash.log 里混着几小时前的旧崩溃,很容易把上一轮的 bug 算到本轮头上。
注意:crash 缓冲在部分低版本系统上不存在,抓不到时退回主缓冲过滤 FATAL EXCEPTION 即可。
4.2 一次崩溃的完整定位:跑、抓、grep 三步
完整流程是三段式:先带着固定 seed 和压测参数跑一轮,把 Monkey 输出落盘;再抓 logcat 崩溃堆栈;最后把两者按时间对上号。
# 第一步:固定 seed 跑一轮,verbose 两级,输出落盘 adb shell monkey -p com.example.demo -s 20240617 --throttle 300 \ --pct-touch 45 --pct-motion 25 --pct-appswitch 10 \ --pct-majornav 10 --pct-nav 5 --pct-anyevent 5 --pct-syskeys 0 \ -v -v 5000 > monkey.log 2>&1 # 第二步:Monkey 输出里的崩溃摘要和终止原因 grep -nE "// CRASH|ANR in|Monkey aborted" monkey.log # 第三步:logcat 里的完整堆栈,-A 80 取到 Caused by grep -A 80 -E "FATAL EXCEPTION|ANR in" crash.log第二步的 grep 是给 Monkey 输出做摘要,崩溃事件序号、发生时间都在这几行里。第三步的 grep 才是真正解决"为什么会崩"的入口:FATAL EXCEPTION 后面跟的是进程名和线程名,往下翻到 Caused by 那一行才是根因,比如 NullPointerException 或 ConcurrentModificationException。定位到异常类型和出问题的类名之后,再去对应代码里看那个方法在什么状态会被调用,这一轮 Monkey 的使命就算完成了。
如果 monkey.log 里只有 aborted 的迹象但没有 // CRASH 行,原因通常是崩溃发生在系统进程,或者被 --ignore-security-exceptions 吞了。别纠结,回 crash.log 按时间窗口翻,崩溃前最后一个打印日志的应用栈大多就是元凶。这里有个玄学规律:Monkey 崩溃扎堆出现在事件序号的 70% 到 90% 区间,因为前面的事件把应用状态逐渐推向了某个极端,崩溃不是突发的,是累积的。
4.3 同 seed 复现的边界:能复现什么,不能复现什么
同 seed 意味着同一条"事件类型和坐标的序列",但不意味着同样的应用状态。网络返回快慢、动画进度、后台任务调度,都会让两次运行走到不同的界面状态。所以结论是:同 seed 在同设备、同系统、同应用版本、同一登录态下,大概率复现同一类问题,但崩溃触发的具体事件序号可能差几十个;跨设备基本复现不出来,因为分辨率、DPI 不同,坐标会漂移。
实操上我给团队定的规矩是:凡是 Monkey 测出来的崩溃,bug 单里必须写清 seed、设备型号、系统版本、应用版本、throttle 和事件总数。缺了 seed,三个月后没人能解释那次崩溃是怎么点出来的;缺了设备型号,连复现的第一步都迈不出去。这套信息完整记录之后,即使崩溃不在本机复现,也能在另一台同型号设备上大概率复现出来,省掉无数次盲试。
5. Monkey 测试避坑指南:五个翻车场景的排查记录
这五个场景是我被同事问过最多、自己也在生产环境里翻过车的点,统一按现象、原因、解决三段写,方便你直接对号入座。
5.1 锁屏息屏后,Monkey 事件全部无效
现象:日志进度照常走到 100%,Monkey finished 也出现了,但应用界面纹丝不动,截图看是锁屏界面。原因:屏幕关闭或锁定时,窗口系统不派发触摸事件,Monkey 的事件全被丢弃,但注入动作本身没有报错,所以进度和收尾都正常。解决:跑之前执行 adb shell svc power stayon true,让设备在充电状态下不休眠;再执行 adb shell input keyevent 82 点亮屏幕并解锁。跑完记得把 stayon 恢复为 false,避免设备一直不灭屏影响后续测试。
5.2 卡在登录页,Monkey 空转几小时还报通过
现象:目标应用需要登录,Monkey 跑了几千个事件,全点在了登录页的输入框和按钮上,一个功能界面都没进入,崩溃率统计出来是零。原因:盲注入只能产生坐标和按键事件,没法输入账号密码,也点不动第三方登录的授权页。解决:在 Monkey 之前先把登录态准备好。常见做法是先手动登录并让应用进程留在后台,再用 adb shell am start 拉起应用主界面,最后才跑 Monkey;或者写一段 UI 自动化脚本完成登录、清掉脚本进程,再在同一个进程环境下启动 Monkey。
5.3 大量 SecurityException 污染崩溃统计
现象:日志里每隔几十个事件就蹦出 SecurityException,grep 出来的"崩溃"数量吓人,但打开一看全是权限拒绝。原因:--pct-syskeys 或 --pct-anyevent 触发了受权限保护的系统操作,比如尝试对系统窗口注入事件、访问受保护的系统服务,被拦截是系统正常行为,不是应用代码的问题。解决:syskeys 直接给 0,anyevent 控制在 5 以内,把事件尽量限制在应用自己的窗口里;统计崩溃时,把 SecurityException 单独归一类,不计入应用崩溃率。加不加 --ignore-security-exceptions 取决于你要不要中断——压测模式建议加,回归模式建议不加,让它在现场停住方便查。
5.4 一崩就退出,压测形同虚设
现象:计划跑 2 小时压测,5 分钟不到就退出,monkey.log 尾部是 ** Monkey aborted due to error **,事件数只跑了 300 多。原因:默认策略遇到第一个未捕获异常就中止,这是为回归定位设计的,不是为稳定性统计设计的。解决:压测模式统一加 --ignore-crashes --ignore-timeouts,并搭配 --kill-process-after-error,让崩溃后进程被清理、由系统重新拉起,测试继续跑满时长。回归模式反过来,刻意不加 ignore,让它在第一个 bug 处停住,拿最短的事件序列去定位。
5.5 throttle 太短触发 ANR 误报
现象:throttle 设成 50ms,跑完 grep 出一堆 ANR in com.example.demo,但开发复查代码说主线程没有耗时操作,手工操作也不会卡。原因:50ms 间隔意味着每秒注入 20 个事件,远超人手的操作速度,主线程消息队列持续堆积,系统判定输入分发超时,属于注入压力导致的假性 ANR。解决:throttle 从 300ms 起步,低压测用 500ms。判断 ANR 真伪有一个办法:把 throttle 加大到 500ms 重跑同一个 seed,如果 ANR 消失,说明是注入过快;如果 500ms 下依然 ANR,才是应用的真实问题。这条血泪经验救了我不止一次——先排除工具自身的干扰,再给应用下结论。
5.6 Windows 下日志带 \r\n,grep 全部匹配不上
现象:在 Windows 的 cmd 或 PowerShell 里执行 adb shell monkey ... > monkey.log,然后用 grep 找 "Monkey aborted" 全部找不到,但文件用记事本打开是正常的。原因:adb shell 在 Windows 下把输出里的 \n 转成了 \r\n,grep 匹配时字符串后面多了一个 \r,自然匹配不上。解决:用 adb exec-out monkey ... > monkey.log 绕过转换;或者先落盘再执行 tr -d '\r' < monkey.log > monkey_clean.log 清洗后解析。脚本里统一用 exec-out 一劳永逸,别在每台机器上重复踩这个坑。
6. 进阶:把 Monkey 收编成可复现的回归压测脚本
6.1 包装脚本:日志名里带上 seed 和时间戳
到这一步,手动敲命令已经不够用了。我习惯写一个包装脚本,把参数固定成默认值,seed 和日志路径每次自动生成,保证每一轮测试都能追溯到原始数据。
#!/bin/bash # run_monkey.sh: 一键跑 Monkey 并归档日志 PKG="com.example.demo" THROTTLE=300 COUNT=2000 SEED="$(date +%s)" TS="$(date +%Y%m%d_%H%M%S)" LOG="monkey_${TS}_${SEED}_${PKG}.log" adb logcat -c adb exec-out monkey -p "$PKG" -s "$SEED" --throttle "$THROTTLE" \ --pct-touch 45 --pct-motion 25 --pct-appswitch 10 \ --pct-majornav 10 --pct-nav 5 --pct-anyevent 5 --pct-syskeys 0 \ --ignore-crashes --ignore-timeouts --kill-process-after-error \ -v -v "$COUNT" > "$LOG" 2>&1 adb logcat -d -b crash -v threadtime > "crash_${TS}.log" grep -E "// CRASH|ANR in|Monkey aborted" "$LOG" && echo "本轮有异常,见 $LOG" || echo "本轮无异常"日志名里带上时间戳和 seed 的意义在于:压测结束看到异常,可以直接用日志名里的 seed 原样重跑;一个月后再翻日志,也能从文件名还原出当时的参数环境。seed 用 date +%s 是省事做法,想固定复现就把日志名里的 seed 手动填回 -s 参数重新执行。
6.2 接进 CI 前要定的阈值和判断标准
把 Monkey 接进每日回归前,先定好判断标准,不然它只会制造噪音。我一般定三个门槛:事件数一万以内出现任何一个 FATAL EXCEPTION 判失败;ANR 在 throttle 500ms 下仍出现判失败;SecurityException 不计入失败但单独汇总。界线之外,还要在 CI 上固定设备型号或云真机机型,因为不同分辨率下同一个 seed 的事件坐标会漂,结果不具备跨设备可比性。
另外别把 Monkey 和另一类工具搞混:MonkeyRunner 是脚本驱动的 UI 自动化,能读控件、能断言,和本文讲的随机事件压测工具是两条路线。日常回归用脚本化工具做功能覆盖,用 Monkey 做随机稳定性兜底,两者互补而不是相互替代。最后说一句我的习惯:任何一次上线前,我都会在真机上跑满一万事件,跑完必须有人读过 CRASH 行才允许发版,而不是只盯着"跑完了"三个字。这套规矩救回过几次差点带着崩溃上线的版本。希望帮到你。
本文还有配套的精品资源,点击获取