🪝 内存读写模块
内存读写模块 - memory
限制
- 此模块并不随附在 XXTouchNG 中。
- 受限于你所在地区的法律法规,你可能无法取得、使用此模块。
数据类型
整形
I8有符号 8 位整数U8无符号 8 位整数I16有符号 16 位整数U16无符号 16 位整数I32有符号 32 位整数U32无符号 32 位整数I64有符号 64 位整数U64无符号 64 位整数
浮点型
F3232 位浮点数F6464 位浮点数
获取运行中 App 的进程号 (memory.get_process_id)
声明
进程号 = memory.get_process_id(标识符)
参数及返回值
- 标识符
- 文本型,App 标识符
- 进程号
- 整数型,如果 App 正在运行,则返回其进程号,否则返回
0
- 整数型,如果 App 正在运行,则返回其进程号,否则返回
说明
效果等同于 app.pid_for_bid。
提示
若要获取前台应用的进程号,请使用 app.front_pid。
示例
memory.get_process_id
local app_proc_id = memory.get_process_id("info.pwfmfx.VividCounter")
sys.alert(app_proc_id)
设置内存搜索范围 (memory.set_search_mode)
声明
memory.set_search_mode(搜索范围)
参数及返回值
- 搜索范围 枚举型
0快速搜索,仅搜索堆、栈段1全局搜索,搜索所有内存段
说明
默认情况下,内存搜索范围为 0,即快速搜索。
示例
memory.set_search_mode
memory.set_search_mode(1) -- 全局搜索
搜索 App 内存 (memory.search)
声明
搜索结果表, 失败原因 = memory.search(进程号, 是否开始新一轮搜索, 起始偏移地址, 搜索条件表, 全局搜索数据类型[, 最大返回结果数量])
参数及返回值
- 进程号
- 整数型,App 进程号
- 是否开始新一轮搜索
- 布尔型,
true表示开始新一轮搜索,false表示在 上一轮 搜索结果基础上继续搜索
- 布尔型,
- 起始偏移地址 整数型
- 搜索条件表
- 顺序表,搜索条件表中的每一项都是一个搜索条件 关联表,搜索条件表中的搜索条件将会被逐一应用于搜索结果,以缩小搜索范围。搜索条件 关联表 的键值对如下:
offset整数型,搜索条件的偏移地址lv整数型,搜索条件左值hv整数型,搜索条件右值type数据类型
- 顺序表,搜索条件表中的每一项都是一个搜索条件 关联表,搜索条件表中的搜索条件将会被逐一应用于搜索结果,以缩小搜索范围。搜索条件 关联表 的键值对如下:
- 全局搜索数据类型
- 数据类型,若 搜索条件表 中某一项搜索条件的
type为nil,则使用此参数作为该搜索条件的数据类型
- 数据类型,若 搜索条件表 中某一项搜索条件的
- 最大返回结果数量
- 整数型,可选,默认为
9999。可设置为0,表示不限制返回结果数量
- 整数型,可选,默认为
- 失败原因 文本型
- 搜索结果表
- 整数型顺序表,其中每一项都是一个符合条件的内存地址,操作失败则为
nil
- 整数型顺序表,其中每一项都是一个符合条件的内存地址,操作失败则为
搜索条件表结构
{
-- 以下 顺序表 填入几个搜索几次,大于一次即为联合搜索
-- 第一次查询搜索数据:以 “无符号8位整数” 的数据类型过滤出大于 1,小于 10 的数据地址(首次搜索 offset 无作用)
{
lv = 1, -- 模糊搜索,搜索内容 >= lv 值,不填 hv 为精确搜索,必填
hv = 10, -- 模糊搜索,搜索内容 <= hv 值,不填 hv 为精确搜索,可不填
type = "U8" -- 数据类型,指定这条数据用 “无符号8位整数” 的类型进行搜索,可不填,默认与 “全局搜索类型” 参数一致
},
-- 第二次过滤搜索数据:以 “有符号8位整数” 的数据类型过滤出大于 2,小于 10, 且相对于上一个数据偏移量为 100 字节的数据地址
{
lv = 2, -- 模糊搜索,搜索内容 >= lv 值,不填 hv 为精确搜索,必填
hv = 10, -- 模糊搜索,搜索内容 <= hv 值,不填 hv 为精确搜索,可不填
offset = 100, -- 可不填,默认为 0,不能为负数
type = "I8" -- 数据类型,指定这条数据用 “有符号8位整数” 的类型进行搜索,可不填,默认与 “全局搜索类型” 参数一致
},
...
}
说明
- 首轮搜索需要指定 是否开始新一轮搜索 为
true,以开启新一轮搜索。 - 后续搜索需要指定 是否开始新一轮搜索 为
false,以在上一轮搜索的基础上继续搜索。
这样做,保证了多次搜索的结果是上一次搜索的结果的子集,即第一次搜索的结果是所有符合条件的数据,第二次搜索的结果 筛选出 第一次搜索结果中符合条件的数据,以此类推。直至搜索结果数量足够小,或者搜索条件不再满足,即可停止搜索。
联合搜索
- 联合搜索通常用于在内存中搜索出符合多个条件特征的 结构体。
- 若 搜索条件表 中只有一项,则为普通搜索,若 搜索条件表 中有多项,则为联合搜索。
- 搜索条件表 中的每一项都是一个搜索条件,每个搜索条件都会在满足上一个搜索条件的基础上进行检查,如果条件不满足,则将数据从搜索结果中剔除。
- 联合搜索的搜索条件为所有搜索条件的交集。
本节示例
重置搜索结果 (memory.reset_search)
声明
memory.reset_search()
说明
- 清空搜索结果列表,并释放其占用的内存空间。
- 重置搜索结果后,可以重新开始新一轮搜索。
获取 App 内存基址 (memory.get_base_address)
声明
内存基址 = memory.get_base_address(进程号)
参数及返回值
- 进程号
- 整数型,App 进程号
- 内存基址 整数型
读取 App 内存 (memory.read)
声明
值, 失败原因 = memory.read(进程号, 内存地址, 数据类型)
参数及返回值
说明
以 数据类型,读取指定 App、指定内存地址上的数值。
限制
由于 Lua 语言的限制,即使你指定了 无符号 64 位整数,即 U64 类型的数据,也将会被强制转换为 I64 类型的数据读出,你可能需要自行将取出的负数值转换为无符号数值。
本节示例
写入 App 内存 (memory.write)
声明
操作成败, 失败原因 = memory.write(进程号, 内存地址, 数据类型, 要写入的数值)
参数及返回值
说明
以 数据类型,向指定 App、指定内存地址上写入数值。
警告
传入的 要写入的数值 将会被强行转换为 数据类型。
本节示例
示例代码
模糊搜索缩小范围
本示例代码演示了如何使用 memory.search 多次搜索缩小范围,最终找到计数器指定的内存地址进行修改。
你需要先从 App Store 中搜索 “Vivid Counter”,安装并打开此应用,方可运行本示例。
普通搜索
--
-- 获取运行中的 App 的进程号
local pid = memory.get_process_id("info.pwfmfx.VividCounter")
assert(pid > 0, "找不到正在运行的 “Vivid Counter”。")
sys.msleep(1000)
--
-- 通过光学字符识别,获取当前计数器的数值
local texts = screen.ocr_text { top = 60, bottom = -60 }
local counter = tonumber(texts[1])
assert(counter, "无法识别当前计数器的数值。")
assert(counter > 0, "当前计数器的数值为 0,请先随便点击几次屏幕后再试。")
assert(counter < 9999, "当前计数器的数值已达上限,请重置计数器再试。")
sys.alert("屏幕识别到当前计数器数值为:" .. tostring(counter) .. ",如果正确,请点击 “好” 继续。")
sys.msleep(1000)
--
-- 在内存中搜索出当前计数器的数值
local data, err
data, err = memory.search(
pid,
true, -- 开始新的搜索
0, -- 从头开始搜索
{ { lv = counter } }, -- 普通搜索
"U64",
0 -- 不限制最大返回结果数量
)
assert(data, err)
sys.alert("开始新的搜索,找到 " .. #data .. " 个结果。")
sys.msleep(1000)
--
-- 开始缩小范围
local loopTimes = 0
while #data > 1 and loopTimes < 5 do
--
-- 随便点击几次屏幕,增加计数器的数值
local tapTimes = math.random(20)
for i=1,tapTimes do
touch.tap(100, 100)
sys.msleep(500)
end
sys.msleep(1000)
counter = counter + tapTimes
sys.alert("当前计数器数值应该为:" .. tostring(counter) .. ",如果正确,请点击 “好” 继续。")
--
-- 在内存中再次搜索当前计数器的数值
data, err = memory.search(
pid,
false, -- 继续上次的搜索
0, -- 从头开始搜索
{ { lv = counter } }, -- 普通精确搜索
"U64",
0 -- 不限制最大返回结果数量
)
assert(data, err)
sys.alert("第 " .. tostring(loopTimes + 2) .. " 轮搜索,找到 " .. #data .. " 个结果。")
sys.msleep(1000)
--
loopTimes = loopTimes + 1
end
--
-- 检查并确保搜索结果只有一个
sys.msleep(1000)
assert(#data == 1, "无法定位到计数器所在的内存地址。")
--
-- 读取并打印搜索到的内存地址数值是否和预期一致
local value
value, err = memory.read(pid, data[1], "U64")
assert(value, err)
assert(value == counter, "计数器数值与预期不符。")
sys.alert("计数器数值符合预期,点击 “好” 以修改它。")
sys.msleep(1000)
--
-- 写入新的数值为 9998
local ok
ok, err = memory.write(pid, data[1], "U64", 9998)
assert(ok, err)
sys.alert("计数器内存数值已被修改为 9998,点击 “好” 以点击屏幕使其刷新显示。")
sys.msleep(1000)
--
-- 再次点击屏幕,使得写入的数值生效
touch.tap(100, 100)
sys.msleep(1000)
--
-- 此时,计数器的数值应该是 9999
-- 通过光学字符识别,获取当前计数器的数值
texts = screen.ocr_text { top = 60, bottom = -60 }
counter = tonumber(texts[1])
assert(counter == 9999, "计数器数值与预期不符。")
sys.alert("屏幕识别到当前计数器的数值与预期相符,点击 “好” 以结束本示例。")
联合搜索结构体
已知 iPhone 8 的屏幕宽度为 750,高度为 1334。
本示例希望通过 screen.image 获取屏幕内容,得到 图片对象 并赋值给某一变量,然后通过内存联合搜索的方式去修改此 图片对象,使其高度减半。
首先,已知 XXTouchNG 用于截取屏幕图像的缓冲区结构体如下:
struct JST_IMAGE {
uint8_t orientation;
int32_t width;
int32_t alignedWidth;
int32_t height;
void *pixels;
uint8_t isDestroyed;
};
因此,我们需要在脚本进程的内存中搜索出所有符合以下条件的结构体:
{
{ lv = 750, type = "I32" }, -- width
{ lv = 750, hv = 800, type = "I32", offset = 4 }, -- alignedWidth
{ lv = 1334, type = "I32", offset = 8 }, -- height
}
示例代码如下:
联合搜索
--
-- 进行一次屏幕截图,将图片对象赋值给一个变量
local img = screen.image()
--
-- 获取脚本进程的进程号
local posix = require("posix")
local pid = posix.getpid().pid
--
-- 搜索所有符合条件的结构体
local data, err
data, err = memory.search(pid, true, 0, {
{ lv = 750, type = "I32" },
{ lv = 750, hv = 800, type = "I32", offset = 4 },
{ lv = 1334, type = "I32", offset = 8 },
}, "U8", 10)
assert(data, err)
assert(#data > 0, "无法搜索到符合条件的结构体。")
--
-- 遍历找到的结构体
local orientation, width, alignedWidth, height, pixels, isDestroyed
for i, v in ipairs(data) do
--
-- 读取结构体的各个字段
orientation, err = memory.read(pid, v - 4, "U8")
assert(orientation, err)
width, err = memory.read(pid, v, "I32")
assert(width, err)
alignedWidth, err = memory.read(pid, v + 4, "I32")
assert(alignedWidth, err)
height, err = memory.read(pid, v + 8, "I32")
assert(height, err)
pixels, err = memory.read(pid, v + 12, "U64")
assert(pixels, err)
isDestroyed, err = memory.read(pid, v + 20, "U8")
assert(isDestroyed, err)
--
-- 打印结构体的各个字段
sys.alert(string.format(
"第 %d 个结构体:\n" ..
"orientation = %d\n" ..
"width = %d\n" ..
"alignedWidth = %d\n" ..
"height = %d\n" ..
"pixels = %d\n" ..
"isDestroyed = %d",
i, orientation, width, alignedWidth, height, pixels, isDestroyed
))
--
-- 修改指定的图像高度为原来的一半
if isDestroyed == 0 and height == 1334 then
local ok
ok, err = memory.write(pid, v + 8, "I32", 667)
assert(ok, err)
sys.alert("已修改第 " .. i .. " 个结构体的 height 字段为 667。")
end
end
--
-- 检查图片对象是否修改成功
local _, h = img:size()
assert(h == 667, "图片对象的高度未修改成功。")
img:save_to_album()
sys.alert("图片对象的高度已修改成功,点击 “好” 以结束本示例。")