Global Functions 全局函数
Global Functions 全局函数
这一页专门讲“运行时直接注入到全局作用域的函数”。我会尽量按源码实际行为来写,不只列名字,还把这些点写清楚:
- 传什么参数才算合法。
- 不传参数时默认会怎样。
- 返回值到底是什么。
- 哪些是别名,哪些只是看起来像别名。
- 传错类型时会不会报错。
先记住这 17 条
log(...args)和print(...args)都是多参数函数,但拼接方式不一样。sleep(ms)只认数字,负数会按0处理。random()返回0 ~ 1之间的小数,random(max)/random(min, max)返回整数。random(min, max)的边界是包含两端的,而且写反了也不会报错。setTimeout/setInterval返回的是任务id,不是对象。clearTimeout(id)/clearInterval(id)返回布尔值。setImmediate(...)当前本质上就是“当前脚本线程上的 0 延迟调度”,不是 Promise 微任务。Task(fn, delayMs?, ...args)当前本质上就是“一次性定时器别名”,不是复杂任务类。setTimeout/setInterval/setImmediate/Task都支持把额外参数继续传进回调。sync(func, lock?)会把函数包装进同一把锁里执行。showGlobalDialog、showHostDialog、confirm用的是同一套实现。currentPackage()/currentActivity()依赖无障碍服务,没开会直接抛错。currentActivity()返回的是前台页面类名字符串,不是全局activity那个Activity对象。selector()/text()/id()这一整组也是全局函数,本质上是自动化选择器的快捷入口。scrollUp([i])/scrollDown([i])/setText([i, ]text)/input([i, ]text)也是全局自动化函数,不只是UiObject上的方法。click()/longClick()/press()/swipe()和Tap()/Swipe()不是同一路;前者偏无障碍手势,后者偏 root 输入。openDexKit(...)/openDexKitByClassLoader(...)/injectActivity(...)也已经直接注入到全局作用域,只是详细规则放在专页。
日志与基础控制
总表
| 函数 | 典型签名 | 返回值 | 默认值 / 特殊规则 | 说明 |
|---|---|---|---|---|
log | log(...args) | null | 多参数用空格拼接 | 普通日志输出 |
print | print(...args) | null | 多参数用制表符拼接 | 更像调试打印 |
toast | toast(...args) | null | 多参数用空格拼接 | 弹宿主 Toast |
toastLog | toastLog(...args) | null | 多参数用空格拼接 | Toast + 日志双写 |
sleep | sleep(ms) | null | 非数字按 0;负数按 0 | 阻塞当前脚本线程 |
exit | exit() | 不返回 | 会中断脚本并取消定时任务 | 主动结束脚本 |
printStackTrace | printStackTrace() | null | 无 | 打印当前调用栈 |
log(...args)
参数规则
- 可以传任意数量参数。
- 每个参数都会先转成字符串。
- 最终用一个空格拼起来。
log("pkg", lpparam.packageName, "proc", lpparam.processName);
print(...args)
和 log() 很像,但当前源码里用的是制表符 \t 拼接,所以更适合“列式调试输出”。
print("pkg", lpparam.packageName, "proc", lpparam.processName);
toast(...args)
参数规则
- 可以传多个参数。
- 最终同样是用空格拼成一条文本。
- 没有可用 Android
Context时,Toast 不会真正显示。
toast("hook attached", lpparam.packageName);
toastLog(...args)
行为等于:
- 先 Toast
- 再写日志
toastLog("ready");
sleep(ms)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
ms | number | 任意数字 | 0 | 休眠毫秒数 |
规则
- 非数字时,按
0处理。 - 负数会被修正成
0。 - 这是阻塞式休眠,会卡住当前脚本线程。
sleep(250);
sleep(-1); // 实际按 0 处理
exit()
这不是“优雅返回”,而是直接中断脚本执行。
它会做什么
- 取消所有
setTimeout任务 - 取消所有
setInterval任务 - 中断脚本运行时
所以它适合:
- 你明确知道这段脚本该停了
- 当前错误已经没必要再继续跑后续逻辑
printStackTrace()
直接打印当前堆栈,最适合临时定位“我是从哪里进来的”。
printStackTrace();
剪贴板与语言
总表
| 函数 | 典型签名 | 返回值 | 默认值 / 特殊规则 | 说明 |
|---|---|---|---|---|
setClip | setClip(text) | null | 只取第一个参数 | 写剪贴板 |
getClip | getClip() | string | 剪贴板空时返回 "" | 读剪贴板 |
getAppLanguage | getAppLanguage() | string | 无 | 返回当前系统语言标签 |
setClip(text)
参数规则
- 只读取第一个参数。
- 会先转成字符串再写入。
- 没有可用
Context时会抛异常。
setClip("https://www.wuyunai.com");
setClip(12345); // 会写成 "12345"
getClip()
返回规则
| 场景 | 返回值 |
|---|---|
| 剪贴板有文本 | 对应文本 |
| 剪贴板服务拿不到 | "" |
| 剪贴板没有内容 | "" |
没有可用 Context | 抛异常 |
const text = getClip();
log(text);
getAppLanguage()
当前直接返回:
Locale.getDefault().toLanguageTag()
所以常见结果像:
zh-CNen-USja-JP
log(getAppLanguage());
当前前台应用信息
总表
| 函数 | 典型签名 | 返回值 | 默认值 / 特殊规则 | 说明 |
|---|---|---|---|---|
currentPackage | currentPackage() | string | null | 依赖无障碍服务;未开启时抛异常 | 取当前前台应用包名 |
currentActivity | currentActivity() | string | null | 依赖无障碍服务;未开启时抛异常 | 取当前前台 Activity 类名 |
这一组和 lpparam.packageName、全局 activity 很容易混。先记住最实用的区分:
lpparam.packageName:当前脚本绑定的目标包名,通常是这次脚本要处理的宿主包。currentPackage():无障碍最近一次确认到的当前前台应用包名,用户切到别的 App 时它会跟着变。activity:当前尽力解析出来的Activity实例对象,可能是null。currentActivity():当前前台页面的类名字符串,不是Activity对象。
currentPackage()
参数与调用规则
- 不需要参数。
- 你就算多传参数,当前实现也不会去读取它们。
- 函数会先检查脚本暂停状态,再去取无障碍服务里的前台信息。
返回规则
currentPackage() 的返回值不是靠单一来源硬取的。当前源码会按下面这个思路尽量找:
- 先看当前窗口列表里有没有更可靠的前台包名。
- 再看当前前台 Activity 组件里能不能反推出包名。
- 再看当前活动 root 节点的
packageName。 - 如果这些都暂时拿不到,再回退到最近一次缓存到的可用前台包名。
所以大多数时候,你可以把它理解成“当前正在前台的应用包名”;但如果无障碍刚启动、窗口状态瞬间变化、系统暂时只给了不完整信息,它也可能返回 null。
如果你之前已经用:
auto.setWindowFilter(screen);
或者 auto.setWindowFilter(7) 这类写法把自动化显示作用域锁到了某块显示器,它会优先从那块屏命中的窗口里拿包名,而不是只按主屏去理解。
没开无障碍时会怎样
这组函数都依赖可用的无障碍服务。
如果当前服务没启动,源码会直接抛:
Accessibility service is not enabled
所以如果你的脚本一上来就要依赖它,最稳的做法是先确保无障碍已经处于可用状态。
最常见例子
log(currentPackage()); // 例如:com.ss.android.ugc.aweme
和 lpparam.packageName 对比着看
这是最值得给新手说清楚的一点。
log(`target=${lpparam.packageName}`);
log(`foreground=${currentPackage()}`);
当你的脚本绑定在某个宿主里,但用户中途切到了别的应用,这两个值就可能不一样:
lpparam.packageName还是脚本当前绑定的宿主包。currentPackage()已经变成了用户眼下切过去的前台 App。
一个很常见的判断写法
const pkg = currentPackage();
if (pkg == "com.ss.android.ugc.aweme") {
log("当前前台已经是抖音");
}
为什么这里示例优先写 ==
currentPackage() 和 currentActivity() 这类值虽然对脚本来说“看起来像字符串”,但底层是从 Java / Android 对象桥接过来的。
在 Rhino 里,这类桥接值直接拿去和 JS 字面量做 ===,有时会因为不是纯 JS 原始字符串而比较失败。
所以最稳的写法有两种:
if (currentPackage() == "com.tencent.mm") {
// 直接比较
}
if (String(currentPackage()) == "com.tencent.mm") {
// 先转成纯 JS 字符串,再比较
}
currentActivity()
它返回的到底是什么
currentActivity() 不是返回完整组件串,也不是返回 Activity 实例。
它最终返回的是前台组件里 / 后面的那部分,也就是页面类名字符串。
比如无障碍内部拿到的是:
com.ss.android.ugc.aweme/com.ss.android.ugc.aweme.im.sdk.chat.ChatRoomActivity
那 currentActivity() 返回给脚本的就是:
com.ss.android.ugc.aweme.im.sdk.chat.ChatRoomActivity
返回规则
- 会先读取无障碍服务当前确认到的前台组件。
- 只取组件字符串里
/后面的类名部分。 - 会自动
trim()并过滤空串。 - 没拿到可用组件时,返回
null。
还有一个源码细节挺有用:
它只会把看起来像真正页面类的组件缓存下来,不会把 TextView、RecyclerView 这种明显控件类名误当成前台 Activity。
如果你已经把自动化显示作用域锁到了某块 vdisplay 虚拟屏,currentActivity() 也会优先围绕那块屏来判断;当系统临时拿不到对应组件时,当前源码还会参考“最近一次启动到这块屏的 Activity 记录”做一次回退,所以它在虚拟屏场景下会比单纯盯主屏更稳一点。
最常见例子
log(currentActivity());
// 例如:com.ss.android.ugc.aweme.im.sdk.chat.ChatRoomActivity
判断是不是到了某个页面
const activityName = currentActivity();
if (activityName == "com.ss.android.ugc.aweme.im.sdk.chat.ChatRoomActivity") {
log("已经进入聊天页");
}
它和全局 activity 的区别
这两个名字非常像,但含义完全不同:
activity:是脚本当前能不能拿到一个真的Activity对象。currentActivity():是无障碍观察到的前台页面类名字符串。
所以这种写法是合理的:
log(activity); // 可能是 Activity 对象,也可能是 null
log(currentActivity()); // 可能是字符串类名,也可能是 null
自动化选择器全局入口
这一组全局函数本质上都来自 automator 模块。
它们的意义不是“额外又有一套选择器系统”,而是把最常用的选择器入口直接挂到全局,让你少写一层 selector().xxx(...)。
当前有哪些
| 全局函数 | 等价理解 | 说明 |
|---|---|---|
selector() | selector() | 拿一个空白 UiSelector |
text(value?) / desc(value?) / id(value?) | selector().text(...) 等 | 精确匹配 |
textContains(value) / descContains(value) / idContains(value) | selector().textContains(...) 等 | 包含匹配 |
textStartsWith(value) / descStartsWith(value) / idStartsWith(value) | selector().textStartsWith(...) 等 | 前缀匹配 |
textEndsWith(value) / descEndsWith(value) / idEndsWith(value) | selector().textEndsWith(...) 等 | 后缀匹配 |
textMatches(pattern) / descMatches(pattern) / idMatches(pattern) | selector().textMatches(...) 等 | 正则匹配 |
className(...) / classNameContains(...) / classNameStartsWith(...) / classNameEndsWith(...) / classNameMatches(...) | selector().className... | 类名匹配 |
packageName(...) / packageNameContains(...) / packageNameStartsWith(...) / packageNameEndsWith(...) / packageNameMatches(...) | selector().packageName... | 包名匹配 |
最关键的 3 条
- 它们每次都会返回一个新的
UiSelector,不会原地修改旧对象。 - 你后面还能继续链
clickable(true)、findOne()、exists()这类选择器方法。 - 详细的空参数规则、正则写法、
id(...)特例,统一去automator 自动化操作-模块看。
最常见写法
const loginButton = text("登录").clickable(true).findOne(3000);
const chatTab = descContains("消息").findOnce();
const settingsItems = idStartsWith("settings_").find();
什么时候直接跳去 automator 专页
- 你要确认
text()/id()省略参数时到底怎么解释 - 你要用正则匹配或组合过滤器
- 你要继续看
UiObject、UiObjectCollection、auto.*这些后续能力
自动化输入与滚动快捷函数
这一组全局函数同样来自 automator 模块。
它们和 UiObject.scrollUp()、UiObject.setText() 那套节点方法不是重复关系,而是“先帮你找目标控件,再代你执行动作”的快捷入口。
当前有哪些
| 全局函数 | 典型签名 | 返回值 | 默认行为 | 更适合什么时候用 |
|---|---|---|---|---|
scrollUp | scrollUp() / scrollUp(index) | boolean | 不传索引时自动挑一个可滚动控件 | 你只想让当前页面往上翻一段 |
scrollDown | scrollDown() / scrollDown(index) | boolean | 不传索引时自动挑一个可滚动控件 | 你只想让当前页面往下翻一段 |
setText | setText(text) / setText(index, text) | boolean | 不传索引时批量覆盖所有已命中的输入框 | 你想直接把输入框内容改成指定文本 |
input | input(text) / input(index, text) | boolean | 不传索引时给所有已命中的输入框追加文本 | 你想在现有文本后面继续补内容 |
先记住这 8 条
scrollUp()/scrollDown()找的是“当前页面里可滚动控件”,不是整机随便滑一下。- 不传索引时,源码会在候选可滚动控件里挑面积最大的那个;面积一样时再看更靠上的、再看更靠左的。
scrollUp()真正执行时会按scrollUp() -> scrollLeft() -> scrollBackward()这个顺序尝试;scrollDown()则是scrollDown() -> scrollRight() -> scrollForward()。- 传了索引以后,索引从
0开始;负数、越界或根本解析不成整数,都会直接返回false。 setText(text)不是“追加”,而是把目标输入框内容直接改成text。input(text)当前实现不是单独走“追加输入动作”,而是先读取现有文本,再重新setText(旧文本 + 新文本);某些控件要是旧文本读出来就是null,甚至可能拼出"nullxxx"。setText()/input()不传索引时会处理所有命中的输入框,而不是只处理第一个。- 这组函数找输入框时,会优先找
editable(true) + enabled(true) + visibleToUser(true)的节点;如果找不到,再回退到类名里包含EditText的启用节点。
scrollUp([index])
参数
| 参数 | 类型 | 可填值 | 默认行为 |
|---|---|---|---|
index | number / 数字字符串 | 0、1、2... | 省略时自动挑面积最大的可滚动控件 |
返回值
boolean
真实行为
- 先找当前搜索范围里的所有可滚动节点。
- 优先挑“可见且有面积”的滚动节点;如果这一步一个都没有,再退到普通
scrollable(true)节点。 - 传了
index时取第index + 1个候选。 - 不传
index时,从候选里选面积最大的那个。 - 选中节点后,不是只试一种动作,而是依次尝试:
target.scrollUp()target.scrollLeft()target.scrollBackward()
- 只要其中一种成功,就返回
true。
为什么会有“向左”这种回退
因为有些横向列表、轮播条、Tab 容器,本质上也是可滚动控件。
源码这里的“上滑”不是死盯屏幕方向,而是更接近“往前翻一页 / 往起始方向移动一下”的尝试序列。
示例
if (!scrollUp()) {
toast("当前页面没有找到可上滑的控件");
}
// 页面上有多个可滚动区域时,滑第一个候选
scrollUp(0);
// 比如左边是导航列表,右边是内容列表时,可以逐个试
for (let i = 0; i < 3; i++) {
if (scrollUp(i)) {
log(`第 ${i + 1} 个可滚动控件已尝试上滑`);
}
}
scrollDown([index])
参数
| 参数 | 类型 | 可填值 | 默认行为 |
|---|---|---|---|
index | number / 数字字符串 | 0、1、2... | 省略时自动挑面积最大的可滚动控件 |
返回值
boolean
真实行为
和 scrollUp() 的找控件逻辑相同,差别只在动作顺序:
target.scrollDown()target.scrollRight()target.scrollForward()
所以它更接近“往后翻一页 / 往结束方向移动一下”。
示例
while (scrollDown()) {
if (text("设置").exists()) {
break;
}
}
// 明确滑第二个可滚动区域
const ok = scrollDown(1);
log(ok ? "second list moved" : "second list not found");
setText([index, ]text)
参数
| 写法 | 含义 |
|---|---|
setText(text) | 把所有命中的输入框内容都改成 text |
setText(index, text) | 只改第 index + 1 个命中的输入框 |
参数规则
- 只有一个参数时,这个参数就会被当成文本。
- 两个及以上参数时,第一个参数会按索引解析,第二个参数会按文本解析。
text最终都会走toString();传null/undefined时会变成空字符串。- 索引解析失败时会按
-1处理,最终返回false。
返回值
boolean
真实行为
- 先找输入目标,优先顺序是:
editable(true) + enabled(true) + visibleToUser(true)classNameContains("EditText") + enabled(true) + visibleToUser(true)- 再退到不要求
visibleToUser(true)的同类条件
- 找到的候选会按节点身份去重,避免同一个输入框重复处理。
- 如果一个目标都找不到,直接返回
false。 - 传了索引时,只处理对应那一个输入框。
- 不传索引时,会尝试把所有命中的输入框都改成同一份文本。
- 批量模式下,只要有一个输入框设置失败,最终就返回
false。
这不是“追加输入”
setText("123");
上面这句的效果是“覆盖成 123”,不是在原有内容后面再加 123。
示例
setText("测试账号");
setText(0, "first field");
setText(1, "second field");
// 清空所有已命中的输入框
setText(null);
input([index, ]text)
参数
| 写法 | 含义 |
|---|---|
input(text) | 给所有命中的输入框都追加 text |
input(index, text) | 只给第 index + 1 个命中的输入框追加 text |
返回值
boolean
当前实现里“追加”到底怎么做
源码不是直接走一个独立的“append”动作,而是按下面的思路做:
- 先读出目标输入框当前
text() - 拼成
旧文本 + 新文本 - 再调用
setText(...)整段写回去
所以你可以把它理解成“语义上是追加,底层上是重新覆盖为拼接后的整段文本”。
这会带来什么影响
- 如果宿主输入框本身不支持
ACTION_SET_TEXT,它一样会失败。 - 如果控件当前文本拿不到,拼接结果取决于
text()读出来的值。 - 它不会保留系统输入法那种“逐字符输入”的过程,也不会触发你期待中的逐字键入效果。
示例
input(" world");
// 只给第二个输入框追加内容
input(1, " appended");
// 先覆盖,再补尾巴
setText(0, "hello");
input(0, " jsxhook");
什么时候直接跳去 automator 专页
- 你想继续看
UiObject.setText()、UiObject.scrollUp()这种节点级方法 - 你想知道这组函数最后操作到的是哪类
UiObject - 你想把选择器、输入框查找、滚动动作串成完整自动化流程
按键与系统动作快捷函数
这一组全局函数本质上都来自 keys 模块。
这里先帮你快速分清“能不能直接全局写”,详细参数、失败行为、KeyCode 解析规则再去 keys 专页看。
当前有哪些
| 全局函数 | 实际对应 | 说明 |
|---|---|---|
back() / home() / powerDialog() | keys.back() / keys.home() / keys.powerDialog() | 系统动作优先的返回、回桌面、电源菜单;其中 back() / home() 还能额外指定虚拟屏目标参数 |
notifications() / quickSettings() / recents() / splitScreen() | 对应 keys.* 同名方法 | 通知栏、快捷设置、最近任务、分屏 |
Home() / Back() / Power() / Menu() | 对应 keys.* 同名方法 | 直接发 KEYCODE_* |
VolumeUp() / VolumeDown() / Camera() | 对应 keys.* 同名方法 | 音量键、相机键 |
Up() / Down() / Left() / Right() / OK() | 对应 keys.* 同名方法 | 方向键与确认键 |
Text(text) | keys.Text(text) | 走 input text 注入文本 |
KeyCode(value) | keys.KeyCode(value) | 先解析 KeyCode 再派发 |
最容易混的 3 组
back()和Back()不是一回事:前者优先无障碍全局返回,后者是直接发KEYCODE_BACK。powerDialog()和Power()不是一回事:前者想打开电源菜单,后者只是发电源键。KeyCode(value)和keys.keyCode(value)不是一回事:前者会真的派发,后者只是解析整数键值。
再补一个这次接虚拟屏后很重要的细节:
这组全局快捷函数里,back() / home() / Back() / Home() / Text(...) / KeyCode(...) 等不少接口,现在都已经能吃可选的虚拟屏目标参数。这里只先讲最常用的 back() / home();更完整的矩阵请直接看 keys 按键模拟。
back()
返回上一级。
参数
可选 1 个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
displayIdOrSession | number | VDisplaySession | string | object | 不传时优先走系统返回;传了后会把返回键打到指定显示器。这里也支持 vdisplay-1 这类 session id 字符串和 { screen: screen } 这类 target 风格对象 |
返回值
boolean
真实行为
当前实现会按下面这个顺序尝试:
- 不传目标显示器时:先走无障碍全局动作
GLOBAL_ACTION_BACK - 上一步失败时:再回退到
input keyevent KEYCODE_BACK - 如果你传了虚拟屏目标参数:当前实现会直接派发
KEYCODE_BACK到那块显示器,而不是先走无障碍全局动作
所以它比 Back() 更像“我想触发系统返回”,而不是“我只想硬发一个返回键值”。
什么时候更适合用它
- 你只是想正常退回上一层页面
- 你优先希望系统自己处理返回语义
- 你当前脚本运行在无障碍可用环境里
示例
if (!back()) {
toastLog("返回失败");
}
const screen = vdisplay.create();
back(screen);
home()
回到桌面。
参数
可选 1 个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
displayIdOrSession | number | VDisplaySession | string | object | 不传时优先走系统回桌面;传了后会把 Home 键打到指定显示器。这里也支持 vdisplay-1 这类 session id 字符串和 { screen: screen } 这类 target 风格对象 |
返回值
boolean
真实行为
当前实现会先尝试:
- 不传目标显示器时:先尝试
GLOBAL_ACTION_HOME - 上一步失败时:再回退到
KEYCODE_HOME - 如果你传了虚拟屏目标参数:当前实现会直接派发
KEYCODE_HOME到那块显示器
示例
home();
sleep(400);
const screen = vdisplay.create();
home(screen);
powerDialog()
尝试打开系统电源菜单。
参数
无。
返回值
boolean
真实行为
- 优先走
GLOBAL_ACTION_POWER_DIALOG - 当前没有额外的
KEYCODE_*回退值
这也是它和 Power() 最大的区别。Power() 发的是电源键;powerDialog() 想要的是“直接打开电源菜单”。
示例
const ok = powerDialog();
log(`power dialog = ${ok}`);
notifications()
打开通知栏。
参数
无。
返回值
boolean
真实行为
当前实现会先尝试:
GLOBAL_ACTION_NOTIFICATIONS- 失败时再尝试
KEYCODE_NOTIFICATION
示例
notifications();
quickSettings()
打开快捷设置面板。
参数
无。
返回值
boolean
真实行为
- 主要依赖
GLOBAL_ACTION_QUICK_SETTINGS - 当前没有单独的 KeyEvent 回退值
示例
quickSettings();
recents()
打开最近任务。
参数
无。
返回值
boolean
真实行为
- 主要依赖
GLOBAL_ACTION_RECENTS - 当前没有额外的
KEYCODE_*回退值
示例
recents();
splitScreen()
尝试切换系统分屏。
参数
无。
返回值
boolean
平台限制
| 条件 | 行为 |
|---|---|
| Android 7.0 以下 | 直接返回 false |
| Android 7.0 及以上 | 尝试 GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN |
额外说明
它更依赖系统和当前宿主场景是否允许分屏,所以即使在高版本系统上,也不代表一定能成功。
示例
const ok = splitScreen();
log(`split screen = ${ok}`);
Home() / Back() / Power() / Menu()
这一组都是直接派发 KeyEvent 的全局快捷函数。
共同规则
- 都是无参函数
- 都直接走
input keyevent - 都返回
boolean - 都有对应的
keys.*模块写法
对应关系
| 全局函数 | 实际键值 |
|---|---|
Home() | KEYCODE_HOME |
Back() | KEYCODE_BACK |
Power() | KEYCODE_POWER |
Menu() | KEYCODE_MENU |
示例
Home();
Back();
VolumeUp() / VolumeDown() / Camera()
这一组也都是直接派发单个 KEYCODE_*。
对应关系
| 全局函数 | 实际键值 |
|---|---|
VolumeUp() | KEYCODE_VOLUME_UP |
VolumeDown() | KEYCODE_VOLUME_DOWN |
Camera() | KEYCODE_CAMERA |
返回值
都返回 boolean。
示例
VolumeUp();
sleep(100);
VolumeDown();
Up() / Down() / Left() / Right() / OK()
这是方向键和确认键这一组全局快捷函数。
对应关系
| 全局函数 | 实际键值 |
|---|---|
Up() | KEYCODE_DPAD_UP |
Down() | KEYCODE_DPAD_DOWN |
Left() | KEYCODE_DPAD_LEFT |
Right() | KEYCODE_DPAD_RIGHT |
OK() | KEYCODE_DPAD_CENTER |
返回值
都返回 boolean。
什么时候会用到
- 控制电视、车机、机顶盒这类方向键界面
- 某些宿主控件只能响应 D-Pad,而不是普通触摸点击
示例
Down();
Down();
OK();
Text(text)
这是 keys.Text(text) 的全局快捷写法。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
text | string | 要输入的文本 |
返回值
boolean
真实行为
当前实现走的是:
input text '...'
并且会先做两件编码处理:
- 百分号
%会转成%25 - 空格会转成
%s
特别说明
| 情况 | 结果 |
|---|---|
text == "" | 直接返回 true |
| shell 成功 | 返回 true |
| shell 失败 | 返回 false,同时记录一条 [keys] 警告日志 |
示例
Text("hello world");
Text("100% done");
KeyCode(value)
把一个值先解析成整数 KeyCode,再立刻派发出去。
参数
| 参数 | 类型 | 可填值 |
|---|---|---|
value | number | string | 数字、数字字符串、短键名、完整 KEYCODE_* 名 |
返回值
boolean
什么时候返回 false
- 你传的值根本解析不出 KeyCode
- shell 派发失败
示例
KeyCode("home");
KeyCode("KEYCODE_BACK");
KeyCode(24);
一个够用的例子
if (currentPackage() != "com.tencent.mm") {
home();
sleep(400);
}
Text("hello scriptx");
sleep(200);
Back();
什么时候直接跳去 keys 专页
- 你要确认某个方法有没有回退逻辑
- 你要知道
keys.keyCode(...)到底认哪些字符串 - 你要查
KEYCODE_*常量、文本转义和返回值细节
坐标自动化全局函数
这一组全局函数来自坐标自动化能力。
它们和节点语义化操作不是替代关系,更像是你在“知道点哪里、滑哪里”的时候,直接给脚本一条坐标级入口。
当前有哪些
| 全局函数 | 路径特点 | 更适合什么时候用 |
|---|---|---|
click(x, y) | 无障碍手势点击 | 你还在无障碍体系里,想做一次普通点击 |
longClick(x, y) | 无障碍长按 | 明确要长按某个坐标 |
press(x, y, duration) | 无障碍按住指定毫秒 | 需要自己控制按住时长 |
swipe(x1, y1, x2, y2, duration) | 无障碍手势滑动 | 需要稳定地走手势轨迹 |
Tap(x, y) | 更偏 root / input tap | 你想走更接近系统输入的点击 |
Swipe(x1, y1, x2, y2, duration?) | 更偏 root / input swipe | 你想走更接近系统输入的滑动 |
最容易混的 2 组
click(...)和Tap(...)不是大小写风格区别,而是两条不同的执行路径。swipe(...)和Swipe(...)也不是一回事;前者要求明确时长,后者当前支持省略时长并回退默认值。
一个够用的例子
const x = 540;
const y = 1200;
if (!click(x, y)) {
Tap(x, y);
}
什么时候直接跳去 coordinates 专页
- 你要确认参数是不是只能传有限数字
- 你要看
Tap / Swipe和无障碍手势版的真实差异 - 你要配合
UiObject.bounds()、UiRect.centerX()这些 API 一起用
详细规则统一在这里:
运行时暂停控制
这一组平时用得没有 sleep()、setTimeout() 那么频繁,但它们确实是源码里已经注入好的全局函数。
总表
| 函数 | 典型签名 | 返回值 | 说明 |
|---|---|---|---|
pausePoint | pausePoint() | null | 在这里显式让脚本检查并响应“暂停”状态 |
isPaused | isPaused() | boolean | 读取当前运行时是否处于暂停状态 |
pausePoint()
它本质上是一个“显式检查暂停点”。
怎么理解
- 如果当前运行时没有被暂停,它就直接过去
- 如果当前运行时处于暂停状态,它会在这里等
这类函数更适合你在长循环、长扫描、批量处理里,自己插一个“可暂停的检查点”。
for (let i = 0; i < 1000; i++) {
pausePoint();
// 你的长耗时逻辑
}
isPaused()
返回当前运行时是不是正处于暂停状态。
返回值
boolean
if (isPaused()) {
log("脚本当前处于暂停态");
}
图像全局快捷函数
这一组不是另一套单独实现,而是 images 模块额外挂出来的全局别名。
当前有哪些
requestScreenCapture(...)requestScreenCaptureAsync(...)captureScreen(...)findColor(...)findColorInRegion(...)findColorEquals(...)findMultiColors(...)findImage(...)findImageInRegion(...)
怎么理解最不容易乱
你可以先把它们直接理解成:
images.requestScreenCapture(...)
images.captureScreen(...)
images.findColor(...)
images.findImage(...)
这一组最该先知道的 3 点
requestScreenCaptureAsync()当前实现和requestScreenCapture()是同一个入口,返回值仍然是boolean。captureScreen(...)默认走 MediaProjection;但如果你传了虚拟屏目标参数,或者当前自动化显示作用域已经锁到某块虚拟屏,它也可能直接截那块虚拟屏。- 完整参数、可填值、返回对象和示例已经单独整理到 images 图像与截图。
最常见的写法
if (!requestScreenCapture()) {
toastLog("没有拿到截图权限");
exit();
}
const screen = captureScreen();
try {
const p = findColor(screen, "#ffffff", {
threshold: 6
});
log(p);
} finally {
screen.recycle();
}
const vScreen = vdisplay.create();
const img = captureScreen(vScreen);
try {
log(`${img.width()}x${img.height()}`);
} finally {
img.recycle();
}
随机数
random()
支持写法
| 写法 | 返回值类型 | 范围 |
|---|---|---|
random() | number | 0 <= x < 1 的小数 |
random(max) | number | 0 到 max 之间的整数,包含两端 |
random(min, max) | number | min 到 max 之间的整数,包含两端 |
细节规则
random()
返回 Kotlin Random.nextDouble(),也就是一个:
0 <= x < 1
的小数。
random(max)
max必须是数字,否则抛错。- 返回的是整数。
- 边界包含
0和max。 - 如果你传的是负数,源码会自动把范围看成“较小值到较大值”,也就是会变成
max ~ 0。
log(random());
log(random(5)); // 可能是 0,1,2,3,4,5
log(random(-3)); // 可能是 -3,-2,-1,0
random(min, max)
min、max都必须是数字,否则抛错。- 返回的是整数。
- 边界包含两端。
min和max写反了也没关系,源码会自动交换。
log(random(3, 8)); // 3~8
log(random(8, 3)); // 仍然是 3~8
可能报错
random(max) requires a numeric max
random(min, max) requires numeric bounds
定时器与任务
总表
| 函数 | 典型签名 | 返回值 | 默认值 / 特殊规则 | 说明 |
|---|---|---|---|---|
setTimeout | setTimeout(fn, delayMs?) | number | delayMs 非数字按 0;负数按 0 | 单次执行 |
clearTimeout | clearTimeout(id) | boolean | id 非数字直接返回 false | 取消单次任务 |
setInterval | setInterval(fn, delayMs?) | number | delayMs 非数字按 0;负数按 0 | 周期执行 |
clearInterval | clearInterval(id) | boolean | id 非数字直接返回 false | 取消周期任务 |
setImmediate | setImmediate(fn, ...args) | number | 当前等价于当前线程上的 setTimeout(fn, 0, ...args) | 即时调度 |
clearImmediate | clearImmediate(id) | boolean | id 非数字直接返回 false | 取消即时调度 |
Task | Task(fn, delayMs?, ...args) | number | 当前等价于一次性 setTimeout | 任务别名 |
setTimeout(fn, delayMs?, ...args)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
fn | function | 回调函数 | 必填 | 不传会抛错 |
delayMs | number | 任意数字 | 0 | 延迟毫秒数 |
...args | any | 任意值 | 无 | 额外传给回调的参数 |
规则
fn不是函数时直接抛错。delayMs非数字时按0。delayMs负数时也会被修正成0。- 返回值是任务
id,后面用于clearTimeout(id)。 - 如果你额外传了参数,它们会按顺序继续传给回调。
- 它会挂到“当前脚本线程”上;如果你现在就在
threads.start(...)的工作线程里,定时器也会挂在那个工作线程里。
const id = setTimeout(() => {
log("run once");
}, 1000);
setTimeout(function (name, count) {
log(name, count);
}, 100, "demo", 3);
clearTimeout(id)
返回规则
| 场景 | 返回值 |
|---|---|
| 成功取消已存在任务 | true |
id 不存在 | false |
id 不是数字 | false |
const id = setTimeout(() => log("x"), 5000);
clearTimeout(id);
setInterval(fn, delayMs?, ...args)
和 setTimeout 基本同规则,但会循环执行。
特别说明
- 只要脚本没中断、任务没被取消,就会继续调度下一轮。
- 如果回调里触发脚本中断,定时器会停止。
- 额外参数同样会继续传进回调。
const id = setInterval(() => {
log("tick");
}, 1000);
const id2 = setInterval(function (label) {
log(label);
}, 500, "heartbeat");
clearInterval(id)
规则和 clearTimeout(id) 一样,也是返回 boolean。
clearInterval(id);
setImmediate(fn, ...args)
尽快在当前脚本线程里安排一个回调执行。
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
fn | function | 回调函数 | 必填 | 不传会抛错 |
...args | any | 任意值 | 无 | 额外传给回调的参数 |
返回值
number
规则
- 当前实现本质上等价于“当前线程上的
setTimeout(fn, 0, ...args)”。 - 它不是 Promise 微任务,也不是浏览器事件循环里的
queueMicrotask。 - 如果你需要显式把任务挂到某个线程,优先用对应的
thread.setImmediate(...)。
setImmediate(function (text) {
log(text);
}, "run soon");
clearImmediate(id)
取消通过 setImmediate(...) 创建的任务。
返回值
| 场景 | 返回值 |
|---|---|
| 成功取消已存在任务 | true |
id 不存在 | false |
id 不是数字 | false |
const id = setImmediate(() => log("x"));
clearImmediate(id);
Task(fn, delayMs?, ...args)
这一项最容易被写错。
当前源码里:
Task(fn, delayMs?, ...args)
本质上就是一次性的:
setTimeout(fn, delayMs, ...args)
也就是说:
- 它不是任务对象类
- 也不是带复杂
options的构造器 - 返回值同样是任务
id - 回调额外参数也一样支持
Task(() => {
log("run once");
}, 800);
Task(function (name) {
log(name);
}, 50, "task-alias");
sync(func, lock?)
把一个函数包装成“同一时刻只允许一个线程进来执行”的同步函数。
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
func | function | 任意函数 | 无 | 要包装的函数,必填 |
lock | threads.lock() 返回的锁对象 | ReentrantLock 句柄 | 自动新建锁 | 可选共享锁 |
返回值
function
规则
- 如果不传
lock,运行时会自己创建一把新的ReentrantLock。 - 如果多个函数要共享同一把锁,就把同一个
lock传进去。 - 包装后的函数在执行前会先拿锁,执行结束后自动放锁。
const lock = threads.lock();
let value = 0;
const addOne = sync(function () {
value += 1;
return value;
}, lock);
log(addOne());
log(addOne());
可能报错
setTimeout requires a function callback
setInterval requires a function callback
setImmediate requires a function callback
Task requires a function callback
sync(func) requires a function
Shell、项目与加载
总表
| 函数 | 典型签名 | 返回值 | 默认值 / 特殊规则 | 说明 |
|---|---|---|---|---|
shell | shell(command) | object | 空命令也会尝试执行 | 执行 shell |
loadDex | loadDex(path) | boolean | 相对路径按当前项目根目录解析 | 加载外部 Dex |
getProjectDir | getProjectDir(suffix?) | string | 无参数返回项目根目录 | 获取当前项目目录 |
require | require(path) | any | null | 自动补 .js | 载入项目内部模块 |
shell(command)
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
command | string | 任意 shell 命令文本 | "" | 执行命令 |
返回对象字段
| 字段 | 类型 | 说明 |
|---|---|---|
stdout | string | 标准输出 |
success | boolean | 是否执行成功 |
stderr | string | 标准错误 |
mode | string | 当前 shell 模式 |
mode 常见值
当前源码里可能出现:
ROOTSHIZUKUSHIZUKU_FALLBACKUSERNONE
const result = shell("id");
log(JSON.stringify(result, null, 2));
loadDex(path)
规则
path为空时返回false- 绝对路径:直接使用
- 相对路径:自动拼到当前项目目录下
- 文件不存在时返回
false - 文件存在且可读时,加载成功返回
true
loadDex("/sdcard/Download/demo.dex");
loadDex("libs/demo.dex"); // 相对项目根目录
getProjectDir(suffix?)
这个函数只在“当前脚本属于项目”时才有意义。
规则
| 调用方式 | 返回值 |
|---|---|
getProjectDir() | 项目根目录字符串,结尾通常带 / |
getProjectDir("libs/demo.js") | 项目根目录 + 你传的后缀 |
log(getProjectDir());
log(getProjectDir("libs/util.js"));
require(path)
require(...) 只负责加载当前项目里的脚本模块,不是 Node.js 那套完整模块系统。
最稳的理解方式是:它会按项目根目录去找一个 .js 文件,执行它,然后把 module.exports 的结果返回给你。
先记住这 5 件事
- 只有当前脚本属于项目时才可用。
- 路径按当前项目目录理解,常见写法是
./lib/util、lib/util.js。 - 你没写
.js时,源码会自动补上。 - 同一个模块名重复
require时,会优先返回缓存,不会重复执行。 - 模块文件可以通过
exports或module.exports导出内容。
它实际怎么跑
源码会把模块文件包成一个函数执行,形式大致是:
(function(exports, module) {
// 你的模块代码
return module.exports;
})
所以你在模块文件里可以直接写:
exports.foo = function() {};
module.exports = { foo: function() {} };
最常见的两种导出方式
1. 导出一个工具对象
lib/helper.js
exports.sum = function(a, b) {
return a + b;
};
exports.clamp = function(value, min, max) {
return Math.max(min, Math.min(max, value));
};
main.js
const helper = require("./lib/helper");
log(helper.sum(3, 5));
log(helper.clamp(12, 0, 10));
2. 导出一个函数
lib/hook-login.js
module.exports = function installLoginHook() {
log("login hook module loaded");
};
main.js
const installLoginHook = require("./lib/hook-login");
installLoginHook();
重复加载会走缓存
同一个模块名重复 require,会直接返回缓存结果。
这意味着它很适合放公共工具,不用担心在多个地方重复执行初始化逻辑。
const helper1 = require("./lib/helper");
const helper2 = require("./lib/helper");
log(helper1 === helper2);
这页不再展开模块系统细节
为了避免 require(...)、exports、module.exports 在 API 区出现双份正文,这里现在只保留一个快速结论:
require(...)负责加载当前项目目录里的脚本模块- 支持
exports.xxx = ...和module.exports = ... - 重复加载会命中缓存
- 文件不存在、内容为空或当前不在项目环境时,会返回
null
真正的完整说明统一放到这里:
如果你现在最想搞清楚的是下面这些细节,就直接跳过去:
- 路径标准化到底怎么做
- 会不会自动补
.js - 缓存什么时候生效
exports和module.exports有什么区别- 为什么项目模块和 Node.js 模块用起来不完全一样
Hook 与反射入口
这一组函数名确实属于“全局函数”,但它们已经在专页里详细展开了。
为了避免在 API 区域里出现双份说明,这里只保留分流关系:
属于 Hook 专页的
hookhookAllhookctorreplace
这些函数的参数签名、before / after / replace 回调、重载匹配、构造函数 Hook 写法,统一只在这里详细讲:
补一个这次实现更新后的短结论:
hook / hookAll / replace这类字符串入口,现在会先把 Rhino / Java 包装值解包,再转成真正的字符串去查找- 所以像 DexKit 结果里的
hit.name,现在通常可以直接传给这些入口
属于反射专页的
imports/importClassfindClasscallStaticMethodcallMethodinvokecreateProxynewnewArrayByteArraygetField/setFieldgetStaticField/setStaticFieldprintFields
这些函数的参数匹配、类加载器、字段读写、构造对象和数组规则,统一只在这里详细讲:
补两个容易漏掉但很重要的点:
importClass当前只是imports的同义入口,不是另一套独立实现imports("java.lang.String")这类调用成功后,会把String这个简单类名挂到当前作用域和运行时全局作用域里,后面就能直接写String
这里也先记一个短结论,避免你翻页前搞混:
callStaticMethod / callMethod / invoke的methodName,现在支持先解包再转字符串getField / setField / getStaticField / setStaticField的fieldName也是同一套行为- 但真正传给 Java 方法的
...args还是按原来的类型规则匹配,不会自动把"1"变成int createProxy(interfaces, callbackOrCallbacks, classLoader?)用来在 JS 里临时实现 Java 接口,详细规则也统一放在反射专页
DexKit 与注入入口
这几项也都是直接注入到全局作用域的函数,只是它们已经在更合适的专页展开了,这里先帮你分清用途。
openDexKit(apkPath?)
- 作用:按 APK 路径打开 DexKit 检索桥。
- 最常见写法:
openDexKit()、openDexKit("/data/app/.../base.apk") - 适合:你更关心“当前 APK 文件里有什么”。
openDexKitByClassLoader(useMemoryDexFile = false)
- 作用:按当前
lpparam.classLoader这条加载链去打开 DexKit。 - 最常见写法:
openDexKitByClassLoader(false) - 适合:你更关心“运行时真实加载到了哪些 dex”。
injectActivity(...)
- 作用:拉起一个承载脚本的注入页面。
- 常见写法:
injectActivity(scriptText)、injectActivity(context, scriptText) - 适合:你要临时开一个带脚本内容的页面,做调试或宿主内展示。
最推荐的跳转方式
- DexKit 入口差异、桥对象方法、结果对象读法:看 dexkit 静态检索
injectActivity(...)的两种签名和实战写法:看 运行时与 Hook
宿主对话框
三个名字,其实是一套实现
源码里:
showGlobalDialogshowHostDialogconfirm
这三个最终都走同一套宿主对话框实现。
最常见写法
confirm({
title: "Confirm",
message: "Continue?",
positiveText: "OK",
negativeText: "Cancel",
onConfirm() {
toastLog("confirmed");
},
onCancel() {
toastLog("canceled");
}
});
支持字段
| 字段 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
title / heading | string | 任意文本 | 无 | 标题 |
message / content / body / text | string | 任意文本 | 无 | 内容文本 |
positiveText / confirmText / okText | string | 任意文本 | 无 | 确认按钮文本 |
negativeText / cancelText | string | 任意文本 | 无 | 取消按钮文本 |
cancelable | boolean | true / false | true | 是否可取消 |
onConfirm / onPositive / onOk | function | 回调函数 | 无 | 正向按钮回调 |
onCancel / onNegative | function | 回调函数 | 无 | 取消按钮回调 |
最低要求
title 和 message 里至少要有一个,否则会报错:
showGlobalDialog requires at least a title or a message
重要说明
- 这不是同步
confirm(),不会直接返回true / false。 - 当前是回调式 API,点击后通过回调通知你。
函数和对象怎么分工
你可以先这样理解:
- 一把梭的直接调用:通常是全局函数
- 需要维持状态或分模块组织:通常是全局对象
例如:
hook()是函数http.get()是对象方法files.read()是对象方法imgui.window()是对象方法mcpServer.tool()是对象方法
如果你想按“当前运行时里到底注入了哪些对象”来理解,请接着看:
