☰
Monkey离线脚本全解析:从随机事件到可控编排的Android稳定性测试
2026/10/15 7:01:33 网站建设 项目流程

1. 从“随机点按”到“按需编排”:Monkey离线脚本到底解决了什么问题

做过Android稳定性测试的同学,对Monkey这个工具都不会陌生。它属于Android SDK自带的一个命令行工具,核心功能就是向目标应用发送海量的伪随机事件流——点击、滑动、按键、缩放等等,用来模拟用户的各种操作,在短时间内暴露应用的崩溃、无响应、内存泄漏以及逻辑异常问题。

但是用久了,你会发现Monkey有一个很让人头疼的短板:默认的随机模式不可控。你明明想重点测登录流程,它偏偏在权限弹窗上反复横跳;你希望先进入某个二级页面再开始“狂点”,它却连首页都没跑通就各种乱按。真实的测试场景里,我们需要的是在可控路径上执行不可控操作,或者干脆就是一套写死顺序的指定操作序列。

这时候就要用上“monkey离线脚本”这套玩法了。它的本质是:不依赖Monkey默认的随机种子,而是通过编写一份基于Monkey自有语法的事件流脚本文件,由Monkey逐条解析并执行。你完全可以把它理解成给Monkey装了一个“剧本”,告诉它每一步该干什么,什么时机干什么,以及按什么顺序干。

这篇文章我不会停留在官网文档的复述层面,而是从实际项目经验出发,讲清楚离线脚本的核心原理、编写规范、执行参数选型、常见坑与排查思路。内容适合正在做Android稳定性测试、CI流水线接入、自动化回归的同学参考,即使你之前完全没有碰过Monkey,按照后面的步骤也能很快上手。

2. 重新理解Monkey的执行模型:随机事件背后藏着什么逻辑

2.1 Monkey的基本事件类型与其触发机制

在聊离线脚本之前,先厘清Monkey在正常情况下是怎么工作的。Monkey在命令行中通过一连串的参数来定义事件流,下面是一个最基础的命令:

adb shell monkey -p com.example.demo --throttle 300 -s 1024 5000

这里几个参数的含义值得展开一下。-p指定被测应用的包名,如果希望同时测试多个包,可以多次使用-p;--throttle表示每条事件之间的延时时间,单位是毫秒;-s是随机种子,它决定了伪随机数生成器产生的序列,好处是如果这次测出了崩溃,下次使用相同的种子便能复现同一条路径;最后的5000则代表发送事件的总数。

默认情况下,Monkey会把所有事件类型混合在一起,按照内部权重随机抽取。事件类型包括触摸事件、手势事件、轨迹球事件、导航事件、按键事件、系统按键事件以及启动Activity的事件。每种事件对应了用户操作的不同维度,比如触摸事件就是屏幕上的点按,而手势事件则模拟滑动。

权重分布是Monkey随机性的核心逻辑。基本默认情况下,触摸事件与手势事件占了较高比重,系统按键事件被有意调低,这是为了避免测试过程中频繁触发Home键或音量键导致用例脱离目标页面。理解这个模型的意义在于:当你发现默认模式覆盖不到某些关键场景,或者希望在更短的时间内打穿某个功能链路,就必须跳出随机模式,主动控制事件序列。

2.2 seed值为什么会失效:随机模式的复现边界

有同学可能会说,既然-s参数能固定种子,那我反复跑同一条序列不就行了?这里我需要提醒一下,seed值只能保证Monkey自身生成的事件顺序一致,它无法保证设备侧的响应状态一致。比如应用开屏广告的实时拉取结果不同、网络请求返回的时序不同、系统弹窗的出现时间不同,都会导致同一条“伪随机序列”走出完全不同的界面路径。

而且更麻烦的一点是,默认的随机事件流中,出现概率较低的系统按键事件往往会让测试跑到一半就跳出应用。这类跳出不是一个严格意义上的崩溃,对稳定性统计虽然影响不大,但对回归测试来说,它会浪费大量时间,因为你无法预期它究竟在哪个时间点退出,也无法便捷地让它回到业务主流程。

离线脚本的核心价值就体现在这个地方:你可以在每一个关键节点上写明等待条件、启动哪个Activity、点击哪个坐标,甚至在步骤之间插入延时,从而极大提高脚本的可控性。它不再是一个“碰运气”的稳定性测试,而是一种可以纳入版本回归体系的白盒化交互测试方案。

3. 离线脚本语法图解:用Monkey语言编排你的操作剧本

3.1 与adb shell命令的根本区别

首先要明确一点:Monkey离线脚本不是普通的Shell脚本,不能直接用一堆adb shell input命令去拼。虽然input命令也能执行点击和滑动,但它与Monkey是两套体系。Monkey离线脚本使用MonkeyRunner内嵌的一套事件指令语法,由Monkey进程本身来解析执行。

两种方式的差异决定了项目选型的方向。基于adb shell input的脚本写起来直观,但有两个明显短板:一是每条adb指令都要经过一次完整的命令解析与传输链路,执行效率不够高;二是它无法直接利用Monkey框架内建的等待、条件判断、事件延迟等机制,很多东西需要自己在外层Shell里造轮子。而Monkey自带脚本语法的设计初衷,就是让事件流描述更加紧凑,并且能在Monkey内部高效调度。

3.2 Monkey脚本的基本单位:事件指令逐条拆解

Monkey脚本是一个纯文本文件,每一行都是一条指令。官方定义的指令关键字并不多,常用的就那么几个,但组合起来非常灵活。

启动Activity的指令是:

LaunchActivity(pkg_name, Activity_name)

其中的Activity名称有讲究,一般需要写完整类名。如果目标Activity配置了exported属性为false,并且你从外部启动,会碰到权限问题,这一点在后期排查部分会再展开。

等待指令有两种写法:

Wait(duration)

和

UserWait(duration)

Wait是硬等待,到了指定时间立即执行下一步;UserWait则模拟用户思考停顿,适合在页面跳转后留出渲染时间,避免下一个操作落在空界面上。两者都接收以毫秒为单位的整数参数。

用于检查目标Activity是否在前台的是:

DispatchPointer(0, x, y, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

这一条看似参数很长,其实关键只有第二、三参数代表坐标位置的x和y。通过一条指针事件,可以实现点击;事件类型本身通过第一个参数区分,0代表按下,如果命令中还包含抬起动作,就需要写成两条来实现完整的点按。Monkey脚本语法中也可以使用DispatchPointer(0, x, y, ...) 配合DispatchPointer(1, x, y, ...) 分别表示按下和抬起。

旋转屏幕使用:

RotateScreen(degree, rotate)

degree取值范围0到3,表示旋转的方向与角度档位。这个指令在测试横竖屏适配时非常有用,但要注意,在系统允许自动旋转时,旋转结果可能仍然受系统传感器策略影响。

输入文本用:

DispatchString(example_input)

地址栏、搜索框、登录表单的文本输入都由它完成。不过它输入的是英文与数字内容,对于中文输入的支持比较有限,如果想输入中文,往往需要配合adb层面的广播或者另行剪贴板方案。

3.3 脚本的核心约束:坐标与依赖

每个脚本的核心约束都落在坐标上。坐标的参照系是屏幕像素,不同分辨率的设备上,同样的脚本可能就要做多套适配。这里有一个常见的实操做法:脚本里不写死坐标,而是通过UIAutomator或者dump命令先抓取控件位置,再动态生成Monkey脚本。这样在自动化流水线里,换了一台新分辨率的机器,脚本生成的逻辑可以自动适配,而不是人工逐条改坐标。

另外一个约束是脚本对页面状态的隐式依赖。因为Monkey脚本本身没有直接的条件判断来识别某个控件是否存在,只提供了Traffic和回调。所以在写脚本时,你必须在前期人工梳理好页面跳转的预期关系,并在关键节点用UserWait留出充足的页面加载时间。

3.4 一个可直接运行的示例脚本结构

下面我给出一个典型的离线脚本示例,做了注释说明,方便理解指令顺序与作用:

# 启动目标应用的主界面 LaunchActivity(com.example.demo, com.example.demo.MainActivity) UserWait(3000) # 等待首页加载完成后,点击底部导航栏的第二个Tab DispatchPointer(0, 540, 1800, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) DispatchPointer(1, 540, 1800, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) UserWait(2000) # 输入一段查询文本并触发搜索 DispatchString(selenium) DispatchPointer(0, 900, 1400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) DispatchPointer(1, 900, 1400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) UserWait(3000) # 执行返回键 DispatchKey(4, 0, 0) UserWait(1500)

这个脚本涵盖了启动、点击、输入、按键四个高频操作。其中DispatchKey的第一个参数4,对应Android系统的Back键键值。如果你是写Shell脚本的老手,第一次看到Monkey脚本可能会觉得参数写得太啰嗦,但习惯了之后会发现,这种格式的好处是结构统一,便于程序化生成。

4. 在自己的机器上跑通Monkey离线脚本:完整实操过程

4.1 前置条件与环境准备

跑Monkey离线脚本不需要额外安装第三方工具,因为Monkey随Android SDK发布,位于platform-tools路径下。你只需要保证adb能正常连接设备或模拟器即可。

我用一个模拟项目X来演示完整流程。这个项目的包名是com.example.demo,主Activity是MainActivity,签名方式和大多数商业应用一样,有权限保护,也有开屏广告逻辑。设备是一台分辨率为1080x2400的测试机。

第一步,确认设备已被adb识别:

adb devices

输出中如果出现设备序列号并且状态是device,说明连接正常。接下来,我需要提前准备好脚本文件。Monkey脚本对文件编码比较敏感,推荐使用UTF-8无BOM格式保存,换行符建议使用LF,避免在Windows环境编辑后产生兼容问题。

4.2 脚本传输与执行命令选型

把写好的monkey_script.txt推到设备上,常见路径是/data/local/tmp/:

adb push monkey_script.txt /data/local/tmp/monkey_script.txt

执行时,命令格式如下:

adb shell monkey -p com.example.demo --throttle 200 -f /data/local/tmp/monkey_script.txt 10

这里需要解释一下尾部数字的作用。Monkey命令中,事件计数值放在最后,即使使用-f参数指定脚本文件,这个数字依然需要存在,它表示脚本执行结束后,如果脚本内事件没有循环结束,则继续随机发送多少条补充事件。如果你想严格只执行脚本中的事件,可以把计数值设置得比脚本事件少,或者为0。实际测试中,我一般会把计数值设为0或1,确保测试完全由脚本驱动,不被随机事件干扰。

--throttle参数在这里也有微妙作用。脚本已经内置了UserWait指令,但throttle会额外在每条指令之间再加一层固定延时。如果你已经为每个步骤设计了充分的等待时间,建议把throttle设为0,避免测试时间白白拉长。

4.3 观察Monkey日志输出:别忽略的崩溃信息

执行过程中,Monkey会在终端持续输出进度信息。当脚本运行正常时,日志会类似这样:

// Event: LaunchActivity // Events injected: 6

如果出现问题,Monkey会抛出异常描述,并且进程会中止。日志末尾通常会带有崩溃的堆栈信息或错误类型分类。养成一个习惯:每次执行完,都把完整日志重定向到文件,然后统一解析。我通常使用:

adb shell monkey -p com.example.demo --throttle 0 -f /data/local/tmp/monkey_script.txt 1 > monkey_run.log 2>&1

这样后续排查就会方便很多。日志中有一个字段需要特别关注,就是System ANR和System Crash。很多只在Monkey测试中暴露的时序问题,都会在这里找到线索。

4.4 页面刷新太快导致脚本失效:我踩过的一个典型坑

有一次我在测某跨平台系统的启动链路时,脚本中第一步LaunchActivity执行完,紧接着就做了点击操作。本地模拟器一切正常,但换到真机上,脚本老是卡在第一步之后没有反应。排查了半天,发现是应用开屏广告的加载时间在真机上明显变长,UserWait给了2秒不够,点击事件落在了开屏广告上,结果被广告SDK拦截,后续所有事件全部错位。

解决办法其实不复杂:在LaunchActivity之后,将UserWait调整为动态轮询,通过外层Shell先周期性检查当前Activity是否已切换到目标页面,确认后再注入Monkey脚本。我当时的做法是写了一个简易的bash循环,每500毫秒执行一次dumpsys activity top,等目标页面出现后立刻启动Monkey。这样既保证了时序稳定,也不会因为固定等待时间太长拖慢整体节奏。

这个案例给了我一个经验:Monkey离线脚本虽然看似只是写事件,但它本质上是依赖页面状态的集成测试,越早把页面状态等待做好,后面就越不会翻车。

5. 把离线脚本变成一套可复用的方案:参数设计、生成策略与工程化

5.1 脚本模板化:一次编写,处处复用

如果目标应用界面结构比较稳定,完全可以把离线脚本模板化。模板里只有坐标、包名、Activity名称、输入字符串这几个关键字段会变化。

我习惯维护如下结构的模板:

LaunchActivity({pkg}, {launcher_activity}) UserWait({launch_wait}) DispatchPointer(0, {x1}, {y1}, ...) DispatchPointer(1, {x1}, {y1}, ...) UserWait({step_wait}) DispatchString({input_text})

然后在上层脚本里,通过对被测应用配置的解析,自动把占位符替换成真实参数。这样在新版本测试时,只需要更新配置,不需要改动脚本框架。

这里需要特别提醒一个细节:坐标参数不要直接在模板里保留默认值,否则一不留神没替换干净,整条链路会全部偏掉。我在工程里加了一个简单的预检逻辑:脚本生成后,先扫描是否存在未被替换的{x}之类的占位符,一旦发现就直接终止,防止坏脚本进入执行队列。

5.2 通过Java或Python脚本批量生成Monkey脚本

在自动化回归场景中,手动维护坐标脚本是不现实的,因为每个版本的UI都可能调整。更合理的方案是让程序动态生成。实际项目中,我用Python做过一个脚本生成器,流程如下:

第一步,连接设备,使用uiautomator dump命令导出当前页面的控件树XML文件:

uiautomator dump /sdcard/ui_dump.xml adb pull /sdcard/ui_dump.xml

第二步,解析XML,定位目标控件。根据控件的resource-id、text、content-desc属性找到目标位置,由此取得它在屏幕上的边界bound。注意,XML中bound的格式类似[left,top][right,bottom],中心点就是((left+right)/2, (top+bottom)/2)。在生成脚本时把这个中心点作为点击坐标填入DispatchPointer事件。

第三步,按用户指定的页面操作顺序,串联所有控件事件,最终生成一个包含完整页面流与交互动作的Monkey脚本。

这种方法的好处是脚本会随着设备分辨率变化而自适应,因为坐标来源于当前设备的实时控件树。比如同一份页面操作流程,在平板和手机上都能生成正确的脚本。相比在一台设备上“录好”再机械套用的方式,显然灵活得多。

生成的脚本示例:

# auto generated by script generator, do not edit manually LaunchActivity(com.example.demo, .MainActivity) UserWait(3500) DispatchPointer(0, 540, 1876, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) DispatchPointer(1, 540, 1876, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) UserWait(2000)

5.3 在CI流水线中嵌入离线脚本执行

离线脚本很适合嵌入CI,尤其是每日构建后的自动化冒烟测试。我的常用做法是:在构建完成后,准备一台连接在测试服务上的设备,脚本执行器从测试用例仓库拉取最新脚本,执行并回传日志与结果。

CI执行需要注意一个要点:执行前必须清理应用数据。如果应用带登录态,上一次的登录数据可能让页面跳过登录页,或者Session过期导致页面停留在异常状态;如果不清理,脚本第一个点击就可能落在错误位置。执行清理使用以下命令:

adb shell pm clear com.example.demo

清理后冷启动应用,等待时间适当拉长,确保应用完成初始化。

异常处理同样不能轻视。Monkey进程偶发出现无响应,脚本卡死。要在CI脚本里给adb shell monkey命令加上超时控制,比如:

timeout 300 adb shell monkey -p com.example.demo -f /data/local/tmp/monkey_script.txt 1

一旦超时,将本次测试标记为失败,然后通过adb shell am force-stop结束应用,恢复设备到一个干净状态,再继续下一条用例。否则设备被一个卡死的Monkey进程占住,整套流水线都会阻塞。

5.4 离线脚本与MonkeyRunner的边界:到底该选哪个

很多初学者会把Monkey离线脚本和MonkeyRunner混在一起。MonkeyRunner是另一个基于Python/Jython的自动化工具,可以启动应用、发送按键和点击,并且支持截图对比。两者最大的区别是,MonkeyRunner可以做条件判断和图像识别,但它依赖客户端持续运行,脚本执行效率相对更低,因为每一步都需要经过PC端的Jython解释。

而Monkey离线脚本完全在设备端执行,不依赖PC端持续交互,适合已经规划好的固定事件流,运行效率更高、稳定性更好。在实际项目中,我会这么分工:固定、高频、简单的交互链路用Monkey离线脚本;复杂逻辑判断、图像比对的场景用MonkeyRunner或者UIAutomator2。两者不是替代关系,而是在不同层次上互补。

6. 常见问题与排查技巧实录:从失灵到修复的完整路径

6.1 脚本执行后没有任何反应:从Activity名到权限的排查链

现象:Monkey日志正常输出了LaunchActivity事件,但屏幕上没有出现目标应用。

排查思路分三步。第一步,确认包名是否写错。可以通过adb shell pm list packages | grep demo来验证。第二步,确认Activity完整类名是否正确。很多应用启动Activity并不等于Manifest中标记的launcherActivity,尤其是有分身、多进程、别名机制的应用。可以通过adb shell cmd activity get-current-user或者dumpsys activity top来观察当前焦点Activity的名称。第三步,检查Activity的exported属性。如果该Activity没有设置android:exported="true",Monkey进程启动它时会直接抛异常。此时要么选择允许导出的入口Activity,要么在测试包中配置相应的权限。

6.2 事件全部偏移:触摸事件的点击位置不在预期控件上

现象:脚本执行了,但是每次都点到错误的位置,或者根本没有触发任何业务动作。

这个问题通常来自两个原因。一是脚本里的坐标还是旧分辨率设备的坐标,换设备后没有重新生成。二是应用界面有全局的窗口偏移,比如系统导航栏是手势模式时,屏幕底部区域有一部分不可点击,真实可视区域从某个y值开始。我遇到过一个比较隐蔽的情况:手机厂商在系统级设置了“防误触模式”,在屏幕边缘预留了一定像素的屏蔽区域,导致最边缘的点击事件被系统吃掉。这种情况下,最直接的办法就是修改脚本坐标,避开边缘区域;如果确实需要测边缘滑动,可以配合sendevent在更底层实现,但一般不建议走那么深。

6.3 脚本正常结束,但没有触发任何崩溃:如何验证脚本是否真的有效

很多同学写完脚本跑一遍,日志显示Events injected若干条,没有报错,就认为测试通过了。这里要泼一盆冷水:Monkey的日志只表示事件已经注入系统,并不代表所有事件都被应用完整消费。潜在原因可能是指针事件没有正确配对,或者事件被某个浮层拦截。

验证脚本是否有效的简单方法是加探针。在脚本执行到某一步后,通过dumpsys activity top获取当前页面Activity,与预期Activity做比对;或者更直接一点,用adb截图看当前界面是否停留在预期的业务页面。把这个验证逻辑集成到CI中,每次执行完脚本都自动执行一遍,能避免很多“假通过”的情况。

6.4 如何从一次崩溃日志回溯到脚本中的具体指令

Monkey日志末尾的崩溃堆栈往往很长,有时几十条堆栈信息里看不到脚本上下文。此时可以先定位崩溃发生的相对时间点,再回溯脚本执行到第几条事件。Monkey日志中每注入一条事件都会打印Events injected: N,如果崩溃发生在Events injected数量从10跳到11之间,那就可以把怀疑范围缩小到脚本文件的第11条指令附近。再结合堆栈中涉及的业务代码类名,基本就能锁死触发链路。

此外,建议执行时加上--bugreport参数,这个参数会让Monkey在出现崩溃或ANR时自动抓取系统bugreport信息,包括CPU、内存、线程状态,对定位问题很有帮助:

adb shell monkey -p com.example.demo --bugreport -f /data/local/tmp/monkey_script.txt 1

6.5 离线脚本在CI中偶发全部失败:检查设备的锁屏与充电状态

设备长时间运行后,如果没有关闭屏幕超时,会自动锁屏。锁屏状态下,Monkey注入的触摸事件统统落到锁屏界面,脚本自然全部失败。执行前务必保持屏幕常亮:

adb shell svc power stayon true

另外一个容易被忽略的点是设备的电池优化策略。有些厂商的系统会默认清理后台进程,Monkey进程因为属于shell进程,一般不会被杀,但被测应用本身可能被系统回收。如果出现脚本前半段正常、后半段突然找不到目标页面,就要考虑应用进程是否被系统杀掉。针对这种情况,可以在测试前关闭被测应用的电池优化,甚至在开发者选项里开启“不保留活动”等调试开关来复现问题。

7. 最后分享一点个人经验

我最初接触Monkey离线脚本时,也觉得它无非是比随机Monkey多写了几条指令。但越往深做越发现,它真正考验的并不是语法记忆,而是对页面状态、设备状态、事件时序的综合把控。离线脚本最大的魅力,在于把一个不可控的随机工具变成了“半可控的测试引擎”,你用很低的成本就能模拟出关键业务链路上的高频操作,让稳定性测试从“撞大运”变成了可预期的回归手段。

如果你现在的项目还在用纯随机Monkey跑稳定性,不妨从一条简单的LaunchActivity脚本开始,逐步把登录、搜索、详情页浏览这些高频路径固化下来。你会发现,脚本维护带来的额外工作量,远小于排查随机事件引发的“幽灵崩溃”所花费的时间。另外一个建议是,脚本生成逻辑尽量早一点纳入工程体系,用自动化的方式管理坐标与控件变化,别等到UI改版后再手工改几十条坐标指令,那真是一场灾难。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询