1.前言
这部分内容是Autojs进阶篇最重要的,包含各种封装函数。由于插件需要打赏后获取,这部分内容我只会介绍函数的参数、使用方式、注意事项等内容,不会粘贴插件中的代码,请大家见谅!!!这部分内容会不断增加和完善,我尽量保证内容和插件的同步关系。推荐使用环境为雷电模拟器4(android 7)、雷电模拟器9(android 9)和VMOS虚拟机安卓7.1精简版(android 7),最推荐使用android 7的环境下的设备,因为有些功能在android 9的环境下兼容性并不好。
插件函数注释我已经尽量写的规范,会自动生成api文档,请参考文档进行使用。
2.使用方式
在群里下载py压缩包,解压后,将py文件夹放到项目根目录上,文件数量可能随着插件功能完善而增加,保证如下方架构:
文件夹docs中是api文档,详细介绍了每个函数的参数情况。函数与此文章函数相同,此文章属于函数通用功能介绍。而api文档是我编写生成文件自动生成,能够保证与此版本插件的一致性。此文章会保证与最新版本一致,如果更新多个版本,可能涉及版本冲突问题。一般情况下,用此文章来看函数分类和案例介绍,而api文档用来看具体的函数参数、返回值等情况,若出现冲突,请以api文档为主。
请根据序号1-4完成内容,其中序号1和2需要根据实际情况修改,序号3和4可以使用默认,导入方式如下面代码:
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
特别注意,指定relativeFolder和fileRootPath两个文件夹时,一定要以"/"结尾,最后一个"/"不能省略。插件需要传递文件夹的地方,都需要以"/"结尾,最后一个"/"不能省略。
3.加密与解密功能
1.py_encrypt
/**
* AES加密
* @param {Object} data - 必填,需要加密的数据
* @param {string} key - 必填,加密密钥,必须为16位
* @returns {string} 加密后的字符串
*/
function py_encrypt(data, key)
2.py_decrypt
/**
* AES解密
* @param {string} encrypted - 必填,需要解密的数据
* @param {string} key - 必填,加密密钥,必须为16位
* @returns {string} 解密后的数据
*/
function py_decrypt(encrypted, key)
3.整体案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 设置对象类型的初始数据
let obj = {
test1: "测试1",
test2: "测试2"
}
// 加密秘钥(必须16位),可以随便输入16位
let encryptionKey = "kjsnxlemzienglkn";
// 进行AES加密
let aesString = pyPlugin.py_encrypt(obj, encryptionKey);
console.log("AES加密后的数据为:");
console.log(aesString);
console.log("----------------------------");
// 进行AES解密
let aesDecodeString = pyPlugin.py_decrypt(aesString, encryptionKey);
console.log("AES解密后的数据为:");
console.log(aesDecodeString);
console.log("----------------------------");
// 将字符串转换为对象
let targetObj = JSON.parse(aesDecodeString);
console.log("解密后对象数据为:");
console.log(targetObj);
4.Root权限功能
1.py_hasRootAccess
/**
* 检查是否有Root权限
* @returns {boolean} 是否有Root权限
*/
function py_hasRootAccess()
2.py_requestRoot
/**
* 请求Root权限
* @returns {boolean} 是否请求Root权限成功
*/
function py_requestRoot()
3.py_getProcessIdByPackageName
/**
* 通过包名获取进程ID
* @param {string} currentPackageName - 必填,应用包名
* @returns {string|null} 返回进程id,未找到返回null
*/
function py_getProcessIdByPackageName(currentPackageName)
4.py_getMemoryUsage
/**
* 获取当前应用内存使用(单位:M)
* @param {string} currentAppPackageName - 必填,应用包名
* @returns {number|undefined} 返回使用内存(保留两位小数),未找到应用返回undefined
*/
function py_getMemoryUsage(currentAppPackageName)
5.函数1-4案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 获取root权限
if (!pyPlugin.py_hasRootAccess()) {
toast("请开启Root权限");
// 如果请求root权限失败
if (!pyPlugin.py_requestRoot()) {
// 清理缓存,退出脚本
// 由于这是学习阶段,没有缓存就直接退出了
exit();
}
}
// 设置按键精灵包名
let currentPackageName = "com.cyjh.mobileanjian";
// 获取应用进程id
let currentPid = pyPlugin.py_getProcessIdByPackageName(currentPackageName);
console.log("按键精灵进程id为" + currentPid);
// 获取应用内存使用量
let memoryUsage = pyPlugin.py_getMemoryUsage(currentPid);
console.log("按键精灵内存用量为" + memoryUsage + "M");
6.函数1-4注意事项
py_requestRoot函数:调用时会有5秒延迟,这是为了应用初次请求Root权限时,有足够时间手动授权。
py_getMemoryUsage函数:会采用四舍五入的方式保留两位小数。
7.py_stopApplicationByPackageName
/**
* 通过包名停止应用
* @param {string} currentAppPackageName - 必填,应用包名
*/
function py_stopApplicationByPackageName(currentAppPackageName)
8.py_switchApp
/**
* 通过包名和活跃包名切换app
* @param {string} packageName - 必填,应用包名
* @param {string} packageActivePackageName - 必填,应用活跃包名
*/
function py_switchApp(packageName, packageActivePackageName)
9.函数7-8案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 切换到按键精灵
pyPlugin.py_switchApp("com.cyjh.mobileanjian", "com.cyjh.mobileanjian.vip.activity.MainActivity");
sleep(5000);
// 关闭按键精灵
pyPlugin.py_stopApplicationByPackageName("com.cyjh.mobileanjian");
10.py_getSwitchAppCallCode
/**
* 获取切换app调用代码
* @param {string} taskId - 必填,应用任务id
* @param {string} packageName - 必填,应用包名
* @param {number} [startTaskId=1] - 选填,调用代码开始值,默认为1
* @param {number} [endTaskId=40] - 选填,调用代码结束值,默认为40
*/
function py_getSwitchAppCallCode(taskId, packageName, startTaskId, endTaskId)
11.py_getRecentTaskPackages
/**
* 获取运行过程中应用信息
* @returns {Array} 返回应用信息数组
*/
function py_getRecentTaskPackages()
12.py_isAppRunning
/**
* 检查应用是否运行
* @param {string} packageName 应用包名
* @returns {boolean} 返回应用是否运行
*/
function py_isAppRunning(packageName)
13.py_getRecentTaskPackageTaskIds
/**
* 获取运行过程中应用信息任务id,主要用于快速切换应用
* @returns {Array} 返回任务id数组
*/
function py_getRecentTaskPackageTaskIds()
14.函数10-13案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 获取后台任务应用包名
console.log("应用包名:");
console.log(pyPlugin.py_getRecentTaskPackages());
console.log("----------------------------------------");
// 获取后台任务id
console.log("任务id:");
let currentPackageName = "org.autojs.autojs";
let taskIdList = pyPlugin.py_getRecentTaskPackageTaskIds()
console.log(taskIdList);
let currentTaskId = taskIdList.map(function (item) {
if (item.packageName == currentPackageName) {
return item.id;
}
})[0];
console.log("----------------------------------------");
// 判断应用是否运行
console.log("应用是否在运行:")
console.log(pyPlugin.py_isAppRunning(currentPackageName));
console.log("----------------------------------------");
// 获取切换应用编码
console.log("获取切换应用编码:");
pyPlugin.py_getSwitchAppCallCode(currentTaskId, currentPackageName);
15.函数10-13注意事项
py_getSwitchAppCallCode函数:用于在频繁切换应用时,获取切换应用编码时使用。由于我们使用环境只有android 7和android 9,android 7环境下切换应用编码固定为24。而android 9环境下通过测试发现无法使用,无法获取到切换应用编码,我感觉是android 9不允许使用这种切换应用的方式。此函数是通过枚举的方式获取切换应用编码,一般不要传递调用代码初始值和调用代码结束值(使用默认值即可),超过这个默认值很容易导致系统崩溃,我将这个函数封装出来是为了让大家知道,我如何获取到的这个值。
16.py_inputText
/**
* 在输入框中输入内容
* @param {string} currentText - 必填,内容
*/
function py_inputText(currentText)
17.py_getCpuType
/**
* 获取cpu架构
* @returns {string} 返回cpu架构
*/
function py_getCpuType()
18.py_getCpuName
/**
* 获取当前cpu名称
* @returns {string} 返回cpu名称
*/
function py_getCpuName()
19.py_clickKey
/**
* 模拟点击按键
* @param {string} key - 必填,按键值,可选值如下:
* - "menu": 菜单键
* - "home": 返回主页
* - "back": 返回
* - "#": #键
* - "volumeDown": 减少音量
* - "ok": 确定
* - "up": 向上
* - "down": 向下
* - "left": 向左
* - "right": 向右
* - "1": 1键
* - "2": 2键
* - "3": 3键
* - "4": 4键
* - "5": 5键
* - "6": 6键
* - "7": 7键
* - "8": 8键
* - "9": 9键
*/
function py_clickKey(key)
20.py_isChmod755
/**
* 判断文件是否具有 755 权限
* @param {string} filePath - 必填,文件路径
* @returns {boolean} 是否具有权限
*/
function py_isChmod755(filePath)
21.函数16-20案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 在输入框输入内容
pyPlugin.py_inputText("123321");
// 获取cpu类型
console.log("cpu类型:" + pyPlugin.py_getCpuType());
// cpu名称
console.log("cpu名称:" + pyPlugin.py_getCpuName());
// 返回首页
pyPlugin.py_clickKey("home");
// 判断是否以755权限运行
console.log("是否以755权限运行:" + pyPlugin.py_isChmod755(fileRootPath + "img.png"));
22.函数16-20注意事项
py_inputText函数:在光标切换到输入框,再调用此函数才能生效。现在很多新应用的输入框可能没有做此函数的适配,此函数有概率调用不生效,此函数在老应用中适配较好。
py_getCpuType和py_getCpuName函数:这两个函数功能类似,py_getCpuName函数中也调用了py_getCpuType函数,py_getCpuName函数将py_getCpuType函数返回值做了匹配,使返回值和内存操作的环境一一对应。同时,py_getCpuName函数使用了本地存储,我更推荐使用py_getCpuName函数获取cpu信息。
py_clickKey函数:将常用的按键操作做了二次封装,现在很多游戏或应用已经去掉了按键操作适配。我们一般只使用此函数的home和back,其他值几乎不再使用。
py_isChmod755函数:判断文件是否以755权限运行,这是文件运行的最高权限。这个函数我们一般不会使用,主要用于内存初始化过程。
23.py_captureScreenWithShell
/**
* 通过shell命令截图
* @returns {image|null} 返回图片,截图失败返回null
*/
function py_captureScreenWithShell()
24.py_initScreenCapturePermission
/**
* 初始化截图权限
*/
function py_initScreenCapturePermission()
25.py_captureScreen
/**
* 自动根据环境进行截图
* @param {boolean} [autoRecycle=true] - 选填,是否自动回收shell截图,默认为true
* @returns {Image} 返回截图图片
*/
function py_captureScreen(autoRecycle)
26.函数22-25案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
let img = pyPlugin.py_captureScreenWithShell();
console.log("shell截图:" + img.width);
// 回收图片
img.recycle();
// 初始化截图权限
pyPlugin.py_initScreenCapturePermission();
// 截图
img = pyPlugin.py_captureScreen();
console.log("自适应截图1:" + img.width);
let img2 = pyPlugin.py_captureScreen();
console.log("自适应截图2:" + img2.width);
console.log("自适应截图1:" + img.width);
27.函数22-25注意事项
py_captureScreenWithShell函数:通过shell命令截图,截图需要手动回收。
py_initScreenCapturePermission函数:根据环境自动加载权限,如果是android 7的模拟器会加载root权限,否则加载截图权限。考虑到初次加载root权限需要手动授权的情况,加载过程中会有个5秒的延迟,全局加载一次权限就好了。
py_captureScreen函数:根据环境自动截图,并且如果是shell截图时,会自动将前面的图片回收,避免我们忘记回收图片而导致内存溢出。如果不需要自动回收图片,也可以通过传值方式关闭。不推荐自己直接使用py_captureScreenWithShell函数截图,而是通过py_initScreenCapturePermission函数和py_captureScreen函数实现。
5.UI功能
1.py_getUiAllChildren
/**
* 递归获取视图的所有子视图
* @param {View} currentView - 必填,当前视图
* @returns {Array} 返回子视图数组
*/
function py_getUiAllChildren(currentView)
2.py_getSpinnerEntries
/**
* 获取Spinner选项值
* @param {SpinnerView} currentSpinner - 必填,下拉菜单视图
* @returns {Array} 返回选项数组
*/
function py_getSpinnerEntries(currentSpinner)
3.py_setSpinnerEntries
/**
* 设置Spinner选项值
* @param {SpinnerView} currentSpinner - 必填,下拉菜单视图
* @param {Array} currentEntries - 必填,选项数组
*/
function py_setSpinnerEntries(currentSpinner, currentEntries)
4.py_setSpinnerSelectedValue
/**
* 设置Spinner初始值
* @param {SpinnerView} currentSpinner - 必填,下拉菜单视图
* @param {string} value - 必填,选项值
*/
function py_setSpinnerSelectedValue(currentSpinner, value)
5.函数1-4案例
UI功能1.xml文件代码如下:
<vertical id="testRoot">
<vertical id="test1">
<spinner id="test2" entries="选项A|选项B|选项C|选项D"></spinner>
<spinner id="test3" entries="选项A|选项B|选项C|选项D"></spinner>
</vertical>
<vertical id="test4">
<spinner id="test5" entries="选项A|选项B|选项C|选项D"></spinner>
<spinner id="test6" entries="选项A|选项B|选项C|选项D"></spinner>
</vertical>
</vertical>
UI功能1.js文件代码如下:
"ui";
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// ui保存文件夹
let uiFolder = relativeFolder + "ui/";
// 读取 XML 文件内容
ui.layoutFile(uiFolder + "UI功能1.xml");
// 获取根节点视图
let testRootView = ui.testRoot;
// 获取根节点所有孩子
let childrenUiList = pyPlugin.py_getUiAllChildren(testRootView);
childrenUiList.forEach(currentUi => {
console.log(currentUi);
});
// 获取id为test2的spinner控件
let currentSpinnerView = ui.test2;
// 获取当前spinner控件的所有选项值
let currentEntries = pyPlugin.py_getSpinnerEntries(currentSpinnerView);
console.log("test2选项值:" + currentEntries);
// 获取id为test3的spinner控件
currentSpinnerView = ui.test3;
currentEntries = ['选项E', '选项F', '选项G', '选项H'];
// 设置当前spinner控件的选项值
pyPlugin.py_setSpinnerEntries(currentSpinnerView, currentEntries);
// 获取当前spinner控件的所有选项值
currentEntries = pyPlugin.py_getSpinnerEntries(currentSpinnerView);
console.log("test3修改后选项值:" + currentEntries);
// 获取id为test5的spinner控件
currentSpinnerView = ui.test5;
// 获取spinner当前选择值
let currentValue = currentSpinnerView.getSelectedItem();
console.log("test4当前选项值:" + currentValue);
// 修改当前选项值
pyPlugin.py_setSpinnerSelectedValue(currentSpinnerView, "选项B");
currentValue = currentSpinnerView.getSelectedItem();
console.log("修改后,test4当前选项值:" + currentValue);
6.py_getRadioChildrenOfRadioGroup
/**
* 获取RadioGroup的Radio孩子
* @param {RadioGroupView} currentRadioGroup - 必填,单选框组合视图
* @returns {Array} 返回单选框视图数组
*/
function py_getRadioChildrenOfRadioGroup(currentRadioGroup)
返回参数类型其实是对象数组,每个对象五个属性,分别表示如下:
{
key: 单选框id,
text: 单选框内容,
checked: 单选框是否被选择
index: 单选框索引
element: 单选框视图对象
}
7.py_setRadioGroupValue
/**
* 设置RadioGroup初始值
* @param {RadioGroupView} currentRadioGroup - 必填,单选框组合视图
* @param {string} value - 必填,选项值
*/
function py_setRadioGroupValue(currentRadioGroup, value)
8.函数6-7案例
UI功能2.xml文件代码如下:
<vertical>
<radiogroup id="testGroup1" checkedButton="@+id/test1" orientation="horizontal">
<radio id="test1" text="任务1" />
<radio id="test2" text="任务2" />
<radio id="test3" text="任务3" />
</radiogroup>
<radiogroup id="testGroup2" checkedButton="@+id/test6" orientation="horizontal">
<radio id="test4" text="任务4" />
<radio id="test5" text="任务5" />
<radio id="test6" text="任务6" />
</radiogroup>
</vertical>
UI功能2.js文件代码如下:
"ui";
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// ui保存文件夹
let uiFolder = relativeFolder + "ui/";
// 读取 XML 文件内容
ui.layoutFile(uiFolder + "UI功能2.xml");
// 获取id为testGroup1单选框组合视图
let currentRadioGroupView = ui.testGroup1;
// 获取当前单选框数组
let radioArray = pyPlugin.py_getRadioChildrenOfRadioGroup(currentRadioGroupView);
// 打印每个单选框
radioArray.forEach(currentRadio => {
console.log(currentRadio);
});
// 获取id为testGroup2单选框组合视图
currentRadioGroupView = ui.testGroup2;
// 获取当前已选择单选框
let currentCheckId = currentRadioGroupView.getCheckedRadioButtonId();
let currentRadioElement = currentRadioGroupView.findViewById(currentCheckId);
let currentValue = currentRadioElement.getText().toString();
console.log("testGroup2当前选择单选框为" + currentValue);
// 修改当前选项值
pyPlugin.py_setRadioGroupValue(currentRadioGroupView, "任务5");
// 获取修改后已选择单选框
currentCheckId = currentRadioGroupView.getCheckedRadioButtonId();
currentRadioElement = currentRadioGroupView.findViewById(currentCheckId);
currentValue = currentRadioElement.getText().toString();
console.log("修改后,testGroup2当前选择单选框为" + currentValue);
9.py_initCheckboxGroup
/**
* 初始化复选框组合
* @param {CheckboxGroupView} currentCheckboxGroup - 必填,复选框组合视图
*/
function py_initCheckboxGroup(currentCheckboxGroup)
10.py_getCheckboxChildrenOfCheckboxGroup
/**
* 从复选框组合中获取所有的复选框信息
* @param {CheckboxGroupView} currentCheckboxGroup - 必填,复选框组合视图
* @returns {Array} 返回复选框视图数组
*/
function py_getCheckboxChildrenOfCheckboxGroup(currentCheckboxGroup)
返回参数类型其实是对象数组,每个对象四个属性,分别表示如下:
{
key: 复选框id,
text: 复选框内容,
checked: 复选框是否被选择
element: 复选框视图对象
}
11.py_getCheckboxGroupCheckedId
/**
* 递归获取复选框组合中当前已选择复选框的id
* @param {Array} currentTree - 必填,视图数组
* @param {CheckboxView} currentUi - 必填,当前复选框组合视图
* @returns {string|undefined} 返回选择的复选框id,未找到返回undefined
*/
function py_getCheckboxGroupCheckedId(currentTree, currentUi)
12.py_setCheckboxGroupValue
/**
* 设置复选框组合初始值
* @param {CheckboxGroupView} currentCheckboxGroup - 必填,复选框组合视图
* @param {string} value - 必填,复选框内容
*/
function py_setCheckboxGroupValue(currentCheckboxGroup, value)
13.函数9-12案例
UI功能3.xml文件代码如下:
<vertical id="checkGroup1">
<!-- 通过多个水平布局来更好划分任务 -->
<horizontal id="checkGroup2">
<!-- 设置复选框默认选择 -->
<checkbox id="test1" text="任务1" checked="true" />
<checkbox id="test2" text="任务2" />
<checkbox id="test3" text="任务3" />
</horizontal>
<horizontal id="checkGroup3">
<!-- 设置复选框不可用 -->
<checkbox id="test4" text="任务4" />
<checkbox id="test5" text="任务5" />
<checkbox id="test6" text="任务6" />
</horizontal>
</vertical>
UI功能3.js文件代码如下:
"ui";
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// ui保存文件夹
let uiFolder = relativeFolder + "ui/";
// 读取 XML 文件内容
ui.layoutFile(uiFolder + "UI功能3.xml");
// 初始化id为checkGroup2的复选框
pyPlugin.py_initCheckboxGroup(ui.checkGroup2);
// 初始化id为checkGroup3的复选框
pyPlugin.py_initCheckboxGroup(ui.checkGroup3);
// 获取复选框组合下所有的复选框
let checkboxArray = pyPlugin.py_getCheckboxChildrenOfCheckboxGroup(ui.checkGroup1);
// 获取复选框所有的id
let checkboxIdArray = checkboxArray.map(function (item) {
return item.id;
});
console.log("id为checkGroup1的复选框组合下的复选框id有" + checkboxIdArray);
// 获取id为checkGroup2的复选框组合框视图
currentCheckboxGroupView = ui.checkGroup2;
// 获取所有视图
let childrenUiArray = pyPlugin.py_getUiAllChildren(currentCheckboxGroupView);
// 获取当前勾选复选框
let currentId = pyPlugin.py_getCheckboxGroupCheckedId(childrenUiArray, currentCheckboxGroupView);
console.log("id为checkGroup2复选框组合,当前勾选复选框id为:" + currentId);
// 获取值
console.log("id为checkGroup2复选框组合,当前勾选复选框值为:" + ui[currentId].text);
// 修改复选框组合的值
pyPlugin.py_setCheckboxGroupValue(currentCheckboxGroupView, "任务3");
// 获取当前勾选复选框
currentId = pyPlugin.py_getCheckboxGroupCheckedId(childrenUiArray, currentCheckboxGroupView);
console.log("id为checkGroup2复选框组合,修改后勾选复选框id为:" + currentId);
// 获取值
console.log("id为checkGroup2复选框组合,修改后勾选复选框值为:" + ui[currentId].text);
14.函数9-12注意事项
所有函数都是可以递归查找的,以上面案例为例,把id为checkGroup1的视图当做复选框组合,代表所有复选框都是一组;把id为checkGroup2和checkGroup2的视图当做复选框组合,代表将复选框分成了两组。简言之,复选框分组,是根据给出的复选框组合视图递归往下查找所有复选框。
py_getCheckboxGroupCheckedId函数:第一个参数为视图数组,需要通过py_getUiAllChildren函数,视图一般传递ui的根视图即可。如果ui配置非常多,也可以根据实际情况传递py_getUiAllChildren函数的视图,但是一定要保证包含了所有需要获取值的复选框和复选框组合视图。py_getCheckboxGroupCheckedId函数第二个参数为复选框组合视图,这个所谓的复选框组合,是我参考单选框组合自定义称呼,这个主要是保证复选框组合能够包含所有的一组需要单选的复选框。
15.py_checkFloatyPermission
/**
* 检查悬浮窗权限是否开启
* @returns {boolean} 返回是否开启
*/
function py_checkFloatyPermission()
16.py_getConfigInformationFromUi
/**
* 从UI中获取配置信息
* @param {View} uiView - 必填,需要获取配置信息的根视图
* @param {Array} checkboxgroupIdArray - 选填,复选框组合id数组
* @returns {Object} 返回固定格式的配置信息对象
*/
function py_getConfigInformationFromUi(uiView, checkboxgroupIdArray)
17.py_getConfigInformationFromFile
/**
* 从文件中获取配置信息,并加载Ui初始值
* @param {View} currentUi - 必填,视图
* @param {string} filePath - 必填,配置文件路径
* @param {string} encryptionKey - 选填,文件加密字符串;不填写代表不解密
* @returns {Object} 返回配置信息
*/
function py_getConfigInformationFromFile(currentUi, filePath, encryptionKey)
18.函数15-17案例
UI功能4.xml文件代码如下:
<vertical id="root">
<input id="test1"></input>
<spinner id="test2" entries="选项A|选项B|选项C|选项D"></spinner>
<Switch id="test3"></Switch>
<radiogroup id="radioGroup1" checkedButton="@+id/test5" orientation="horizontal">
<radio id="test4" text="任务1" />
<radio id="test5" text="任务2" />
<radio id="test6" text="任务3" />
</radiogroup>
<horizontal id="checkboxGroup1">
<!-- 设置复选框默认选择 -->
<checkbox id="test7" text="任务1" checked="true" />
<checkbox id="test8" text="任务2" />
<checkbox id="test9" text="任务3" />
</horizontal>
<checkbox id="test10" text="任务4" />
<!-- 保存按钮 -->
<button id="saveButton" text="保存" textSize="18" marginTop="24" background="#FF6200EE"
textColor="#FFFFFF" padding="12" />
</vertical>
UI功能4.js文件代码如下:
"ui";
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// ui保存文件夹
let uiFolder = relativeFolder + "ui/";
// 读取 XML 文件内容
ui.layoutFile(uiFolder + "UI功能4.xml");
// 请求悬浮窗权限(单纯为了测试功能,使用悬浮窗时再请求这个功能)
console.log("悬浮窗权限:" + pyPlugin.py_checkFloatyPermission());
// 初始化复选框组合
pyPlugin.py_initCheckboxGroup(ui.checkboxGroup1);
// 16位加密密码
let encryptKey = "sdfjldnfgaldfnaf";
// 保存文件路径
let currentFilePath = fileRootPath + "py_uiConfig.txt";
// 监听按钮操作
ui.saveButton.click(function () {
let checkboxgroupIdArray = ["checkboxGroup1"];
let configObj = pyPlugin.py_getConfigInformationFromUi(ui.root, checkboxgroupIdArray);
// 将数据加密保存到本地
let encryptString = pyPlugin.py_encrypt(configObj, encryptKey);
files.write(currentFilePath, encryptString);
toast("保存成功!");
})
// 根路径视图
let currentUiView = ui.root;
let uiConfig = pyPlugin.py_getConfigInformationFromFile(currentUiView, currentFilePath, encryptKey);
console.log(uiConfig);
19.函数15-17注意事项
py_checkFloatyPermission函数:此函数用于请求悬浮窗功能,如果使用了ui的悬浮窗,需要提前请求一下这个权限。这里为了测试才调用,不使用悬浮窗是时,不需要请求此权限。
py_getConfigInformationFromUi函数:通过第一个参数可以控制读取范围,允许局部读取。由于不存在真实的复选框组合,所谓的复选框组合是我模拟出来的,需要通过第二个参数指定哪些包裹复选框的视图为复选框组合。如果不指定复选框组合,所有复选框变成独立个体。
py_getConfigInformationFromFile函数:此函数需要配合py_getConfigInformationFromUi函数使用,用py_getConfigInformationFromUi函数读取Ui数据并保存,然后py_getConfigInformationFromFile函数将文件中数据加载到Ui中。一般情况下,我们使用上面案例中加密与解密过程来完成加载更加安全,能够隐藏脚本配置的数据结构。当然,如果不想加密也可以,保存完配置后,不加密的情况下保存到本地。然后在使用py_getConfigInformationFromFile函数时,不传递encryptionKey参数,代表不使用解密的方式加载数据。
需要特别注意,py_getConfigInformationFromFile函数和py_getConfigInformationFromUi函数只支持上面案例中给出的控件,包括input、spinner、Switch、radiogroup和checkbox。这五个控件时常用控件,我一直不推荐大家使用那些花哨的控件。我并不是没能力封装其他控件的类似功能,只是想从根上解决大家花哨控件的交互。
6.文件功能
1.py_getFileInformation
/**
* 获取文件信息
* @param {string} filePath - 必填,文件路径
* @returns {string|undefined} 返回文件信息,未找到返回undefined
*/
function py_getFileInformation(filePath)
2.py_isFileIdentical
/**
* 通过文件大小判断文件是否一致
* @param {string} filePath1 - 必填,文件路径1
* @param {string} filePath2 - 必填,文件路径2
* @returns {boolean} 返回文件是否一致
*/
function py_isFileIdentical(filePath1, filePath2)
3.整体注意事项
py_getFileInformation函数:封装内容比较简单,文件存在时返回读取到的文件内容;文件不存在时返回undefined。虽然内容很简单,但是我们可以通过返回信息来判断内容是否有效,不用我们读取文件之前手动判断文件是否存在。
py_isFileIdentical函数:通过文件大小判断两个文件是否一致。其实,我初始打算是完全比对两个文件是否一致,但是比较起来速度非常慢,最终我选择了通过文件大小来判断文件是否一致的方式。虽然这个函数存在一定的误差,但是想做到两个文件大小完全一致是非常困难的。那种非常低概率出现的误差,和比较速度来选择,我宁愿选择速度更快的方式。目前,我没有遇到过两个文件不一致,但是比较结果是一致的情况出现。
目前,这个模块封装函数比较少,后期我会参考文件操作的常用功能来封装其他函数。
7.图片功能
1.py_isImageRecycled
/**
* 判断图片是否回收
* @param {Image} img - 必填,图片
* @returns {boolean} 返回图片是否回收
*/
function py_isImageRecycled(img)
2.py_getImagePointByAnJian
/**
* 通过按键精灵值获取图片坐标点二维数组
* @param {string} currentColor - 必填,按键精灵多点比色颜色描述
* @returns {Array} 返回点颜色数组
*/
function py_getImagePointByAnJian(currentColor)
3.py_getImagePointColors
/**
* 打印图片点颜色信息并拼接对应函数
* @param {Image} img - 必填,图片
* @param {Array|string} points - 必填,点数组或颜色字符串
* @param {Object} options - 选填,配置信息
* @param {number} [options.type=1] - 选填,打印函数类型,默认为1
* - "1": pyPlugin.py_matchPointColorBySimilar函数
* - "2": pyPlugin.py_findMultiColorsBySimilar函数
* @param {number} [options.deviceWidthOffset=0] - 选填,设备宽度偏移量,默认为0,有偏移一般设置为-1,比如VMOS Pro虚拟机
*/
function py_getImagePointColors(img, points, options)
4.py_getPointColor
/**
* 获取点颜色
* @param {Image} img - 必填,图片
* @param {number} currentX - 必填,点横坐标
* @param {number} currentY - 必填,点纵坐标
* @returns {string} 返回点颜色信息
*/
function py_getPointColor(img, currentX, currentY)
5.py_getHorizontalScreenImagePointColors
/**
* 打印横屏图片点颜色信息并拼接对应函数
* @param {Image} img - 必填,图片
* @param {Array|string} points - 必填,点数组或颜色字符串
* @param {Object} options - 选填,配置信息
* @param {number} [options.type=1] - 选填,打印函数类型,默认为1
* - "1": pyPlugin.py_matchPointColorBySimilar函数
* - "2": pyPlugin.py_findMultiColorsBySimilar函数
* @param {number} [options.deviceWidth=720] - 选填,设备宽度,默认为720
* @param {number} [options.deviceWidthOffset=0] - 选填,设备宽度偏移量,默认为0,有偏移一般设置为-1,比如VMOS Pro虚拟机
*/
function py_getHorizontalScreenImagePointColors(img, points, options)
6.py_hexToRgb
/**
* 颜色值十六进制转RGB数组
* @param {string} hex - 必填,十六进制颜色
* @returns {Array} 返回RGB颜色值数组
*/
function py_hexToRgb(hex)
7.py_isColorSimilar
/**
* 计算颜色相似度(欧几里得距离算法)
* @param {string|Array} color1 - 必填,颜色1,允许十六进制颜色值或RGB数组
* @param {string|Array} color2 - 必填,颜色2,允许十六进制颜色值或RGB数组
* @param {number} threshold - 必填,相似度
* @param {boolean} [hasLog=false] - 选填,是否打印比对信息,默认为false
* @returns {boolean} 返回是否满足比对条件
*/
function py_isColorSimilar(color1, color2, threshold, hasLog)
8.py_matchPointColorBySimilar
/**
* 多点比色,通过相似度匹配各个点颜色
* @param {Image} img - 必填,颜色
* @param {Array} pointColors - 必填,点颜色数组
* @param {number} threshold - 必填,相似度
* @param {boolean} [hasLog=false] - 选填,是否打印比对信息,默认为false
* @returns {boolean} 返回是否匹配成功
*/
function py_matchPointColorBySimilar(img, pointColors, threshold, hasLog)
9.py_getSimilarColorPointCount
/**
* 获取相似点数量
* @param {Image} img - 必填,图片
* @param {Array} region - 必填,查找区域,分别为[左上角横坐标,左上角纵坐标,宽度,高度]
* @param {string} compareColor - 必填,十六进制颜色值
* @param {number} threshold - 必填,阈值,严格匹配推荐:10-20,常规匹配:推荐:20-30,宽松匹配推荐:30-40
* @param {boolean} [hasLog=false] - 选填,是否打印比对信息,默认为false
* @returns {number} 返回相似点数量
*/
function py_getSimilarColorPointCount(img, region, compareColor, threshold, hasLog)
10.py_findMultiColorsBySimilar
/**
* 多点找色,获取第一点的位置信息
* @param {Image} img - 必填,图片
* @param {string} firstColor - 必填,第一个颜色十六进制值
* @param {Array} colorsArray - 必填,颜色数组
* @param {number} threshold - 必填,阈值,严格匹配推荐:10-20,常规匹配:推荐:20-30,宽松匹配推荐:30-40
* @param {Array} region - 选填,查找区域,分别为[左上角横坐标,左上角纵坐标,宽度,高度]
* @returns {Array|undefined} 返回点信息数组,分别为[横坐标,纵坐标],未找到返回undefined
*/
function py_findMultiColorsBySimilar(img, firstColor, colorsArray, threshold, region) {
let currentObj = {};
currentObj["threshold"] = threshold;
if (region) {
currentObj["region"] = region;
}
let currentPoint = images.findMultiColors(img, firstColor, colorsArray, currentObj);
if (currentPoint) {
currentPoint = currentPoint.toString();
currentPoint = currentPoint.substring(1, currentPoint.length - 1);
currentPoint = currentPoint.replace(/\s/g, "");
let currentPointArray = currentPoint.split(",");
currentPointArray[0] = Number(currentPointArray[0]);
currentPointArray[1] = Number(currentPointArray[1]);
return currentPointArray;
}
return undefined;
}
11.py_preloadOpenCV
/**
* 预加载OpenCV
* @returns {boolean} 返回是否加载成功
*/
function py_preloadOpenCV()
12.py_recycleImage
/**
* 回收图片
* @param {Image} img - 必填,图片
*/
function py_recycleImage(img)
13.py_saveErrorImageToLocal
/**
* 保存异常图片到本地
* @param {Image} img - 必填,图片
* @param {string} functionName - 必填,功能名称
* @param {string} filePath - 选填,文件路径;未填写会保存到根文件夹
*/
function py_saveErrorImageToLocal(img, functionName, filePath)
14.整体案例
图片功能.js文件代码如下:
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 初始化截图权限
pyPlugin.py_initScreenCapturePermission();
// 初始化OpenCV
console.log("初始化OpenCV是否成功:" + pyPlugin.py_preloadOpenCV());
// 截图
let img = pyPlugin.py_captureScreen();
// 判断图片是否回收
console.log("图片是否回收:" + pyPlugin.py_isImageRecycled(img));
// 竖屏状态下的图像处理
// 按键精灵多点比色字符串
let anjianString = "108|231|1FC5F4,112|267|1FC5F4,268|247|FEFEFE,425|241|E4B468,98|412|E4BB76,276|410|98C9F7";
// 将字符串转换为点数组
let anjianPoints = pyPlugin.py_getImagePointByAnJian(anjianString);
console.log("按键精灵点数组:");
console.log(anjianPoints);
console.log("--------------------------------------------------------");
// 打印图像处理的函数
pyPlugin.py_getImagePointColors(img, anjianPoints);
// 获取点(108,231)的颜色
let currentColor = pyPlugin.py_getPointColor(img, 108, 231);
console.log("点(108,231)的颜色为" + currentColor);
// 将颜色转换为RGB值
let currentRgbColor = pyPlugin.py_hexToRgb(currentColor);
console.log("颜色" + currentColor + "的RGB值为" + currentRgbColor);
// 比较两个颜色是否相似
let color1 = "#68b4e4";
let color2 = "#76bbe4";
// 颜色对比,设置相似度为20,并且打印比对信息
pyPlugin.py_isColorSimilar(color1, color2, 20, true);
// 多点比色是否成功
console.log("多点比色是否成功:" + pyPlugin.py_matchPointColorBySimilar(img, [[108, 231, "#f4c51f"], [112, 267, "#f4c51f"], [268, 247, "#fefefe"], [425, 241, "#68b4e4"], [98, 412, "#76bbe4"], [276, 410, "#f7c998"]], 40));
// 获取相似点数量
currentColor = "#68b4e4";
let currentRegion = [410, 210, 60, 70]
console.log("与颜色" + currentColor + "相似度20的点数量为" + pyPlugin.py_getSimilarColorPointCount(img, currentRegion, currentColor, 20));
// 打印多点找色函数
let options = {
// 打印pyPlugin.py_findMultiColorsBySimilar函数
type: 2,
}
// 第二个参数传递点数组或者多点比色字符串都是可以的
pyPlugin.py_getImagePointColors(img, anjianString, options);
// 多点比色
let currentPoint = pyPlugin.py_findMultiColorsBySimilar(img, "#f4c51f", [[4, 36, "#f4c51f"], [160, 16, "#fefefe"], [317, 10, "#68b4e4"], [-10, 181, "#76bbe4"], [168, 179, "#f7c998"]], 20);
if (currentPoint) {
console.log("多点找色找到点(" + currentPoint[0] + "," + currentPoint[1] + ")");
} else {
console.log("多点找色匹配失败");
}
// 将图片当做错误图片保存到本地(使用默认保存路径)
pyPlugin.py_saveErrorImageToLocal(img, "test");
// 回收图片
pyPlugin.py_recycleImage(img);
console.log("图片是否回收成功:" + pyPlugin.py_isImageRecycled(img));
15.整体注意事项
py_getImagePointByAnJian、py_getImagePointColors和py_getHorizontalScreenImagePointColors函数:这三个函数是我当初通过运行脚本来点颜色值的辅助函数,这种方式每次获取点颜色值都需要运行一次脚本,非常不方便。现在推荐使用Autojs基础部分给出的辅助工具,这三个函数能够让我们更好的理解辅助工具,就保留了下来。其中,py_getImagePointColors和py_getHorizontalScreenImagePointColors函数第二个参数可以接收点数据,也可以接收按键精灵字符串,传递字符串时,这两个函数自动调用py_getImagePointByAnJian函数进行转换。特别注意,竖屏状态下不需要设置宽度偏移量,横屏状态下需要设置宽度偏移量。
竖屏状态下,通过函数实现按键精灵多点比色颜色描述转换为autojs点颜色:
let anjianString = "108|231|1FC5F4,112|267|1FC5F4,268|247|FEFEFE,425|241|E4B468,98|412|E4BB76,276|410|98C9F7";
// 将字符串转换为点数组
let anjianPoints = pyPlugin.py_getImagePointByAnJian(anjianString);
// 打印多点比色函数
pyPlugin.py_getImagePointColors(img, anjianPoints);
// 打印多点找色字符串
pyPlugin.py_getImagePointColors(img, anjianPoints, { type: 2 });
竖屏状态下,通过辅助工具分别完成多点比色和多点找色功能:
横屏状态下如果传递数组,需要自己进行坐标转换,而传字符串,函数会自动进行坐标转换。横屏状态下,通过函数实现按键精灵多点比色颜色描述转换为autojs点颜色:
// 横屏状态下
let anjianString = "701|39|1EC5F5,680|39|1EC5F5,307|37|49CEAB,304|46|43CCA9,222|233|222222,481|132|6778FF";
// 打印多点比色函数
pyPlugin.py_getHorizontalScreenImagePointColors(img, anjianString, { deviceWidthOffset: -1 });
// 打印多点找色字符串
pyPlugin.py_getHorizontalScreenImagePointColors(img, anjianString, { type: 2, deviceWidthOffset: -1 });
横屏状态下,通过辅助工具分别完成多点比色和多点找色功能:
py_preloadOpenCV函数:用于提前加载OpenCV,如果不提前加载,有概率出现第一个图片比对失效的问题。一般在请求完截图权限之后,图像处理之前执行一次这个函数。
py_saveErrorImageToLocal函数:脚本打包后,容易出现各种问题,有概率在特殊情况下,出现图片识别失败的情况。我们可以通过逻辑,在图片识别失败时,将识别失败的图片保存到本地,然后将这些图片通过加载的方式,检查识别问题。默认情况下,会在根路径下创建一个errorImage文件夹,然后将图片保存到errorImage文件夹中,图片会通过时间戳拼接名称,不会出现替换的情况。我们可以通过传递的功能名称,来区分不同功能的出错图片。如果传递了最后一个参数,指定了文件夹(必须以"/"结尾,不能省略最后的"/"),会在此文件夹中再创建一个errorImage文件夹,进行保存图片。
8.控件功能
1.py_startAppByName
/**
* 通过名称启动应用
* @param {string} currentAppName - 必填,应用名称
*/
function py_startAppByName(currentAppName)
2.py_getHomePackageAndActivityPackage
/**
* 打印主页应用包名和活跃包名
* @param {string} appName - 必填,应用名称
* @param {number} [delay=5000] - 选填,等待应用打开时间(单位:毫秒),默认为5000
*/
function py_getHomePackageAndActivityPackage(appName, delay)
3.整体注意事项
py_startAppByName函数:需要保证返回主界面后再执行,应用不在当前页也能启动,但是应用在文件夹中无法启动。
py_getHomePackageAndActivityPackage函数:通过启动应用,然后获取当前应用包名和活跃包名的方式打印信息,有时候设备性能较差,启动较慢时会获取失败。如果默认的5秒延迟时间不够,可以手动传等待时间。
控件兼容性很差,一般我不会使用,比如:同一个模拟器,选择不同手机型号都有可能不同。而且,有概率出现开发和生产环境运行不同的情况,开发环境正常,到了生产环境有概率报错。因此,这部分内容,我不太可能会增加了。当然,如果后期有好的功能方向,我也会考虑封装下函数。
9.无障碍功能
1.py_clickPoint
/**
* 通过随机区域获取点击点
* @param {number} currentX - 必填,点横坐标
* @param {number} currentY - 必填,点纵坐标
* @param {number} [offsetX=6] - 选填,横坐标偏移量,默认为6
* @param {number} [offsetY=6] - 选填,纵坐标偏移量,默认为6
* @returns {Array} 返回点数组,分别为[横坐标,纵坐标]
*/
function py_clickPoint(currentX, currentY, offsetX, offsetY)
函数注意事项如下:
我推荐大家点击通过此函数来完成,能够通过随机变量来设置点击位置。很多游戏会通过检测点击位置来判断是否为脚本,通过此函数,就不会一直点一个点,降低被检测到的概率。需要注意,偏移量是指允许点击的区域,比如横坐标偏移量使用默认值6,而点横坐标为100,那么横坐标会随机点击97~103的点。
2.py_initFloaty
/**
* @param {Object} options - 选填,配置信息
* @param {number} [options.hideTime=5000] - 选填,未操作悬浮窗时间,超过这个时间悬浮窗会隐藏,默认为5000,单位:毫秒
* @param {number} [options.everyUpdateLogCount=20] - 选填,每次更新日志数量,默认为20
* @param {string} [options.textSize=14] - 选填,字体大小,默认为14,单位:sp
* @param {boolean} [options.hasSwitchApp=true] - 选填,交互时是否自动切换回应用(需要root权限),默认为true
* @returns {Object} 自定义事件触发器
* - "on函数": 监听事件
* - "emit函数": 触发事件
* - "off函数": 移除事件
* - "once函数": 触发一次事件后移除
*/
function py_initFloaty(options)
函数案例如下:
"ui";
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件导出";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
let options = {
everyUpdateLogCount: 40,
textSize: 16
}
let customEventEmitter = pyPlugin.py_initFloaty(options);
customEventEmitter.once("start", function () {
console.log("脚本已经运行");
})
customEventEmitter.on("stop", function () {
console.log("脚本已经停止");
})
customEventEmitter.on("console", function () {
console.log("脚本打开控制台");
})
customEventEmitter.on("setting", function () {
console.log("脚本打开UI设置");
})
customEventEmitter.on("exit", function () {
console.log("脚本已退出");
})
函数注意事项:
这个悬浮窗初始化函数用起来非常简单,但是手搓这个功能花费了巨量时间。这个悬浮窗是在基础篇介绍的悬浮窗基础上进行了改造,但是比原有悬浮窗兼容性更好、功能更强、适配性更好,新版悬浮窗有以下几个功能:
1.悬浮窗改为横向显示功能的方式,适配左右两侧,并且采用图标+文字的显示方式。
2.悬浮窗增加长时间未操作隐藏功能,适配左右两侧,减少悬浮窗遮挡控件。
3.内置自定义控制台打印控件,此功能为悬浮窗最重要的功能模块。将控制台打印日志改为本地保存的方式,自动保存在插件根路径下的py_log.txt文件中。此函数会自动将每次运行的日志分组,然后将最新的日志分组显示在前面,将旧日志分组显示在后面。同时,支持根据类型设置不同颜色、内容搜索高亮、位置定位、清空日志、懒加载等多个功能。
4.悬浮窗五个功能通过自定义事件触发器分别触发,可以通过绑定对应的事件完成操作。
5.悬浮窗兼容7201280和10801920两个常用分辨率机型,其他分辨率也能使用,但是显示效果不如这两个分辨率好。
上面介绍的自定义控制台打印控件不够详细,我再次介绍下这个控件的功能以及设计原因。由于最新日志会保存到日志文件最后,我初次设置考虑的是加载全部日志后,自动滚动到底部,但是,如果日志过多加载非常慢,同时搜索会从头开始搜索,非常不友好。因此,考虑将最新日志放在前面,将旧日志显示在后面。但是,完全按照时间进行倒序,就会导致每次“结束运行”都在“开始运行”上面,尤其是特殊类型数据输出时,看起来非常不习惯。因此,采用了先将数据分组,然后将最新输出的日志放在前面,这样既能实现新日志放在前面,又能按照顺序进行输出的功能。在这个过程中,由于日志只有时间,我们需要按照日志输出前后给他赋值虚拟日期,否则会出现类似于昨天20点的日志在今天10点日志前面的情况。
为了更好的查找输出内容,增加了日志搜索功能。我开始考虑是一个text控件中,然后将所有的打印信息转换为html,搜索内容加上html的亮度显示,同时根据回车加上html的换行方式。html显示方式样式比较丑,并且除了手动换行外,如果内容过长也会自动换行,这就增加了计算每个搜索内容距离顶部高度难度,我又不想放弃搜索定位的功能。我后来考虑了通过text控件来显示每一行日志,左下角的函数其实是text控件个数,由于显示内容长度等原因,真正行数必然比我显示的要多,但是我显示的行数和我在每行日志里面前面的序号是对应的。在日志前面增加序号,是为了让大家准确区分不同的行数,也利于我们在小屏幕的情况下查看信息。以上面为例,行数为2985,需要加载同样数量的text控件,那速度会非常慢。我们其实并不需要这么多数据,只需要前面的打印信息,再结合List控件的功能,我自己封装了个textList控件,也List控件类似,也支持懒加载方式。我在开始时,会加载一定数量的text控件,然后滚动到底部时,然后再加载这个数量的text控件,这样既能满足了功能需求,又不会加载非常慢。有小伙伴可能会问,我在搜索功能模块有个“下一个”功能,如果控件没加载怎么办?小伙伴放心,我写东西尽量追求完美,如果控件没有加载,他会先将跳转过程中的所有text控件都加载,然后再执行跳转功能。这样加载大量数据时,有概率会导致定位不是非常准确,会偏移几行,这个是设置加载等待时间导致的,我也不能将时间设置太久,那样会很影响体验。最终,设置了这个时间,有概率偏移几行也没问题,毕竟还有高亮显示呢。
此函数的参数,均为非必填,一般使用默认即可,参数或返回值使用注意事项:
options.hideTime参数:设置过低导致我们刚点开功能就收缩,设置过高导致很久隐藏而遮挡内容。
options.everyUpdateLogCount参数:设置过低会导致不显示滚动条,就无法触发滚动到底部的懒加载功能,最终导致后续日志无法显示。设置过高会影响日志加载速度,非常影响体验。
options.textSize参数:只能接受数字,并且单位为sp。
options.hasSwitchApp参数:此参数用于是否自动切换回脚本页面,依托于root权限。在不支持root权限的环境下,开启此功能会报错。由于悬浮窗的控制台打印和配置功能,需要在脚本页面才能看到,我们悬浮窗大部分时间显示在其他应用上,我们需要先切换回脚本应用,才能查看这些功能。如果在不支持root权限的环境下,需要将options.hasSwitchApp设置false,点击配置和控制台打印后,需要手动点下脚本应用去查看信息。我可考虑了非root切换应用的方式,但是这种方式会导致悬浮窗消息,因此,只有root环境下切换应用或者手动切换应用两种方式才能实现功能。
返回值:此值其实就是py_initEventEmitter函数初始化的事件触发器,只是在悬浮窗中自动触发各种事件,我们只需要绑定对应事件,然后执行相关函数。一共有五个事件,分别为start、stop、console、setting和exit,分别代表开始、停止、打印、设置和退出五个功能被点击。在开始事件绑定函数中最好启动一个新的线程,然后在停止事件绑定函数中结束这个线程,实现脚本的多次启动。打印事件绑定函数中,最好也停止下脚本,不然切换回脚本页面仍然执行脚本会出现乱点的情况,悬浮窗会自动切换回启动图标。设置事件绑定函数中,也要停止下脚本,悬浮窗会自动切换回启动图标,然后再加载下自己的ui配置。退出事件绑定函数中,悬浮窗会自动将一些资源回收并且退出,你也需要在这个函数中,将自己的资源进行回收,但是不要退出脚本。这些功能会在后续项目开发经验中详细介绍。
功能使用介绍如下:
1.需要搜索后才能使用“上一个”和“下一个”功能,搜索内容后,会自动调到第一搜索到内容的位置。
2.清空日志功能会将日志文件保存为空文件。
3.搜索到的匹配项数量,是指text控件数量,也就是日志前面标号数量。一个text控件中存在多个匹配项,虽然高亮多个,但是只跳转一次。
4.将搜索框的内容清除后,然后再点一次搜索,会取消所有的高亮。
需要特别注意,此函数在雷电模拟器9(android 9)中不支持,并不是运行报错,而是悬浮窗显示后,返回系统主界面会导致悬浮窗消失,我打包后此问题仍然存在。
10.内存功能
1.py_getChineseNameArrayFromDecimalArray
/**
* 将十进制数字数组转换回中文名
* @param {Array} decimalArray - 必填,十进制数字数组
* @param {Object} options - 选填,配置选项
* @param {boolean} [options.isLittleEndian=false] - 选填,是否为小端序,默认值为false
* @param {boolean} [options.isRemoveZero=false] - 选填,是否移除零值,默认值为false
* @returns {Array} 返回中文字符数组
*/
function py_getChineseNameArrayFromDecimalArray(decimalArray, options)
2.py_getDecimalArrayByChineseName
/**
* 将中文名转换为十进制数据
* @param {string} currentName - 必填,中文名
* @param {Object} options - 配置选项
* @param {boolean} [options.isLittleEndian=false] - 选填,是否为小端序,默认值为false
* @param {boolean} [options.isStringArray=false] - 选填,返回数组是否为字符串数组,默认值为false
* @param {boolean} [options.getGGString=false] - 选填,是否返回GG修改器字符串,默认值为false
* @returns {Array|string} options.getGGString为true时返回字符串;否则,返回数组
*/
function py_getDecimalArrayByChineseName(currentName, options)
3.py_decimalArrayToHexArray
/**
* 将十进制数组转换为十六进制字符串数组
* @param {Array} decimalArray - 十进制数字数组
* @param {Object} options - 配置选项(可选)
* @param {boolean} [options.uppercase=true] - 是否转换为大写字母,默认true
* @param {boolean} [options.padding=true] - 是否补零到8位,默认true
* @param {string} [options.prefix=""] - 前缀,如"0x",默认空
* @returns {Array} 返回十六进制字符串数组
*/
function py_decimalArrayToHexArray(decimalArray, options)
4.函数1-3案例
内存功能1.js文件代码如下:
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
let chineseName = "按键精灵";
// 获取大端序十进制数组
let bigEndianDecimalArray = pyPlugin.py_getDecimalArrayByChineseName(chineseName);
console.log("大端序十进制数组:");
console.log(bigEndianDecimalArray);
console.log("------------------------");
// 获取小端序十进制数组
let littleEndianDecimalArray = pyPlugin.py_getDecimalArrayByChineseName(chineseName, { isLittleEndian: true });
console.log("小端序十进制数组:");
console.log(littleEndianDecimalArray);
console.log("------------------------");
// 获取小端序十进制GG修改器字符串
let littleEndianDecimalString = pyPlugin.py_getDecimalArrayByChineseName(chineseName, { isLittleEndian: true, getGGString: true });
console.log("小端序十进制GG修改器字符串:");
console.log(littleEndianDecimalString);
console.log("------------------------");
// 获取小端序十进制字符串数组
let littleEndianDecimalStringArray = pyPlugin.py_getDecimalArrayByChineseName(chineseName, { isLittleEndian: true, isStringArray: true });
console.log("小端序十进制字符串数组:");
console.log(littleEndianDecimalStringArray);
console.log("------------------------");
// 将打端序十进制数组转换为中文名称
let chineseNameFromBigEndian = pyPlugin.py_getChineseNameArrayFromDecimalArray(bigEndianDecimalArray);
console.log("大端序十进制数组转换为中文:");
console.log(chineseNameFromBigEndian);
console.log("------------------------");
// 将小端序十进制数组转换为中文名称
let chineseNameFromLittleEndian = pyPlugin.py_getChineseNameArrayFromDecimalArray(littleEndianDecimalArray, { isLittleEndian: true });
console.log("小端序十进制数组转换为中文:");
console.log(chineseNameFromLittleEndian);
console.log("------------------------");
// 将十进制数据转换为十六进制
let hexArray = pyPlugin.py_decimalArrayToHexArray(littleEndianDecimalArray);
console.log("小端序十进制数组转换为十六进制:");
console.log(hexArray);
console.log("------------------------");
5.函数1-3注意事项
我们实现内存操作功能,大约90%的时间都在GG修改器中找数据保存到内存信息中的规则,然后换不同的环境来验证规则的通用性。通过GG修改器发现,有大量内存信息,我们操作内存之前,首先要确定内存数据属于哪个内存区域。一般情况下,我们会通过应用中的数字来找到内存所属区域,这个过程非常复杂。找到这个数据,然后找他其对应的区域,后面函数能够指定内存区域,内存区域在同一个android环境一般不会改变,但是如果从android 7环境切换android 9环境会导致内存区域改变。我们脚本一般在一个android环境下开发,如果是多环境,需要考虑内存操作适配问题。如果确实有这个需求也没事,不同环境下内存区域发生改变,但是内存数据保存规则是不变的。
android 7会将内存数据划分的更加精细,内存数据都是保存一个内存区域里,划分的精细,我们可以更加快速的查找。而android 9内存数据划分非常粗糙,内存数据会保存在一个非常大的区域,如果不设置偏移量,查询速度非常慢,这就需要花更多的时间来找内存规则。对比与android 9环境,android7优势更大,这也就是为什么很多内存操作喜欢在android 7里面。android 7环境找到通用数据规则,再配合适合的偏移量,很容易秒出结果。
我只能给大家分享经验,如果想学习好内存操作,需要自己学习好GG修改器操作。这种教程非常多,我就不去介绍了。如果你熟悉GG修改器,你就会明白这个三个函数对你快速定位内存数据帮助有多大。上面函数中,会有个参数来指定大端序还是小端序,这种情况会出现在中文等特殊数据上面,因为这些特殊数据都比较大,会有这两种保存方式。GG修改器的内存搜索只能搜索大端序的中文,小端序的中文没办法搜索。我不推荐大家通过中文搜索,我还是推荐大家先通过上面函数来获取中文对应的十进制数据,然后再搜索十进制数据,这样更快速准确。有小伙伴可能会问,我不知道中文数据保存的是大端序还是小端序,我们可以先用默认值大端序进行搜索,如果搜不到,然后再用小端序搜索。我只能说,大端序情况比较多,小端序情况比较少,一个应用一般只会只用一种中文数据保存规则,只要确定了,以后可以直接使用。
我们可以通过上面函数,来完成十进制数据与中文不断转换,既能转换GG修改器需要的数据,又能将内存读取到的数据转换为中文。后面介绍内存获取函数中,会有些十进制的内存地址,然而GG修改器内存地址为十六进制,我们通过py_decimalArrayToHexArray函数进行转换。
除了py_decimalArrayToHexArray函数外,小伙伴们一定要仔细查看另外两个函数的参数,每个参数的使用都会提高内存操作的效率。py_decimalArrayToHexArray函数,我们一般使用默认值即可,默认会转换成和GG修改器一致的内存地址数据。
GG修改器使用过程中,我们只需要会使用,Dword、Word和Byte三种类型即可,其他数据类型几乎用不到,特殊数据保存到内存中还是数字。Dword一般保存内存地址,对于比较长的数据,有可能会保存在多个地方,然后通过地址来连接;Word是最常用的数据类型,数字大部分都是用这种类型;Byte一般会保存一些比较短的数据。这里分享内存经验有点多了,后面我会结合内存操作函数再次分享经验。
6.py_getMemoryPort
/**
* 获取内存端口
*/
function py_getMemoryPort()
7.py_restartTcpPort
/**
* 重启tcp端口
*/
function py_restartTcpPort()
8.py_initRegion
/**
* 初始化内存信息
* @param {Object} paramObj - 参数对象
* @param {string} paramObj.pid - 必填,进程id
* @param {string} paramObj.perms - 选填,内存包含权限
* @param {string} paramObj.path - 选填,内存包含路径(不能包含:)
* @param {number} [paramObj.position] - 选填,内存位置,默认为1
* @returns {Object} 返回内存区域信息
*/
function py_initRegion(paramObj)
9.py_callMemoryTCP
/**
* 通过socket发送数据
* @param {Array} cmdTokens - 必填,请求信息
* @returns {string|number|null} 返回请求到的返回值,参数类型为字符串或数字,请求失败或者没有返回值时返回null
*/
function py_callMemoryTCP(cmdTokens)
10.py_stopTcpConnection
/**
* 停止TCP连接
* @returns {boolean} 返回是否停止成功
*/
function py_stopTcpConnection()
11.py_setMemoryEditor
/**
* 设置内存操作
*/
function py_setMemoryEditor()
12.py_searchMemory
/**
* 搜索内存信息
* @param {Object} paramObj - 参数对象
* @param {Array} paramObj.patternStrArray - 必填,规则字符串数组,每个位置都对应一个规则
* 规则说明:
* - [x-y]: 匹配x至y之间的数据
* - >x: 匹配大于x的数据
* - <x: 匹配小于x的数据
* - x: 匹配值为x的数据
* - ??: 匹配任意数据
* @param {Array} paramObj.dataByteArray - 必填,字节数组,每个位置读取数据的字节数,和patternStrArray一一对应
* @param {Object} paramObj.appendMap - 选填,追加规则,属性为patternStrArray的索引字符串
* 设置索引字符串后,会根据索引字符串属性对应的数组来搜索数据,将追加规则对象的数据拼接到符合规则字符串数组的数据后面
* 索引字符串属性对应的数组需要传递特定值:
* - 值为1时:[1, 读取数据字节大小, 读取数量] - 将索引对应位置的数据当做内存地址,跳转到内存地址读取数据
* - 值为2时:[2, 偏移大小, 读取数据字节大小, 读取数量] - 偏移索引对应内存地址一定位置后读取数据
* @param {number} [paramObj.startOffset] - 选填,偏移起始内存大小,将偏移后的内存当做起始位置
* @param {number} [paramObj.offsetSize] - 选填,读取内存区域大小
* @param {Object} [paramObj.convertChineseMap] - 选填,转换为中文的配置
* @param {boolean} [paramObj.convertChineseMap.isRemoveZero=false] - 选填,是否将数值为0的数据转换为空字符,默认为false
* @param {boolean} [paramObj.convertChineseMap.isLittleEndian=false] - 选填,是否为小端序,默认为false
* @param {Array} paramObj.convertChineseMap.convertIndexArray - 必填,需要转换的索引数组
* @returns {Object} 包含内存地址和对应数据的对象,格式为{"内存地址": [符合规则的数据, 符合追加规则的数据]}
*/
function py_searchMemory(paramObj)
13.py_updateMemory
/**
* 修改内存信息
* @param {Object} paramObj - 参数对象
* @param {Array} paramObj.addrArray - 必填,地址数组
* @param {Array} paramObj.valArray - 必填,值数组
* @returns {boolean} 是否修改成功
*/
function py_updateMemory(paramObj)
14.py_getMemory
/**
* 获取内存信息
* @param {Object} paramObj
* @param {number} paramObj.startAddress - 必填,起始地址
* @param {number} paramObj.size - 必填,读取数据字节大小
* @param {number} paramObj.count - 必填,读取数据数量
* @returns {Array} 内存数据
*/
function py_getMemory(paramObj)
15.函数6-14案例
内存功能2.js文件代码如下:
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 1.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 2.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 3.设置内存操作
pyPlugin.py_setMemoryEditor();
// 4.内存初始化
// 获取当前线程id
let currentPid = pyPlugin.py_getProcessIdByPackageName("com.skymobi.xjwq1");
// 初始化内存信息
let paramObj;
// 获取android版本
let androidVersion = device.release;
androidVersion = androidVersion.includes(".") ? androidVersion.substring(0, androidVersion.indexOf(".")) : androidVersion;
console.log("Android版本:" + androidVersion);
// 如果是android 7
if (androidVersion == 7) {
paramObj = {
pid: currentPid,
perms: "rwxp",
path: "libc_malloc"
}
} else if (androidVersion == 9) {
paramObj = {
pid: currentPid,
perms: "rw-p",
path: "libc_malloc",
position: 5
}
}
console.log(paramObj);
pyPlugin.py_initRegion(paramObj);
// 5.搜索内存
paramObj = {
dataByteArray: [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4],
appendMap: {
"12": [1, 2, 6]
},
convertChineseMap: {
isRemoveZero: true,
isLittleEndian: true,
convertIndexArray: [13, 14, 15, 16, 17, 18]
}
}
// 如果是android 7
if (androidVersion == 7) {
paramObj["patternStrArray"] = ["32", "0", "2", "[1-1500]", "[1-1500]", "<100", "<100", "[1-1500]", "??", "??", "<1000", ">58536", ">1000000000"];
paramObj["startOffset"] = 92160;
paramObj["offsetSize"] = 560000;
} else if (androidVersion == 9) {
// 如果是android 9
paramObj["startOffset"] = 150400000;
paramObj["offsetSize"] = 560000;
paramObj["patternStrArray"] = ["32", "0", "2", "[1-1500]", "[1-1500]", "<100", "<100", "[1-1500]", "??", "??", "<1000", ">50000", ">1000000000"];
}
let searchMemoryObj = pyPlugin.py_searchMemory(paramObj);
// 6.操作内存
for (let key in searchMemoryObj) {
let currentArray = searchMemoryObj[key];
let monsterX = currentArray[3];
let monsterY = currentArray[4];
let nameArray = currentArray.slice(13);
let emptyNameIndex = nameArray.indexOf("");
nameArray = nameArray.slice(0, emptyNameIndex);
console.log(nameArray.join("") + ",横坐标:" + monsterX + ",纵坐标:" + monsterY);
// 起始位置地址
let startMemoryAddress = Number(key);
// 怪横坐标内存地址
let monsterXMemoryAddress = startMemoryAddress + 3 * 2;
// 怪纵坐标内存地址
let monsterYMemoryAddress = startMemoryAddress + 4 * 2;
// 修改内存
paramObj = {
addrArray: [monsterXMemoryAddress, monsterYMemoryAddress],
valArray: [100, 100]
}
let hasUpdate = pyPlugin.py_updateMemory(paramObj);
console.log("位置修改是否成功:" + hasUpdate);
//获取内存信息
paramObj = {
startAddress: startMemoryAddress,
size: 2,
count: 19
}
currentArray = pyPlugin.py_getMemory(paramObj);
monsterX = currentArray[3];
monsterY = currentArray[4];
console.log("修改后,横坐标为" + monsterX + ", 纵坐标为" + monsterY);
}
// 按键精灵内存操作(中文使用大端序保存)
currentPid = pyPlugin.py_getProcessIdByPackageName("com.cyjh.mobileanjian");
console.log(currentPid);
if (!currentPid) {
console.log("按键精灵未启动");
exit();
}
// 初始化内存信息
paramObj = {
pid: currentPid,
perms: "rw-p",
path: "/dev/ashmem/dalvik-main space (deleted)",
position: 2
}
pyPlugin.py_initRegion(paramObj);
console.log(pyPlugin.py_getDecimalArrayByChineseName("快速引导", {
getGGString: true
}));
// 搜索内存
paramObj = {
patternStrArray: ['4', '0', '0', '0', '24555', '36895', '24341', '23548'],
dataByteArray: [2, 2, 2, 2, 2, 2, 2, 2],
convertChineseMap: {
isRemoveZero: true,
convertIndexArray: [4, 5, 6, 7]
}
}
searchMemoryObj = pyPlugin.py_searchMemory(paramObj);
let decimalArray = pyPlugin.py_getDecimalArrayByChineseName("测试测试");
for (let key in searchMemoryObj) {
let currentArray = searchMemoryObj[key];
let currentMemoryAddress = Number(key);
let firstMemoryAddress = currentMemoryAddress + 4 * 2;
let addrArray = [];
console.log(currentArray);
for (let index = 0; index < decimalArray.length; index++) {
addrArray.push(firstMemoryAddress + index * 2);
}
console.log("内存修改的十六进制地址:" + pyPlugin.py_decimalArrayToHexArray(addrArray));
let paramObj = {
addrArray: addrArray,
valArray: decimalArray
}
console.log(pyPlugin.py_updateMemory(paramObj));
}
16.函数6-14注意事项
上面案例给出了大端序和小端序两种保存中文方式的内存修改,小端序数据是我当初写脚本的一个游戏,我对这个游戏内存数据规则已经非常了解了,我做了个android 7和android 9环境下的内存适配;大端序数据是我为了介绍功能而写的,以按键精灵为例进行了介绍,内存规则没有进行通用规则适配,稳定性较差。按键精灵修改数据后,需要将下面的tab页切换到“发现”然后再切回到第一个才会完成修改,这是只有tab切换时才会读取内存数据导致的。我介绍上面的案例目的是为了让大家知道,内存操作函数是可以使用,并不是让大家运行使用。因为小端序数据由于你没有那个游戏无法操作成功,大端序数据使用的按键精灵没有做通用规则适配有概率运行不成功。我说的通用规则适配,并不是说切换android环境的适配,我是指同一个android环境,模拟器和虚拟机不同设备内存会有稍微不同,需要找到通用规则。哪怕都是用模拟器,不同电脑运行,内存也有稍微不同,但是找到了通用规则同一android环境都能适配,如果通用规则做的好,不同android环境也能适配。android环境只是将内存保存位置改了,但是内存数据规则差距不大。
上面关于内存操作的函数非常多,有很多是插件内存自动调用的函数,我们不需要学习这些函数。我们只需要学习函数8和函数11-14共五个函数,函数py_setMemoryEditor用于初始化内存操作,函数py_initRegion初始化内存信息,函数py_searchMemory用于根据规则查找内存,函数py_updateMemory用于修改内存,函数py_getMemory用于根据内存地址查找内存。
py_initRegion函数:perms和path不能包含":"等特殊符号,有些path中包含":",请匹配":"前面或者后面部分。同时,有可能匹配多个内存区域,可以通过position设置需要指定的内存位置。可以按照下面步骤查看内存区域的perms和path。
下面是GG修改器通过仅限于内存搜索功能看到的内存区域,序号1的位置为perms,序号2的位置为path。其中,path中包含":",我们可以将path设置为"libc_malloc"。
py_searchMemory函数:将我写项目时能用到的所有内存查找融合后的功能,参数非常多,功能非常强大。尤其是追加规则,这属于特殊功能了,追加规则初值设置为1时,用于处理数据比较多的内存数据,他会有个内存地址指向另一个位置,我们通过这个功能可以完成数据的拼接;追加规则设置为2时,用于获取偏移一定位置的数据,可以将无用数据去掉后,继续读取。
py_updateMemory函数:内存修改需要注意,修改后的字节大小和数据数量需要与原来修改之前的数据一一对应。内存类似于一个个格子,每个格子代表的意义和大小都已经固定,我们如果改变了,会导致内存崩溃而退出应用。我们一般只会修改一些数字数据,这数据一般保存在Word数据类型,大小固定为2。我上面是为了测试修改功能才去修改文字,正常情况下,我们会和小端序案例类似,用于修改怪的坐标位置,然后达到“吸怪”的目的。内存修改时,需要遵循游戏规则,不然容易导致封号,虽然我能将全图怪都吸到面前,但是考虑了安全性等问题,我只会吸人物坐标一定范围内的怪。
在内存功能部分,我已经尽量将内存操作的经验分享给大家,但仍然是杯水车薪。我介绍的再多也只能提供指引作用,小伙伴们需要花大量时间来研究GG修改器和内存信息之间的关系,使用多了会有一种顿悟的感觉,真正理解了"内存如流水,内存数据取决于如何取值"这句话,就算真正入门内存操作了。这个时间会非常漫长,可能几个月,也可能几年。这里将内存比喻成流水,上面又说“内存如格子,大小固定”,看似冲突的两句话,实际上并不冲突。内存如流水,是指我们手动去规定大小来取值,指定不同的大小取值不同。内存如格子,是指应用已经指定好了每个格子的作用和大小,我们操作过程中,不能改变格子原有规则,否则会导致内存崩溃。内存操作的经验是永远讲不完的,如果还有机会,我会在项目经验中再来介绍下这部分内容。但是,我需要完成的内容太多了,不知道什么时候才能介绍。
11.普通功能
1.py_isPortOpen
/**
* 判断端口是否开启
* @param {number} port - 必填,端口号
* @param {number} [timeout=3000] - 选填,等待时间(单位:毫秒),默认为3000
* @returns {boolean} 返回是否开启端口
*/
function py_isPortOpen(port, timeout)
2.py_inspectObject
/**
* 分析对象并打印
* @param {Object} obj - 必填,需要分析的对象
* @param {number} [type=0] - 选填,分析类型,默认为0
* - 0: 打印函数和属性
* - 1: 打印函数
* - 2: 打印属性
*/
function py_inspectObject(obj, type)
3.py_arraysContentEqual
/**
* 判断两个数组的内容是否相等(不区分先后顺序)
* @param {Array} arr1 - 必填,数组1
* @param {Array} arr2 - 必填,数组2
* @returns {boolean} 返回是否相等
*/
function py_arraysContentEqual(arr1, arr2)
4.py_hasDuplicateArrayItem
/**
* 判断数组是否有重复项
* @param {Array} arr - 必填,数组
* @returns {boolean} 返回有重复项
*/
function py_hasDuplicateArrayItem(arr)
5.py_upperStringFirstCharacter
/**
* 将字符串首字母大写
* @param {string} str - 必填,字符串
* @returns {string} 返回操作后的字符串
*/
function py_upperStringFirstCharacter(str)
6.py_UpperFirstCharAndPrefixString
/**
* 字符串首字符大写,并在前面拼接字符串
* @param {string} currentStr - 必填,需要首字母大写的字符串
* @param {string} prefixStr - 必填,拼接在前面的字符串
* @returns {string} 返回操作后的字符串
*/
function py_UpperFirstCharAndPrefixString(currentStr, prefixStr)
7.py_getCurrentTime
/**
* 获取当前时间戳
* @returns {number} 返回时间戳
*/
function py_getCurrentTime()
8.py_getCurrentDateString
/**
* 获取当前日期字符串
* @returns {string} 返回日期字符串
*/
function py_getCurrentDateString()
9.py_getEndTime
/**
* 获取结束时间(支持跨天)
* @param {number} startHour - 必填,开始小时
* @param {number} startMinute - 必填,开始分钟
* @param {number} endHour - 必填,结束小时
* @param {number} endMinute - 必填,结束分钟
* @returns {number} 返回时间戳
*/
function py_getEndTime(startHour, startMinute, endHour, endMinute)
10.py_hasCurrentTimeIsTimeInterval
/**
* 判断当前时间是否在时间区间内
* @param {number} startHour - 必填,开始小时
* @param {number} startMinute - 必填,开始分钟
* @param {number} endHour - 必填,结束小时
* @param {number} endMinute - 必填,结束分钟
* @returns {boolean} 返回是否在区间内
*/
function py_hasCurrentTimeIsTimeInterval(startHour, startMinute, endHour, endMinute)
11.函数1-10案例
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
console.log("8888端口是否开启: " + pyPlugin.py_isPortOpen(8888));
let obj = {
test1: 123,
test2: function () {
console.log("test");
}
}
// 分析对象
pyPlugin.py_inspectObject(obj);
let arr1 = [1, 2, 3];
let arr2 = [3, 2, 1];
console.log("两个数组是否一致:" + pyPlugin.py_arraysContentEqual(arr1, arr2));
let arr3 = [1, 2, 3, 1, 4];
console.log("数组是否有重复项:" + pyPlugin.py_hasDuplicateArrayItem(arr3));
let str1 = "adffdf";
console.log("将" + str1 + "字符串首字母大写:" + pyPlugin.py_upperStringFirstCharacter(str1));
let str2 = "test";
console.log("将" + str1 + "字符串首字母大写,并且在前面拼接" + str2 + "后:" + pyPlugin.py_UpperFirstCharAndPrefixString(str1, str2));
console.log("当前时间戳:" + pyPlugin.py_getCurrentTime());
console.log("当前日期字符串:" + pyPlugin.py_getCurrentDateString());
let startHour = 2;
let startMinute = 30;
let endHour = 11;
let endMinute = 30;
console.log("结束时间戳:" + pyPlugin.py_getEndTime(startHour, startMinute, endHour, endMinute));
console.log("当前时间是否在时间范围:" + pyPlugin.py_hasCurrentTimeIsTimeInterval(startHour, startMinute, endHour, endMinute));
12.函数1-10注意事项
py_inspectObject函数:可以分析js、autojs和java对象,已经做了适配,自动通过类型分析。但是,只分析当前对象下面的属性和函数,不分析继承父类的属性和函数。因为autojs和java对象有复杂的继承关系,那样分析非常慢,并且有很多属性和函数进行干扰。由于上面这种原因,会出现分析到的公开函数是能用的,没分析到的函数,有可能在父类中存在,也有概率能用。
py_getEndTime和py_hasCurrentTimeIsTimeInterval函数:这两个函数完全是为了定时任务模块而封装的函数,我们可以在ui中填写定时任务每天每个时间段的任务情况,然后通过这两个函数来判断是否在当前任务段,是非常方便的,后面会考虑介绍这部分内容。
13.py_initEventEmitter
/**
* 初始化自定义事件触发器
* @returns {Object} 返回创建的事件触发器
* - "on函数": 监听事件
* - "emit函数": 触发事件
* - "off函数": 移除事件
* - "once函数": 触发一次事件后移除
*/
function py_initEventEmitter()
函数案例如下:
// 文件路径获取
// 生产环境相对路径文件夹
let procRelativeFolder = "./";
// 1.修改实际项目名称
// 项目名称,根据实际情况替换
let projectName = "插件导出";
// 开发环境相对路径文件夹
let devRelativeFolder = files.cwd() + "/" + projectName + "/";
// 相对路径文件夹
// 开发时,切换到开发环境
// 打包时,切换到生产环境
// 读取相对路径文件时,都采用这种方式
// 为了实现编辑器能够直接运行,切换为开发环境
let relativeFolder = devRelativeFolder;
// 2.替换为自己的文件总地址
// 文件总地址
let fileRootPath = "/storage/emulated/0/com.py.test/";
// 如果根路径不存在,创建根路径
if (!files.exists(fileRootPath)) {
files.create(fileRootPath);
}
// 3.获取插件
let pyPlugin = require(relativeFolder + "py/pyPlugin.min.js");
// 4.插件属性赋值(必须完成)
// 赋值相对路径文件夹
pyPlugin.relativeFolder = relativeFolder;
// 赋值文件总地址
pyPlugin.fileRootPath = fileRootPath;
// 初始化事件触发器
let eventEmitter = pyPlugin.py_initEventEmitter();
// 绑定事件test1
eventEmitter.on("test1", function (data) {
console.log("事件test1成功触发,数据为: " + data.msg);
});
// 绑定事件test2,触发一次后移除
eventEmitter.once("test2", function (data) {
console.log("事件test2成功触发,数据为: " + data.msg);
});
// 绑定事件test3
let test3Function = function (data) {
console.log("事件test3成功触发,数据为: " + data.msg);
}
eventEmitter.on("test3", test3Function);
// 触发事件test1
eventEmitter.emit("test1", { msg: "第一次触发" });
// 触发事件test1
eventEmitter.emit("test1", { msg: "第二次触发" });
// 触发事件test2
eventEmitter.emit("test2", { msg: "第一次触发" });
// 触发事件test2
eventEmitter.emit("test2", { msg: "第二次触发" });
// 触发事件test3
eventEmitter.emit("test3", { msg: "第一次触发" });
// 手动移除test3事件绑定的test3Function函数
eventEmitter.off("test3", test3Function);
// 触发事件test3
eventEmitter.emit("test3", { msg: "第二次触发" });
注意事项如下:
此函数创建的事件触发器对象与其他事件触发器类似,包含on、once、emit和off四个函数。
on函数:用于将事件绑定到函数上,需要传递两个参数,分别为事件名称和需要绑定的函数,函数可以通过一个参数接收emit函数触发事件时传递的数据。
once函数:功能和on函数类似,但是事件触发一次后,就会将once函数绑定的事件函数移除。
emit函数:用于触发事件,需要传递两个参数,分别为事件名称和传递数据(可不传递)。我们一般传递对象数据,因为一个参数的情况下,对象数据能够容纳更多的数据。
off函数:移除事件中绑定的函数,需要传递两个参数,分别为事件名称和绑定的函数。如果需要手动移除绑定函数,需要将绑定函数保存到变量里面,在特殊情况下进行移除。
12.总结
目前,插件内容已经非常多了,我只能简单测试,或者特殊功能多环境测试。如果有插件功能出现bug,可以私信我进行修改,请见谅!!!
特别注意,只有通过个人主页博客或者个人介绍中方式,才能获取源码