Autojs进阶-插件(pyPlugin)

0 阅读1小时+

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,可以私信我进行修改,请见谅!!!
特别注意,只有通过个人主页博客或者个人介绍中方式,才能获取源码