在自动化脚本开发中多线程并发编程能够大幅提升脚本的执行效率,尤其适用于需要同时处理多个任务的自动化场景,比如多应用同时操作、批量数据处理等。而线程锁则是解决多线程并发访问共享资源时数据混乱、执行冲突的核心手段。本文将从多线程基础、线程锁使用、实战案例及开发注意事项四个方面,全面讲解冰狐平台中多线程与锁的落地实现。
一、多线程开发基础
冰狐智能辅助平台的 JS 脚本原生支持多线程并发编程,通过Thread构造函数及配套方法实现线程的创建、启动、停止和等待,同时结合平台特有的全局变量机制实现线程间的数据通信,无需依赖外部框架,语法简洁且贴合自动化脚本的开发场景。
1.1 核心 API 介绍
冰狐平台的多线程核心 API 围绕Thread对象展开,主要包含构造函数和四个核心方法,各方法的功能与使用规则如下:
- Thread 构造函数 :
var t = new Thread();,用于创建一个新的线程实例,无入参,创建后线程处于未启动状态。 - start(func, params, stackSize) :启动线程的核心方法,必填参数为线程执行函数
func,选填参数为传递给执行函数的参数数组params和栈大小stackSize。其中stackSize默认为 0(使用系统默认栈大小),若线程中包含 OCR 识别等耗内存操作,建议设置为 12388608。注意 :传递执行函数时仅写函数名,不可加括号(如t.start(func)而非t.start(func()))。 - stop():强制停止线程,调用后线程立即终止执行,无入参。
- getId():获取线程的唯一标识 ID,返回值为整型,可用于线程的区分和管理。
- join(millis) :等待线程执行结束,选填参数
millis为最大等待毫秒数,不填则表示永久等待,直到线程执行完成后再继续执行主线程逻辑。
1.2 基础多线程实现 Demo
以下 Demo 实现了主线程创建子线程并传递参数,子线程执行计算任务的基础功能,可直接在冰狐平台运行,清晰展示了多线程的创建、启动和参数传递流程:
javascript
// 主线程入口函数
function main() {
console.log('主线程启动,开始创建子线程');
// 创建线程实例
var calcThread = new Thread();
// 启动子线程,传递执行函数和参数数组[100, 200],栈大小使用默认值
calcThread.start(calcTask, [100, 200]);
// 获取子线程ID并打印
console.log('子线程ID:' + calcThread.getId());
// 等待子线程执行完成,再执行主线程后续逻辑
calcThread.join();
console.log('子线程执行完成,主线程结束');
}
// 子线程执行函数:实现两数相加并打印结果
function calcTask(a, b) {
console.log('子线程开始执行计算任务');
var result = a + b;
console.log(`计算结果:${a} + ${b} = ${result}`);
// 模拟耗时操作,睡眠2秒
sleep(2000);
}
// 辅助计算函数(可选)
function calc(a, b) {
return a + b;
}
执行结果:
主线程启动,开始创建子线程
子线程ID:1
子线程开始执行计算任务
计算结果:100 + 200 = 300
(睡眠2秒)
子线程执行完成,主线程结束
1.3 批量创建线程
在实际自动化场景中,常需要同时启动多个子线程处理不同任务,比如同时操作多个 APP、批量处理多组数据,以下 Demo 实现了批量创建 3 个子线程并分别执行不同任务的功能:
javascript
function main() {
console.log('批量线程创建演示开始');
// 定义线程任务数组,包含任务函数和对应参数
var threadTasks = [
{func: task1, params: ['任务1']},
{func: task2, params: [1, 2, 3]},
{func: task3, params: [true, '测试']}
];
// 定义数组存储线程实例,便于后续管理
var threads = [];
// 批量创建并启动线程
for (var i = 0; i < threadTasks.length; i++) {
var t = new Thread();
var task = threadTasks[i];
t.start(task.func, task.params);
threads.push(t);
console.log(`第${i+1}个子线程启动,ID:${t.getId()}`);
}
// 等待所有子线程执行完成
for (var t of threads) {
t.join();
}
console.log('所有子线程执行完成,批量线程演示结束');
}
// 任务1:字符串处理
function task1(taskName) {
console.log(`${taskName}开始执行,当前时间:${new Date().getTime()}`);
sleep(1500);
console.log(`${taskName}执行完成`);
}
// 任务2:数组遍历
function task2(a, b, c) {
var arr = [a, b, c];
console.log('任务2开始遍历数组:' + arr);
sleep(2000);
console.log('任务2数组遍历完成');
}
// 任务3:布尔值判断
function task3(flag, str) {
console.log(`任务3开始执行,flag:${flag},str:${str}`);
sleep(1000);
console.log('任务3执行完成');
}
该 Demo 通过数组管理线程实例和任务参数,实现了线程的批量创建和统一管理,符合自动化脚本中批量处理任务的开发需求,执行时 3 个子线程会并发运行,总执行时间由耗时最长的线程(任务 2,2 秒)决定,而非各线程耗时之和,充分体现了多线程的并发优势。
二、线程锁的核心作用与实现
多线程并发虽然能提升效率,但当多个线程同时访问和修改共享资源 (如全局变量、设备硬件、APP 界面元素)时,会出现数据竞争、执行顺序混乱等问题,比如一个线程正在修改全局变量,另一个线程同时读取该变量,会导致读取到错误的中间值。冰狐平台提供了Lock对象实现线程锁机制,通过lock()和unlock()方法实现对共享资源的互斥访问,确保同一时间只有一个线程能操作共享资源,解决并发冲突。
2.1 线程锁核心 API 与使用规则
冰狐平台的线程锁核心围绕Lock对象展开,核心方法仅有两个,但其使用有严格的规则,否则会导致死锁、锁失效等问题:
- Lock 构造函数 :
var lock = new Lock();,创建锁实例,无入参。 - lock():获取锁,调用后若锁未被其他线程占用,则当前线程获取锁并继续执行;若锁已被占用,则当前线程阻塞,直到其他线程释放锁。
- unlock():释放锁,调用后锁被释放,其他阻塞的线程可获取锁执行。
核心使用规则:
- 锁实例必须通过
__global声明为全局变量,确保所有线程都能访问同一个锁实例,否则各线程持有独立的锁,无法实现同步。 - 获取锁后必须释放锁,即使执行过程中出现异常,也需保证
unlock()被调用(冰狐脚本中可通过合理的逻辑判断实现)。 - 锁的作用域应尽可能小,仅包裹操作共享资源的代码块,避免因锁的范围过大导致并发效率降低。
2.2 解决共享变量并发修改问题
以下 Demo 模拟了两个线程同时修改全局共享变量的场景,分别展示无锁 和有锁的执行结果,清晰体现线程锁的同步作用:
2.2.1 无锁版本
javascript
// 声明全局共享变量
var __global count = 0;
function main() {
console.log('无锁版本:共享变量修改演示');
// 创建两个线程,同时执行加1任务
var t1 = new Thread();
var t2 = new Thread();
t1.start(addCount);
t2.start(addCount);
// 等待两个线程执行完成
t1.join();
t2.join();
// 打印最终结果,预期为2000,实际结果随机(如1256、1899等)
console.log(`无锁最终count值:${count}`);
}
// 加1任务:循环1000次,每次给count加1
function addCount() {
for (var i = 0; i < 1000; i++) {
count = count + 1;
// 模拟微小耗时,放大并发冲突
sleep(1);
}
}
无锁执行结果 :无锁最终count值:1358(结果随机,始终小于 2000),原因是两个线程同时修改count,出现了读 - 改 - 写 的并发冲突,比如线程 1 读取count=100,线程 2 同时读取count=100,两者均加 1 后赋值为 101,导致一次加 1 操作失效。
2.2.2 有锁版本
javascript
// 声明全局锁实例和全局共享变量
var __global lock;
var __global count = 0;
function main() {
console.log('有锁版本:共享变量修改演示');
// 初始化锁实例
lock = new Lock();
// 创建两个线程,同时执行加1任务
var t1 = new Thread();
var t2 = new Thread();
t1.start(addCount);
t2.start(addCount);
// 等待两个线程执行完成
t1.join();
t2.join();
// 打印最终结果,预期为2000,实际结果稳定为2000
console.log(`有锁最终count值:${count}`);
}
// 加1任务:加锁保护共享变量修改逻辑
function addCount() {
for (var i = 0; i < 1000; i++) {
// 获取锁,确保同一时间只有一个线程能修改count
lock.lock();
count = count + 1;
// 释放锁,让其他线程可以获取锁
lock.unlock();
// 模拟微小耗时
sleep(1);
}
}
有锁执行结果 :有锁最终count值:2000,结果稳定符合预期。因为线程锁保证了count = count + 1这行代码的原子性,同一时间只有一个线程能执行该代码,彻底解决了并发修改冲突。
2.3 线程锁进阶 Demo
在自动化脚本中,共享资源不仅包括变量,还包括设备的 APP 界面、硬件等,比如多个线程同时操作x信界面,会出现点击、滚动等操作的冲突。以下 Demo 实现了两个线程同时操作x信界面,通过线程锁保证操作的有序性:
javascript
// 声明全局锁实例
var __global wxLock;
function main() {
console.log('多线程操作x信:加锁演示');
// 初始化锁实例
wxLock = new Lock();
// 启动x信
var ret = launchApp('com.tencent.mm', 'txt*:x信', {maxStep: 40, afterWait: 2000});
if (1 == ret) {
console.log('x信启动成功,开始多线程操作');
// 创建两个线程,分别执行滚动和消息查看任务
var scrollThread = new Thread();
var checkThread = new Thread();
scrollThread.start(scrollWx);
checkThread.start(checkMsg);
// 等待线程执行完成
scrollThread.join();
checkThread.join();
console.log('x信多线程操作完成');
} else {
console.log('x信启动失败');
}
}
// 线程1:x信页面滚动任务
function scrollWx() {
for (var i = 0; i < 2; i++) {
// 获取锁,确保滚动操作时无其他线程操作x信
wxLock.lock();
console.log(`滚动任务:开始第${i+1}次滚动`);
scroll('up', {distance: 0.5, duration: 300, afterWait: 1000});
console.log(`滚动任务:第${i+1}次滚动完成`);
// 释放锁
wxLock.unlock();
sleep(500);
}
}
// 线程2:x信消息查看任务
function checkMsg() {
for (var i = 0; i < 2; i++) {
// 获取锁,确保查看消息时无其他线程操作x信
wxLock.lock();
console.log(`消息任务:开始第${i+1}次消息查看`);
// 模拟点击消息列表第一项
click('txt*:x信', {afterWait: 1000});
// 模拟返回
back({afterWait: 1000});
console.log(`消息任务:第${i+1}次消息查看完成`);
// 释放锁
wxLock.unlock();
sleep(500);
}
}
该 Demo 中,两个线程分别执行x信滚动和消息查看操作,通过线程锁保证了同一时间只有一个线程能操作x信界面,避免了点击、滚动操作的冲突,确保自动化操作的有序性和稳定性。
三、多线程与锁的实战综合案例
结合冰狐平台的自动化场景,以下实现一个多线程批量处理 APP 数据 + 线程锁同步结果的综合实战案例,该案例模拟了同时启动 3 个子线程分别获取x信、支付x、抖x的界面数据,通过全局共享变量收集结果,线程锁保证结果收集的有序性,主线程最终汇总并打印所有结果,充分融合了多线程和线程锁的核心用法,贴合实际自动化开发需求。
3.1 实战案例需求
- 主线程启动后,依次启动 3 个子线程,分别处理x信、支付x、抖x的界面数据获取;
- 每个子线程启动对应 APP,获取界面文本数据,将数据存入全局共享数组;
- 使用线程锁保证全局数组的修改操作有序,避免数据插入混乱;
- 主线程等待所有子线程执行完成后,汇总并打印所有 APP 的界面数据;
- 所有操作完成后,关闭所有启动的 APP,释放资源。
3.2 实战案例完整源码
javascript
// 声明全局锁实例、全局结果数组、全局APP包名映射
var __global dataLock;
var __global resultList = [];
var __global appMap = {
wx: {pkg: 'com.tencent.mm', name: 'x信'},
ali: {pkg: 'com.eg.android.AlipayGphone', name: '支付x'},
dy: {pkg: 'com.ss.android.ugc.aweme', name: 'x音'}
};
// 主线程入口函数
function main() {
console.log('多线程批量获取APP数据实战案例启动');
// 初始化锁实例
dataLock = new Lock();
// 定义线程任务数组
var tasks = [
{func: getAppData, params: [appMap.wx]},
{func: getAppData, params: [appMap.ali]},
{func: getAppData, params: [appMap.dy]}
];
// 批量创建并启动线程
var threads = [];
for (var task of tasks) {
var t = new Thread();
t.start(task.func, task.params);
threads.push(t);
console.log(`${task.params[0].name}数据获取线程启动,ID:${t.getId()}`);
}
// 等待所有子线程执行完成
for (var t of threads) {
t.join();
}
// 汇总并打印结果
console.log('==================== 数据汇总 ====================');
for (var data of resultList) {
console.log(`APP名称:${data.appName},包名:${data.pkg},界面文本数:${data.textList.length}`);
console.log(`界面文本内容:${data.textList.join(' | ')}`);
console.log('------------------------------------------------');
}
// 关闭所有启动的APP
closeAllApps();
console.log('实战案例执行完成,所有资源已释放');
}
// 子线程执行函数:获取APP界面数据并存入全局数组
function getAppData(app) {
try {
// 启动APP
console.log(`开始启动${app.name},包名:${app.pkg}`);
var ret = launchApp(app.pkg, `txt*:${app.name}`, {maxStep: 50, afterWait: 2000});
if (1 == ret) {
console.log(`${app.name}启动成功,开始获取界面数据`);
// 获取当前窗口文本数据(冰狐内置API,需开启无障碍)
var textList = getWindowTextList({afterWait: 1000});
// 加锁保护全局结果数组的修改
dataLock.lock();
console.log(`${app.name}:获取锁,开始存入数据`);
// 将数据存入全局数组
resultList.push({
appName: app.name,
pkg: app.pkg,
textList: textList,
time: new Date().getTime()
});
console.log(`${app.name}:数据存入完成,释放锁`);
// 释放锁
dataLock.unlock();
// 退到桌面,不影响其他APP操作
home({afterWait: 1000});
} else {
console.log(`${app.name}启动失败,包名:${app.pkg}`);
}
} catch (e) {
console.log(`${app.name}数据获取异常:${e}`);
// 异常时确保锁被释放,避免死锁
dataLock.unlock();
}
}
// 辅助函数:关闭所有启动的APP
function closeAllApps() {
console.log('开始关闭所有启动的APP');
for (var key in appMap) {
var app = appMap[key];
closeApp(app.pkg, {afterWait: 500});
console.log(`已关闭${app.name},包名:${app.pkg}`);
}
}
3.3 案例核心亮点
- 线程与锁的深度融合 :通过线程锁保证全局结果数组
resultList的插入操作原子性,避免多线程同时插入导致的数据顺序混乱、数据丢失; - 异常处理与锁释放 :在子线程执行函数中增加
try/catch异常捕获,确保即使执行过程中出现异常,锁也能被释放,彻底避免死锁问题; - 资源统一管理:通过数组管理线程实例,通过对象映射管理 APP 信息,实现线程和业务资源的统一创建、启动和释放,符合工程化开发规范;
- 贴合自动化场景 :结合冰狐平台的
launchApp、getWindowTextList、closeApp等自动化核心 API,实现多 APP 的并发操作,直接适用于实际自动化脚本开发。
四、多线程与锁开发的核心注意事项
冰狐平台的多线程与锁使用虽然简洁,但在自动化脚本开发中,由于涉及设备硬件、APP 界面等实际资源的操作,需遵循一系列核心注意事项,否则会导致脚本执行异常、死锁、设备卡死等问题,以下是开发中必须牢记的关键要点:
4.1 多线程开发注意事项
- 避免过多线程创建:设备的 CPU 核心数有限,过多的线程会导致线程切换开销过大,反而降低执行效率,一般建议同时运行的线程数不超过 5 个;
- UI 脚本的线程限制 :冰狐平台中 UI 脚本(如悬浮按钮回调
cbFloatButton)禁止直接执行耗时任务,也不能直接创建线程,耗时任务需通过runTask函数开启新线程执行,避免阻塞 UI 线程导致界面卡死; - 线程间通信仅用全局变量 :冰狐脚本不支持匿名函数和闭包,线程间的通信只能通过
__global声明的全局变量实现,且全局变量的修改需通过线程锁保护; - 合理使用 join () 方法 :
join()方法会阻塞主线程,若在主线程中对多个线程依次调用join(),会导致多线程变为串行执行,失去并发优势,建议通过数组批量管理线程,最后统一等待所有线程完成。
4.2 线程锁开发注意事项
- 锁实例必须全局化 :这是冰狐线程锁使用的最核心规则 ,锁实例若未通过
__global声明,各线程会创建独立的锁实例,无法实现同步,导致锁失效; - 锁的范围最小化:仅对操作共享资源的代码块加锁,避免将整个任务函数包裹在锁中,否则会导致多线程变为串行执行,丧失并发效率;
- 确保锁的释放 :获取锁后,无论执行成功还是失败,都必须调用
unlock()释放锁,可通过try/catch异常捕获实现,否则会导致其他线程永久阻塞,出现死锁; - 避免嵌套锁 :冰狐平台不支持锁的重入,同一线程多次调用
lock()会导致自身阻塞,开发中应避免嵌套使用锁。
4.3 自动化场景特有注意事项
- 无障碍权限开启 :冰狐平台的自动化 API(如
launchApp、scroll、getWindowTextList)需要开启设备的无障碍权限,否则多线程操作 APP 时会执行失败; - APP 操作的延时设置 :多线程操作 APP 时,需通过
afterWait参数设置合理的延时,确保 APP 界面加载完成后再执行后续操作,避免因界面未加载完成导致的操作失效; - 共享硬件资源的保护:设备的摄像头、麦克风、存储等硬件资源为共享资源,多线程操作时需通过线程锁保护,避免同时访问导致的硬件异常;
- 脚本的动态部署与生效:冰狐平台支持脚本的在线编辑、动态部署并立即生效,修改多线程和锁相关代码后,需重启脚本执行,避免旧代码与新代码冲突。
五、总结
通过Thread对象可快速实现线程的创建、启动和管理,通过Lock对象可轻松解决多线程并发访问共享资源的冲突问题。在实际开发中,只需掌握线程创建的基础语法、锁的全局化声明、共享资源的原子性保护三个核心点,即可实现高效的并发自动化脚本。同时,开发中需结合自动化场景特性,遵循线程和锁的使用规则,合理控制线程数量、最小化锁的作用域、确保锁的释放,避免死锁、设备卡死等问题。本文的所有 Demo 均基于冰狐原生 API 编写,可直接在平台中运行,开发者可根据实际需求进行修改和扩展,比如将多线程与锁应用于多设备调度、批量数据采集、多 APP 同时监控等复杂自动化场景,大幅提升脚本的执行效率和稳定性。