写自动化脚本时如何使用多线程和锁?

在自动化脚本开发中多线程并发编程能够大幅提升脚本的执行效率,尤其适用于需要同时处理多个任务的自动化场景,比如多应用同时操作、批量数据处理等。而线程锁则是解决多线程并发访问共享资源时数据混乱、执行冲突的核心手段。本文将从多线程基础、线程锁使用、实战案例及开发注意事项四个方面,全面讲解冰狐平台中多线程与锁的落地实现。

一、多线程开发基础

冰狐智能辅助平台的 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():释放锁,调用后锁被释放,其他阻塞的线程可获取锁执行。

核心使用规则

  1. 锁实例必须通过__global声明为全局变量,确保所有线程都能访问同一个锁实例,否则各线程持有独立的锁,无法实现同步。
  2. 获取锁后必须释放锁,即使执行过程中出现异常,也需保证unlock()被调用(冰狐脚本中可通过合理的逻辑判断实现)。
  3. 锁的作用域应尽可能小,仅包裹操作共享资源的代码块,避免因锁的范围过大导致并发效率降低。

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 实战案例需求

  1. 主线程启动后,依次启动 3 个子线程,分别处理x信、支付x、抖x的界面数据获取;
  2. 每个子线程启动对应 APP,获取界面文本数据,将数据存入全局共享数组;
  3. 使用线程锁保证全局数组的修改操作有序,避免数据插入混乱;
  4. 主线程等待所有子线程执行完成后,汇总并打印所有 APP 的界面数据;
  5. 所有操作完成后,关闭所有启动的 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 案例核心亮点

  1. 线程与锁的深度融合 :通过线程锁保证全局结果数组resultList的插入操作原子性,避免多线程同时插入导致的数据顺序混乱、数据丢失;
  2. 异常处理与锁释放 :在子线程执行函数中增加try/catch异常捕获,确保即使执行过程中出现异常,锁也能被释放,彻底避免死锁问题;
  3. 资源统一管理:通过数组管理线程实例,通过对象映射管理 APP 信息,实现线程和业务资源的统一创建、启动和释放,符合工程化开发规范;
  4. 贴合自动化场景 :结合冰狐平台的launchAppgetWindowTextListcloseApp等自动化核心 API,实现多 APP 的并发操作,直接适用于实际自动化脚本开发。

四、多线程与锁开发的核心注意事项

冰狐平台的多线程与锁使用虽然简洁,但在自动化脚本开发中,由于涉及设备硬件、APP 界面等实际资源的操作,需遵循一系列核心注意事项,否则会导致脚本执行异常、死锁、设备卡死等问题,以下是开发中必须牢记的关键要点:

4.1 多线程开发注意事项

  1. 避免过多线程创建:设备的 CPU 核心数有限,过多的线程会导致线程切换开销过大,反而降低执行效率,一般建议同时运行的线程数不超过 5 个;
  2. UI 脚本的线程限制 :冰狐平台中 UI 脚本(如悬浮按钮回调cbFloatButton)禁止直接执行耗时任务,也不能直接创建线程,耗时任务需通过runTask函数开启新线程执行,避免阻塞 UI 线程导致界面卡死;
  3. 线程间通信仅用全局变量 :冰狐脚本不支持匿名函数和闭包,线程间的通信只能通过__global声明的全局变量实现,且全局变量的修改需通过线程锁保护;
  4. 合理使用 join () 方法join()方法会阻塞主线程,若在主线程中对多个线程依次调用join(),会导致多线程变为串行执行,失去并发优势,建议通过数组批量管理线程,最后统一等待所有线程完成。

4.2 线程锁开发注意事项

  1. 锁实例必须全局化 :这是冰狐线程锁使用的最核心规则 ,锁实例若未通过__global声明,各线程会创建独立的锁实例,无法实现同步,导致锁失效;
  2. 锁的范围最小化:仅对操作共享资源的代码块加锁,避免将整个任务函数包裹在锁中,否则会导致多线程变为串行执行,丧失并发效率;
  3. 确保锁的释放 :获取锁后,无论执行成功还是失败,都必须调用unlock()释放锁,可通过try/catch异常捕获实现,否则会导致其他线程永久阻塞,出现死锁;
  4. 避免嵌套锁 :冰狐平台不支持锁的重入,同一线程多次调用lock()会导致自身阻塞,开发中应避免嵌套使用锁。

4.3 自动化场景特有注意事项

  1. 无障碍权限开启 :冰狐平台的自动化 API(如launchAppscrollgetWindowTextList)需要开启设备的无障碍权限,否则多线程操作 APP 时会执行失败;
  2. APP 操作的延时设置 :多线程操作 APP 时,需通过afterWait参数设置合理的延时,确保 APP 界面加载完成后再执行后续操作,避免因界面未加载完成导致的操作失效;
  3. 共享硬件资源的保护:设备的摄像头、麦克风、存储等硬件资源为共享资源,多线程操作时需通过线程锁保护,避免同时访问导致的硬件异常;
  4. 脚本的动态部署与生效:冰狐平台支持脚本的在线编辑、动态部署并立即生效,修改多线程和锁相关代码后,需重启脚本执行,避免旧代码与新代码冲突。

五、总结

通过Thread对象可快速实现线程的创建、启动和管理,通过Lock对象可轻松解决多线程并发访问共享资源的冲突问题。在实际开发中,只需掌握线程创建的基础语法、锁的全局化声明、共享资源的原子性保护三个核心点,即可实现高效的并发自动化脚本。同时,开发中需结合自动化场景特性,遵循线程和锁的使用规则,合理控制线程数量、最小化锁的作用域、确保锁的释放,避免死锁、设备卡死等问题。本文的所有 Demo 均基于冰狐原生 API 编写,可直接在平台中运行,开发者可根据实际需求进行修改和扩展,比如将多线程与锁应用于多设备调度、批量数据采集、多 APP 同时监控等复杂自动化场景,大幅提升脚本的执行效率和稳定性。

相关推荐
ai_coder_ai2 天前
如何使用shizuku来实现自动化脚本?
autojs·自动化脚本·冰狐智能辅助·easyclick
Mr -老鬼3 天前
EasyClick 大文件分割合并
自动化·autojs·easyclick·易点云测
ai_coder_ai4 天前
如何使用adb来实现自动化脚本
adb·autojs·自动化脚本·冰狐智能辅助·easyclick
PyHaVolask6 天前
SQL 注入实战:布尔盲注原理与自动化脚本解析
sql注入·二分查找算法·自动化脚本·布尔盲注·sqli-labs靶场
Mr -老鬼6 天前
EasyClick 热更新坑点处理方案
自动化·ec·easyclick·易点云测
ai_coder_ai8 天前
如何使用ocr来实现自动化脚本?
ocr·autojs·自动化脚本·冰狐智能辅助·easyclick
ai_coder_ai9 天前
在自动化脚本中如何在自定义ui中使用webview来无限扩展ui?
ui·autojs·自动化脚本·冰狐智能辅助·easyclick
ai_coder_ai13 天前
在冰狐中如何正确处理文件夹和文件?
autojs·自动化脚本·冰狐智能辅助·easyclick
ai_coder_ai14 天前
如何使用图色操作实现自动化脚本?
autojs·自动化脚本·冰狐智能辅助·easyclick