在自动化脚本中使用线程和线程锁

在移动端自动化脚本开发场景中,单线程执行模式存在明显短板:当脚本同时需要完成多类任务,例如主线程执行 APP 操作、实时监控弹窗、后台同步网络数据、循环采集页面信息时,串行执行会大幅拉长运行耗时,随机弹窗、网络延迟等突发情况还会直接中断主流程。本文从线程基础使用、多线程并发冲突成因、Lock 锁核心用法、实战案例、避坑规范五个维度完整讲解,适用于所有 Android 端自动化脚本开发人员。

一、线程基础:Thread 对象创建与基础操作

冰狐脚本采用自研简化版 ECMAScript 语法,原生提供Thread类用于创建独立子线程,主线程负责核心业务流程,子线程可独立执行循环监控、网络请求、日志写入、弹窗拦截等耗时、异步任务,线程之间相互隔离运行,单一线程崩溃不会终止整个脚本进程。

1.1 线程创建与启动标准语法

创建线程分为三步:实例化 Thread 对象、绑定执行函数、调用start()启动线程。基础模板如下:

function main() {
    // 1.实例化线程对象
    var monitorThread = new Thread();
    // 2.绑定目标函数并启动子线程
    monitorThread.start(popupMonitor);
    // 主线程正常执行业务逻辑
    runMainTask();
}
// 子线程执行函数:循环监控弹窗
function popupMonitor() {
    while(true) {
        var popElements = findView("txt:广告|txt:弹窗确认");
        if(popElements.length > 0) {
            click("txt:关闭");
        }
        sleep(800);
    }
}
function runMainTask() {
    // 主线程APP操作逻辑
    launchApp("com.example.app");
    click("id:home");
    sleep(1000);
}

该示例是自动化开发高频场景:子线程无限循环监控页面弹窗,主线程不受弹窗干扰持续执行业务流程,解决传统单线程弹窗中断脚本的痛点。

1.2 多线程并行与线程等待

当脚本存在多个独立并行任务(弹窗监控、数据上报、状态检测),可创建多个 Thread 对象同时启动;若主线程需要等待所有子线程执行完毕再结束,使用join()方法阻塞主线程。

function main() {
    var t1 = new Thread();
    var t2 = new Thread();
    t1.start(popupMonitor);
    t2.start(dataUpload);
    // 等待两个子线程执行完成后主线程才退出
    t1.join();
    t2.join();
    console.log("所有子线程执行完毕");
}
// 数据上报线程
function dataUpload() {
    while(true) {
        var res = httpGet("https://xxx/data");
        console.log("同步数据:" + res);
        sleep(3000);
    }
}

1.3 线程间数据通信:__global 全局变量

普通局部变量仅当前线程可见,多线程共享数据必须使用__global修饰符声明全局变量,全局变量可在主线程、所有子线程中读写,是线程通信的基础载体,也是并发冲突的根源。

// 全局共享变量,所有线程可访问
var __global taskCount = 0;
var __global runLock;

function main() {
    runLock = new Lock();
    var t1 = new Thread();
    var t2 = new Thread();
    t1.start(countAdd);
    t2.start(countAdd);
}
// 两个线程同时修改全局计数器,无锁会出现数据错乱
function countAdd() {
    for(var i=0; i<10; i++) {
        taskCount = taskCount + 1;
        console.log("当前计数:" + taskCount);
        sleep(100);
    }
}

上述代码中两个线程同时修改taskCount,会出现计数丢失、数值错乱,这就是典型资源竞争问题,必须依靠 Lock 锁实现同步控制。

二、多线程并发冲突:为什么需要 Lock 线程锁

2.1 并发冲突典型场景

在冰狐自动化脚本中,共享资源分为三类:全局变量、本地文件、页面控件操作,多线程同时访问会产生异常:

  1. 全局变量读写错乱:多个线程同时增减计数器、修改任务状态标记,数据计算结果不准确;
  2. 文件读写损坏:一线程写入本地日志文件,另一线程同步读取,出现文件内容截断、乱码;
  3. 控件操作冲突:主线程点击页面按钮,弹窗线程同步执行关闭操作,触发页面控件丢失、点击失效;
  4. 网络请求重复提交:双线程同时调用上报接口,产生重复数据、重复订单等业务异常。

冲突核心原理:线程调度由系统随机分配,读写共享资源的操作不具备原子性,一个线程读取数据后未完成写入,另一线程抢先修改,最终数据不一致。

2.2 Lock 锁核心作用

冰狐官方提供Lock锁对象,用于实现互斥访问:同一时刻仅允许一个线程进入临界区(读写共享资源的代码块),其他线程调用lock()方法时会阻塞等待,直到持有锁的线程执行unlock()释放资源,保证共享资源操作完整、原子执行,从根源消除并发冲突。

Lock 核心三个方法:

  1. new Lock():实例化锁对象,多线程共享同一锁实例;
  2. lock():加锁,抢占资源,未抢到则阻塞等待;
  3. unlock():解锁,释放锁资源,其他阻塞线程可竞争获取。

三、冰狐 Lock 锁完整使用规范与标准示例

3.1 官方标准锁模板(多线程计数防错乱)

参考官方文档原生示例,完整实现线程同步计数器,解决全局变量并发修改问题:

// 全局锁对象,多线程共享
var __global lock;
var __global taskCount = 0;

function main() {
    console.log("主线程启动");
    // 创建锁实例
    lock = new Lock();
    // 创建子线程
    var childThread = new Thread();
    childThread.start(childFunc);

    // 主线程操作共享变量,进入临界区前加锁
    lock.lock();
    console.log("主线程获取锁,开始修改计数");
    taskCount = taskCount + 5;
    sleep(5000); // 模拟耗时业务操作
    console.log("主线程释放锁,当前计数:" + taskCount);
    lock.unlock();

    childThread.join();
    console.log("主线程执行结束,最终计数:" + taskCount);
}
// 子线程函数
function childFunc() {
    console.log("子线程启动");
    // 子线程修改共享资源前加锁
    lock.lock();
    console.log("子线程获取锁,开始修改计数");
    taskCount = taskCount + 3;
    sleep(5000);
    console.log("子线程释放锁,当前计数:" + taskCount);
    lock.unlock();
    console.log("子线程执行结束");
}

运行逻辑:主线程先抢占锁,持有 5 秒期间子线程卡在lock()处等待;主线程执行unlock()释放锁后,子线程才能获取锁并修改全局变量,两次计数操作互不干扰,数值准确无误。

3.2 实战场景 1:多线程文件写入锁保护

自动化脚本常需要多线程同步写入本地日志文件,不加锁会导致日志内容交叉错乱,使用 Lock 包裹文件读写逻辑:

var __global fileLock;
var logPath = "/sdcard/autoLog.txt";

function main() {
    fileLock = new Lock();
    var t1 = new Thread();
    var t2 = new Thread();
    t1.start(writeLog1);
    t2.start(writeLog2);
}
// 线程1写入操作日志
function writeLog1() {
    for(var i=0; i<5; i++) {
        fileLock.lock();
        var file = new FileX(logPath);
        file.append("主线业务操作日志:第" + i + "条\n");
        fileLock.unlock();
        sleep(200);
    }
}
// 线程2写入弹窗监控日志
function writeLog2() {
    for(var i=0; i<5; i++) {
        fileLock.lock();
        var file = new FileX(logPath);
        file.append("弹窗监控日志:第" + i + "条\n");
        fileLock.unlock();
        sleep(200);
    }
}

3.3 实战场景 2:控件操作互斥锁,避免点击冲突

当主线程操作页面控件、子线程同步关闭弹窗时,双线程同时操作页面控件会出现点击失效、控件找不到,使用锁统一管控页面操作权限:

var __global viewLock;

function main() {
    viewLock = new Lock();
    var popThread = new Thread();
    popThread.start(popupMonitor);
    // 主线程循环操作页面
    for(var i=0; i<10; i++) {
        viewLock.lock();
        click("id:goods_item_" + i);
        sleep(500);
        viewLock.unlock();
    }
}
// 弹窗监控线程
function popupMonitor() {
    while(true) {
        var pop = findView("txt:关闭弹窗");
        if(pop.length > 0) {
            viewLock.lock();
            click(pop);
            viewLock.unlock();
        }
        sleep(500);
    }
}

四、线程与锁配套开发前置环境保障

冰狐多线程长期稳定运行依赖设备系统配置,若后台线程被系统清理、权限受限,会出现线程自动终止、锁失效等问题,需提前完成文档规定的设备配置:

  1. 系统版本要求:Android7.0 及以上(SDK≥24),低版本系统后台线程调度存在兼容性缺陷;
  2. 全权限开放:冰狐客户端悬浮窗、后台运行、存储、无障碍权限全部允许;
  3. 锁定后台任务:小米 / 红米进入手机管家 - 优化加速 - 设置,锁定冰狐 APP;华为、OPPO、vivo 自行搜索品牌专属锁定方案,防止内存回收杀死线程;
  4. 关闭省电限制:电池设置中关闭省电模式、后台限制,开启冰狐后台高耗电权限,避免系统休眠冻结子线程;
  5. 开启自启动:允许冰狐自启动,重启设备后线程可自动恢复;
  6. 关闭系统与应用自动更新:APP、系统升级会改变页面控件 ID,导致线程控件操作逻辑失效。

五、线程与锁开发避坑指南

5.1 锁使用致命错误

  1. 忘记调用 unlock ():线程获取锁后未释放,所有其他线程永久阻塞,脚本卡死;建议临界区代码精简,避免锁内写超长循环、网络请求;
  2. 多线程使用不同 Lock 实例:必须使用__global全局锁对象,每个线程独立 new Lock 无法实现互斥;
  3. 锁嵌套滥用:一个线程持有锁后再次调用 lock (),冰狐锁不支持可重入,会直接死锁;
  4. 锁内执行超长耗时操作:锁持有期间其他线程持续阻塞,大幅降低脚本并发效率,网络请求、复杂循环移出临界区。

5.2 线程开发常见问题

  1. 局部变量跨线程失效:仅__global变量可多线程共享,普通 var 变量仅当前线程生效;
  2. 无限循环子线程无法终止:增加全局状态标记var __global isRun = true;,主线程修改标记控制子线程循环退出;
  3. UI 脚本耗时逻辑放主线程:自定义 UI 界面脚本中,耗时任务必须通过runTask()开启新线程执行,防止界面卡顿、线程阻塞;
  4. 线程数量无限制创建:过多并发线程会占用设备内存,建议根据设备性能控制线程数≤5 个。

5.3 死锁规避方案

死锁场景:线程 A 持有锁 A 等待锁 B,线程 B 持有锁 B 等待锁 A,双方永久阻塞。规避规范:

  1. 全局统一锁获取顺序,所有线程按固定顺序抢占锁;
  2. 拆分锁粒度,不同共享资源使用独立锁对象,减少锁嵌套;
  3. 锁临界区代码尽量简短,快速释放锁资源。

六、综合完整实战案例:自动化任务监控脚本

整合 Thread 多线程、Lock 锁、全局变量、文件日志,实现一套完整自动化脚本:主线程执行商品浏览任务,子线程监控弹窗、后台同步任务计数、写入本地日志,全程无资源冲突。

// 全局共享资源
var __global runLock;
var __global logLock;
var __global taskNum = 0;
var __global scriptRunning = true;
var logFile = "/sdcard/taskLog.txt";

function main() {
    // 初始化锁对象
    runLock = new Lock();
    logLock = new Lock();
    // 启动弹窗监控线程
    var popThread = new Thread();
    popThread.start(popupCheck);
    // 启动日志写入线程
    var logThread = new Thread();
    logThread.start(recordLog);
    // 主线程业务流程
    launchApp("com.shop.app");
    while(scriptRunning) {
        runLock.lock();
        taskNum = taskNum + 1;
        console.log("已完成任务数:" + taskNum);
        click("id:product_list");
        sleep(1200);
        runLock.unlock();
        // 运行15秒后停止脚本
        if(taskNum >= 10) {
            scriptRunning = false;
        }
    }
    popThread.join();
    logThread.join();
    console.log("脚本全部执行完成");
}
// 弹窗监控子线程
function popupCheck() {
    while(scriptRunning) {
        var popClose = findView("txt:关闭|txt:跳过");
        if(popClose.length > 0) {
            runLock.lock();
            click(popClose);
            runLock.unlock();
        }
        sleep(600);
    }
}
// 日志记录子线程
function recordLog() {
    while(scriptRunning) {
        logLock.lock();
        var file = new FileX(logFile);
        file.append("当前任务计数:" + taskNum + "\n");
        logLock.unlock();
        sleep(1000);
    }
}

该案例同时使用两把独立锁,分别管控任务计数与文件写入,互不阻塞,兼顾并发效率与数据安全,是日常自动化项目通用架构。

结语

多线程与锁是自动化开发的核心能力,合理使用 Thread 可以并行处理监控、上报、日志等辅助任务,大幅提升脚本执行效率;Lock 互斥锁则解决多线程资源竞争的底层问题,保障数据、页面操作稳定可靠。开发时需兼顾语法规范与设备系统配置,规避死锁、锁遗漏、线程被系统回收等问题,根据业务场景拆分线程职责、精细化控制锁粒度,才能写出健壮、高效、长期稳定运行的移动端自动化脚本。对于复杂多任务场景,可基于本文架构拓展多线程任务调度、状态同步、异常捕获等逻辑,进一步完善自动化工程体系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值