跳到主要内容
版本:3.0.3

🪝 内存读写模块

内存读写模块 - memory

限制
  • 此模块并不随附在 XXTouchNG 中。
  • 受限于你所在地区的法律法规,你可能无法取得、使用此模块。

数据类型

整形

  • I8 有符号 8 位整数
  • U8 无符号 8 位整数
  • I16 有符号 16 位整数
  • U16 无符号 16 位整数
  • I32 有符号 32 位整数
  • U32 无符号 32 位整数
  • I64 有符号 64 位整数
  • U64 无符号 64 位整数

浮点型

  • F32 32 位浮点数
  • F64 64 位浮点数

获取运行中 App 的进程号 (memory.get_process_id)

声明

进程号 = memory.get_process_id(标识符)

参数及返回值

  • 标识符
  • 进程号
    • 整数型,如果 App 正在运行,则返回其进程号,否则返回 0

说明

效果等同于 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(进程号, 是否开始新一轮搜索, 起始偏移地址, 搜索条件表, 全局搜索数据类型[, 最大返回结果数量])

参数及返回值

  • 进程号
  • 是否开始新一轮搜索
    • 布尔型true 表示开始新一轮搜索,false 表示在 上一轮 搜索结果基础上继续搜索
  • 起始偏移地址 整数型
  • 搜索条件表
    • 顺序表,搜索条件表中的每一项都是一个搜索条件 关联表,搜索条件表中的搜索条件将会被逐一应用于搜索结果,以缩小搜索范围。搜索条件 关联表 的键值对如下:
      • offset 整数型,搜索条件的偏移地址
      • lv 整数型,搜索条件左值
      • hv 整数型,搜索条件右值
      • type 数据类型
  • 全局搜索数据类型
    • 数据类型,若 搜索条件表 中某一项搜索条件的 typenil,则使用此参数作为该搜索条件的数据类型
  • 最大返回结果数量
    • 整数型可选,默认为 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()

说明

  • 清空搜索结果列表,并释放其占用的内存空间。
  • 重置搜索结果后,可以重新开始新一轮搜索。

获取 App 内存基址 (memory.get_base_address)

声明

内存基址 = memory.get_base_address(进程号)

参数及返回值

读取 App 内存 (memory.read)

声明

, 失败原因 = memory.read(进程号, 内存地址, 数据类型)

参数及返回值

  • 进程号
  • 内存地址 整数型
  • 数据类型 数据类型
  • 失败原因 文本型
    • 整数型,读出的整形数据值
    • 数值型,读出的浮点型数据值
    • 操作失败则为 nil

说明

数据类型,读取指定 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("图片对象的高度已修改成功,点击 “好” 以结束本示例。")