Global Functions 全局函数
Global Functions 全局函数
这一页专门讲“运行时直接注入到全局作用域的函数”。我会尽量按源码实际行为来写,不只列名字,还把这些点写清楚:
- 传什么参数才算合法。
- 不传参数时默认会怎样。
- 返回值到底是什么。
- 哪些是别名,哪些只是看起来像别名。
- 传错类型时会不会报错。
先记住这 13 条
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对象。
日志与基础控制
总表
| 函数 | 典型签名 | 返回值 | 默认值 / 特殊规则 | 说明 |
|---|---|---|---|---|
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。
没开无障碍时会怎样
这组函数都依赖可用的无障碍服务。
如果当前服务没启动,源码会直接抛:
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("当前前台已经是抖音");
}
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。
最常见例子
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
运行时暂停控制
这一组平时用得没有 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();
}
随机数
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);
什么时候会返回 null
- 当前脚本不属于项目
- 你传的模块文件不存在
- 模块文件读取出来是空内容
一个很实用的小技巧
如果你不确定路径对不对,先拿 getProjectDir(...) 打日志看最终路径,再拼给 require(...):
const helperPath = getProjectDir("lib/helper.js");
log(helperPath);
const helper = require("./lib/helper");
例子
const util = require("./lib/util");
const api = require("api/client.js");
Hook 与反射入口
这一组函数名确实属于“全局函数”,但它们已经在专页里详细展开了。
为了避免在 API 区域里出现双份说明,这里只保留分流关系:
属于 Hook 专页的
hookhookAllhookctorhookcotrreplace
这些函数的参数签名、before / after / replace 回调、重载匹配、构造函数 Hook 写法,统一只在这里详细讲:
补一个这次实现更新后的短结论:
hook / hookAll / replace这类字符串入口,现在会先把 Rhino / Java 包装值解包,再转成真正的字符串去查找- 所以像 DexKit 结果里的
hit.name,现在通常可以直接传给这些入口
属于反射专页的
imports/importClassfindClasscallStaticMethodcallMethodinvokenewnewArrayByteArraygetField/setFieldgetStaticField/setStaticFieldprintFields
这些函数的参数匹配、类加载器、字段读写、构造对象和数组规则,统一只在这里详细讲:
这里也先记一个短结论,避免你翻页前搞混:
callStaticMethod / callMethod / invoke的methodName,现在支持先解包再转字符串getField / setField / getStaticField / setStaticField的fieldName也是同一套行为- 但真正传给 Java 方法的
...args还是按原来的类型规则匹配,不会自动把"1"变成int
宿主对话框
三个名字,其实是一套实现
源码里:
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()是对象方法
如果你想按“当前运行时里到底注入了哪些对象”来理解,请接着看:
