使用持久设备工作线程跨任务复用 uiautomator2 会话 #110

Closed
opened 2026-08-10 17:22:25 +08:00 by ila · 6 comments
Owner

基本信息

  • 类型:重构
  • 父级大工单:#1
  • 所属 MVP / 版本:MVP 后续 / 任务执行性能优化(不纳入 #2、#97)
  • 阶段:跨任务复用 Android 设备会话
  • 前置依赖:#45、#109
  • 原实施门槛:#109 的热启动数据证明 uiautomator2.connect() + 可避免的重复设备/应用检查中位耗时不少于 2 秒,或占领取后到商品页就绪总耗时的 20% 以上
  • 当前决策:#109 未达到原门槛;用户于 2026-08-10 明确要求继续实施 #110,本次按已确认范围进行受控重构

决策变更(2026-08-10)

#109 的真机数据判定为 No-Go 后,用户再次明确要求“做 #110”。因此原性能门槛不再作为是否开始实施的阻塞条件,但仍保留为结果评价和回退依据。本次变更不放宽线程归属、每任务页面重校验、不可逆阶段只核对、真实下单默认关闭等安全约束;若收益不足或稳定性下降,验收时明确建议回退。

本工单不改变可见 UI 布局和操作路径,因此无需制作 HTML 交互原型;后台任务继续沿用现有状态提示和取消入口。

要解决什么

当前持续自动获取流程每轮创建新的任务 Worker 和 QThread,任务执行服务随后重新调用 uiautomator2.connect(serial)。同一 Android 设备连续执行多个任务时,重复建立会话、检查应用和初始化自动化环境会增加领取任务到打开商品页的等待时间。

直接把 uiautomator2.Device 放入全局变量并交给不同线程复用不安全:Device 及其相关调用必须由固定线程创建、使用和销毁。同一设备也不得被采集、采购、手动重新执行和订单核对并发控制。

做什么 / 不做什么

做

  • 为当前选中 Android 设备建立一个长生命周期的设备工作对象,使用 QObject + moveToThread 固定到一个 QThread。
  • Device 会话只能在该工作线程内创建、访问、健康检查、重连和销毁。
  • 所有需要控制设备的工作通过串行命令队列进入同一个执行器,包括:
    • 自动领取后的采集或采购;
    • 手动重新执行;
    • 不可逆阶段后的订单核对。
  • 跨任务只复用设备连接;每个任务必须重新读取当前应用、当前页面、控件树、商品信息、价格、规格和安全状态。
  • 每个任务开始前做轻量健康检查;不可逆动作前发现会话失效时,丢弃旧会话并最多重连一次,然后从新页面状态重新校验。
  • task_runs.irreversible_action_at 有值后,任何异常都只允许进入订单核对,绝不重新下单。
  • 空闲会话设置默认 90 秒 TTL;停止自动获取、切换设备、设备断连、配置变化、窗口关闭或 TTL 到期时安全释放。
  • 线程退出使用协作式停止和 quit()/wait(),不得使用 terminate()。
  • 保留可回退到“每任务新建会话”的实现边界。

不做

  • 不使用跨线程共享的全局 uiautomator2.Device。
  • 不允许同一设备并发执行多个任务或多个设备命令。
  • 不跨任务缓存或复用 XML、Selector、坐标、价格、颜色尺码和页面判断结果。
  • 不修改 Admin API、数据库结构、任务状态和 pdd_data。
  • 不降低商品页、验证码、风控、登录或采购安全门禁校验。
  • 不打开真实下单默认开关,不自动付款。
  • 不把 Outbox 纯网络重试放到设备线程;不需要 Android 设备的工作继续走原有路径。

已确认的实现方案

  1. 新增单设备执行器(文件名由 Codex 按现有分层确定),其职责仅为管理固定 QThread、串行设备命令和会话生命周期。
  2. 设备工作对象在自己的线程中懒创建 uiautomator2.Device,同一 serial 且在 TTL 内健康时复用;serial 变化或健康检查失败时关闭旧会话并重建。
  3. 任务调度层把“采集、采购、核对”作为带 task_id 和操作类型的命令提交;同一时间最多运行一个命���,后续命令排队或按现有交互明确拒绝重复提交。
  4. 自动获取的定时调度仍在 Qt 主线程;只有设备操作进入持久工作线程,不在 Worker 内使用无限循环或阻塞等待下一轮。
  5. 每条命令开始时建立全新的任务上下文,重新获取应用与页面状态;会话复用不得改变页面安全判断和取消点。
  6. 重连策略按不可逆边界区分:
    • 不可逆动作前:健康检查失败可丢弃会话、最多重连一次、重新校验后继续;
    • 不可逆动作后:不重放下单步骤,只允许核对既有订单。
  7. 停止、切换设备和关闭窗���时先停止接收新命令,等待当前命令到安全点结束,再释放 Device 和线程。
  8. 使用 #109 的阶段计时做改造前后对照;若收益不达验收门槛或稳定性下降,可回退本工单而保留 #109 的诊断与低风险优化。

预计修改文件(Codex 实施前须按实际代码再次确认):

  • client/src/pdd_ui_event.py
  • client/src/task_dispatcher.py
  • client/src/pdd_device_service.py
  • client/src/pdd_collect_service.py
  • client/src/pdd_u2_purchase_adapter.py
  • 新增单设备执行器及相应测试文件
  • 必要的 docs/client/02-architecture.md、线程/采购安全相关文档

验收标准

  • #109 已完成并记录基线数据;虽未达到原性能门槛,用户已明确授权继续实施,原因已记录在“决策变更”。
  • 同一设备在 90 秒 TTL 内连续执行任务时,uiautomator2.connect() 只调用一次。
  • Device 的创建、全部调用和销毁发生在同一个 QThread,可由测试记录并断言线程 ID。
  • 同一设备任意时刻最多执行一个设备命令,自动获取、手动重试和订单核对不会竞争控制权。
  • 每个任务都重新读取并验证应用、页面、控件树、商品和采购安全状态,未复用上个任务的页面数据。
  • 停止自动获取、切换设备、断连、配置变化、窗口关闭和 TTL 到期都会安全释放会话,且不会重复销毁或访问已销毁对象。
  • 不可逆动作前会话失效最多重连一次并重新校验;不可逆动作后只核对订单,绝不重新下单。
  • 与 #109 基线相比,热启动“本地保存完成到发出 open_url()”中位耗时至少降低 30% 或 1.5 秒。
  • 若性能门槛未达到或真机稳定性下降,能够回退本工单且不影响任务、Outbox 和 Admin 数据。
  • 相关专项测试和 Client 全量测试通过。
  • 真实下单开关仍默认关闭,验证仅使用采集或采购 dry_run,不执行付款。

验证方式

从 client/ 目录执行:

C:/Python310/python.exe -m unittest discover -s test -p "test_*.py"

专项测试至少覆盖:

  1. 同一 serial 连续两条命令只连接一次,且全部设备调用线程 ID 一致。
  2. 两条同时提交的设备命令严格串行。
  3. serial 变化、TTL 到期、停止、关闭窗口和设备断连会安全重建或释放。
  4. 不可逆前断连最多重连一次;不可逆后断连只进入订单核对。
  5. 每个任务的页面状态和 XML 都重新读取。
  6. 使用同一设备、同一网络和与 #109 一致的样本执行至少 5 次热启动,对比中位数与 P95。

风险和回退

  • 风险:线程归属错误导致竞态或崩溃;停止和切换设备时迟到信号访问已销毁对象;会话失效后误重放不可逆步骤;长会话掩盖页面状态变化。
  • 控制:Device 全生命周期固定在线程内;设备命令单队列串行;每任务重新校验页面;以 irreversible_action_at 为硬边界;关闭过程停止接单并等待安全点。
  • 回退:保留原每任务创建会话的工厂边界,可整体回退本工单提交;数据库、Admin 接口和任务数据无需回滚。
## 基本信息 - 类型:重构 - 父级大工单:#1 - 所属 MVP / 版本:MVP 后续 / 任务执行性能优化(不纳入 #2、#97) - 阶段:跨任务复用 Android 设备会话 - 前置依赖:#45、#109 - 原实施门槛:#109 的热启动数据证明 `uiautomator2.connect() + 可避免的重复设备/应用检查`中位耗时不少于 2 秒,或占领取后到商品页就绪总耗时的 20% 以上 - 当前决策:#109 未达到原门槛;用户于 2026-08-10 明确要求继续实施 #110,本次按已确认范围进行受控重构 ## 决策变更(2026-08-10) #109 的真机数据判定为 No-Go 后,用户再次明确要求“做 #110”。因此原性能门槛不再作为是否开始实施的阻塞条件,但仍保留为结果评价和回退依据。本次变更不放宽线程归属、每任务页面重校验、不可逆阶段只核对、真实下单默认关闭等安全约束;若收益不足或稳定性下降,验收时明确建议回退。 本工单不改变可见 UI 布局和操作路径,因此无需制作 HTML 交互原型;后台任务继续沿用现有状态提示和取消入口。 ## 要解决什么 当前持续自动获取流程每轮创建新的任务 Worker 和 QThread,任务执行服务随后重新调用 `uiautomator2.connect(serial)`。同一 Android 设备连续执行多个任务时,重复建立会话、检查应用和初始化自动化环境会增加领取任务到打开商品页的等待时间。 直接把 `uiautomator2.Device` 放入全局变量并交给不同线程复用不安全:Device 及其相关调用必须由固定线程创建、使用和销毁。同一设备也不得被采集、采购、手动重新执行和订单核对并发控制。 ## 做什么 / 不做什么 ### 做 - 为当前选中 Android 设备建立一个长生命周期的设备工作对象,使用 `QObject + moveToThread` 固定到一个 QThread。 - Device 会话只能在该工作线程内创建、访问、健康检查、重连和销毁。 - 所有需要控制设备的工作通过串行命令队列进入同一个执行器,包括: - 自动领取后的采集或采购; - 手动重新执行; - 不可逆阶段后的订单核对。 - 跨任务只复用设备连接;每个任务必须重新读取当前应用、当前页面、控件树、商品信息、价格、规格和安全状态。 - 每个任务开始前做轻量健康检查;不可逆动作前发现会话失效时,丢弃旧会话并最多重连一次,然后从新页面状态重新校验。 - `task_runs.irreversible_action_at` 有值后,任何异常都只允许进入订单核对,绝不重新下单。 - 空闲会话设置默认 90 秒 TTL;停止自动获取、切换设备、设备断连、配置变化、窗口关闭或 TTL 到期时安全释放。 - 线程退出使用协作式停止和 `quit()/wait()`,不得使用 `terminate()`。 - 保留可回退到“每任务新建会话”的实现边界。 ### 不做 - 不使用跨线程共享的全局 `uiautomator2.Device`。 - 不允许同一设备并发执行多个任务或多个设备命令。 - 不跨任务缓存或复用 XML、Selector、坐标、价格、颜色尺码和页面判断结果。 - 不修改 Admin API、数据库结构、任务状态和 `pdd_data`。 - 不降低商品页、验证码、风控、登录或采购安全门禁校验。 - 不打开真实下单默认开关,不自动付款。 - 不把 Outbox 纯网络重试放到设备线程;不需要 Android 设备的工作继续走原有路径。 ## 已确认的实现方案 1. 新增单设备执行器(文件名由 Codex 按现有分层确定),其职责仅为管理固定 QThread、串行设备命令和会话生命周期。 2. 设备工作对象在自己的线程中懒创建 `uiautomator2.Device`,同一 serial 且在 TTL 内健康时复用;serial 变化或健康检查失败时关闭旧会话并重建。 3. 任务调度层把“采集、采购、核对”作为带 task_id 和操作类型的命令提交;同一时间最多运行一个命���,后续命令排队或按现有交互明确拒绝重复提交。 4. 自动获取的定时调度仍在 Qt 主线程;只有设备操作进入持久工作线程,不在 Worker 内使用无限循环或阻塞等待下一轮。 5. 每条命令开始时建立全新的任务上下文,重新获取应用与页面状态;会话复用不得改变页面安全判断和取消点。 6. 重连策略按不可逆边界区分: - 不可逆动作前:健康检查失败可丢弃会话、最多重连一次、重新校验后继续; - 不可逆动作后:不重放下单步骤,只允许核对既有订单。 7. 停止、切换设备和关闭窗���时先停止接收新命令,等待当前命令到安全点结束,再释放 Device 和线程。 8. 使用 #109 的阶段计时做改造前后对照;若收益不达验收门槛或稳定性下降,可回退本工单而保留 #109 的诊断与低风险优化。 预计修改文件(Codex 实施前须按实际代码再次确认): - `client/src/pdd_ui_event.py` - `client/src/task_dispatcher.py` - `client/src/pdd_device_service.py` - `client/src/pdd_collect_service.py` - `client/src/pdd_u2_purchase_adapter.py` - 新增单设备执行器及相应测试文件 - 必要的 `docs/client/02-architecture.md`、线程/采购安全相关文档 ## 验收标准 - [x] #109 已完成并记录基线数据;虽未达到原性能门槛,用户已明确授权继续实施,原因已记录在“决策变更”。 - [ ] 同一设备在 90 秒 TTL 内连续执行任务时,`uiautomator2.connect()` 只调用一次。 - [ ] Device 的创建、全部调用和销毁发生在同一个 QThread,可由测试记录并断言线程 ID。 - [ ] 同一设备任意时刻最多执行一个设备命令,自动获取、手动重试和订单核对不会竞争控制权。 - [ ] 每个任务都重新读取并验证应用、页面、控件树、商品和采购安全状态,未复用上个任务的页面数据。 - [ ] 停止自动获取、切换设备、断连、配置变化、窗口关闭和 TTL 到期都会安全释放会话,且不会重复销毁或访问已销毁对象。 - [ ] 不可逆动作前会话失效最多重连一次并重新校验;不可逆动作后只核对订单,绝不重新下单。 - [ ] 与 #109 基线相比,热启动“本地保存完成到发出 `open_url()`”中位耗时至少降低 30% 或 1.5 秒。 - [ ] 若性能门槛未达到或真机稳定性下降,能够回退本工单且不影响任务、Outbox 和 Admin 数据。 - [ ] 相关专项测试和 Client 全量测试通过。 - [ ] 真实下单开关仍默认关闭,验证仅使用采集或采购 dry_run,不执行付款。 ## 验证方式 从 `client/` 目录执行: ```powershell C:/Python310/python.exe -m unittest discover -s test -p "test_*.py" ``` 专项测试至少覆盖: 1. 同一 serial 连续两条命令只连接一次,且全部设备调用线程 ID 一致。 2. 两条同时提交的设备命令严格串行。 3. serial 变化、TTL 到期、停止、关闭窗口和设备断连会安全重建或释放。 4. 不可逆前断连最多重连一次;不可逆后断连只进入订单核对。 5. 每个任务的页面状态和 XML 都重新读取。 6. 使用同一设备、同一网络和与 #109 一致的样本执行至少 5 次热启动,对比中位数与 P95。 ## 风险和回退 - 风险:线程归属错误导致竞态或崩溃;停止和切换设备时迟到信号访问已销毁对象;会话失效后误重放不可逆步骤;长会话掩盖页面状态变化。 - 控制:Device 全生命周期固定在线程内;设备命令单队列串行;每任务重新校验页面;以 `irreversible_action_at` 为硬边界;关闭过程停止接单并等待安全点。 - 回退:保留原每任务创建会话的工厂边界,可整体回退本工单提交;数据库、Admin 接口和任务数据无需回滚。
Author
Owner

状态:待实施(有前置门槛)。交给 Codex 实施,但必须先完成 #109,并确认其真机热启动数据达到正文 Go/No-Go 门槛。本轮未修改代码、未提交 Git。

状态:**待实施(有前置门槛)**。交给 Codex 实施,但必须先完成 #109,并确认其真机热启动数据达到正文 Go/No-Go 门槛。本轮未修改代码、未提交 Git。
Author
Owner

#109 真机数据已完成,本工单当前判定 No-Go,保持不实施。

热启动 5 次中位数:uiautomator2.connect() 203 ms,领取到商品页端到端 14328 ms,占约 1.4%,未达到“至少 2 秒或 20%”的前置门槛。#109 已消除相邻第二次 app_current();每个任务仍必须重新做一次应用状态检查,持久 Device 会话不能安全省掉该检查。

当前最大耗时是首次 app_current() 中位数 10578 ms,且这台 OPPO/ColorOS 即使 PDD 位于屏幕前台仍可能报告系统设置页。若继续优化,应另建边界清晰的应用状态检测诊断任务;不要直接启动本工单的线程架构重构。详细冷/热启动表见 #109 评论。

#109 真机数据已完成,本工单当前判定 **No-Go,保持不实施**。 热启动 5 次中位数:`uiautomator2.connect()` 203 ms,领取到商品页端到端 14328 ms,占约 1.4%,未达到“至少 2 秒或 20%”的前置门槛。#109 已消除相邻第二次 `app_current()`;每个任务仍必须重新做一次应用状态检查,持久 Device 会话不能安全省掉该检查。 当前最大耗时是首次 `app_current()` 中位数 10578 ms,且这台 OPPO/ColorOS 即使 PDD 位于屏幕前台仍可能报告系统设置页。若继续优化,应另建边界清晰的应用状态检测诊断任务;不要直接启动本工单的线程架构重构。详细冷/热启动表见 #109 评论。
Author
Owner

状态更新为 进行中。用户在 #109 No-Go 结论后明确要求继续实施 #110,已把该决策和不放宽的安全约束写入正文。实施将保持 PyQt5、真实下单默认关闭,并只用单元测试及采集/采购 dry_run 验证。

状态更新为 **进行中**。用户在 #109 No-Go 结论后明确要求继续实施 #110,已把该决策和不放宽的安全约束写入正文。实施将保持 PyQt5、真实下单默认关闭,并只用单元测试及采集/采购 dry_run 验证。
Author
Owner

实施进度:固定 QThread、90 秒 Device 连接复用、健康检查失败后最多重连一次、停止/设置变更/TTL/窗口关闭释放,以及采集/采购/核单官方 Adapter 的线程内服务绑定已完成。重新上报仍使用独立网络 Worker;数据库、Admin 接口、任务状态和 pdd_data 未改,真实下单默认关闭。

验证结果:

  • 语法检查通过;
  • Client 全量测试:316 项通过;
  • 离屏 MainWindow 冒烟通过;
  • 真机只读验证(192.168.0.173:5555,不点击、不领取任务):连续 5 个任务会话仅调用 1 次 uiautomator2.connect(),所有调用线程 ID 均为 22788。

性能差异如实记录:5 次会话建立/健康检查分别为 11730.6、10756.5、10589.1、10738.6、10826.3 ms,当前包名仍被设备报告为 com.android.settings。因此连接复用成立,但正文“降低 30% 或 1.5 秒”的性能项未达到/无法由本改造达到;主要耗时仍是每任务不可省略的 app_current()。该项保留为待验收差异,不降低页面和采购安全校验。

实施进度:固定 QThread、90 秒 Device 连接复用、健康检查失败后最多重连一次、停止/设置变更/TTL/窗口关闭释放,以及采集/采购/核单官方 Adapter 的线程内服务绑定已完成。重新上报仍使用独立网络 Worker;数据库、Admin 接口、任务状态和 pdd_data 未改,真实下单默认关闭。 验证结果: - 语法检查通过; - Client 全量测试:316 项通过; - 离屏 MainWindow 冒烟通过; - 真机只读验证(192.168.0.173:5555,不点击、不领取任务):连续 5 个任务会话仅调用 1 次 `uiautomator2.connect()`,所有调用线程 ID 均为 22788。 性能差异如实记录:5 次会话建立/健康检查分别为 11730.6、10756.5、10589.1、10738.6、10826.3 ms,当前包名仍被设备报告为 `com.android.settings`。因此连接复用成立,但正文“降低 30% 或 1.5 秒”的性能项未达到/无法由本改造达到;主要耗时仍是每任务不可省略的 `app_current()`。该项保留为待验收差异,不降低页面和采购安全校验。
Author
Owner

状态:实现完成,待用户验收。

  • 实现提交:70cedc3(refactor: 复用持久设备工作线程 (#110))
  • 归档提交:40cd8af(docs: 归档任务 #110)
  • 本地归档:docs/task/110-持久设备工作线程复用uiautomator2会话.md
  • Client 全量测试:316 项通过;语法、离屏冒烟、差异检查通过。
  • 真机只读验证:5 个会话共用 1 次底层连接、线程 ID 一致;未点击、未领取任务、未下单、未付款。

待验收差异:连接复用和线程安全已实现,但原“中位耗时降低 30% 或 1.5 秒”未达到,原因是每任务不可省略的 app_current() 仍约 10.6 秒且设备报告 com.android.settings。本次没有用删除安全校验来换速度。工单暂不关闭,父工单清单暂不勾选,等待用户验收。

状态:**实现完成,待用户验收**。 - 实现提交:`70cedc3`(`refactor: 复用持久设备工作线程 (#110)`) - 归档提交:`40cd8af`(`docs: 归档任务 #110`) - 本地归档:`docs/task/110-持久设备工作线程复用uiautomator2会话.md` - Client 全量测试:316 项通过;语法、离屏冒烟、差异检查通过。 - 真机只读验证:5 个会话共用 1 次底层连接、线程 ID 一致;未点击、未领取任务、未下单、未付款。 待验收差异:连接复用和线程安全已实现,但原“中位耗时降低 30% 或 1.5 秒”未达到,原因是每任务不可省略的 `app_current()` 仍约 10.6 秒且设备报告 `com.android.settings`。本次没有用删除安全校验来换速度。工单暂不关闭,父工单清单暂不勾选,等待用户验收。
Author
Owner

用户已于 2026-08-11 明确验收通过。

  • 本地归档状态已更新为“已完成,用户验收通过”
  • 验收记录提交:eca9289
  • 本工单按项目流程关闭
用户已于 2026-08-11 明确验收通过。 - 本地归档状态已更新为“已完成,用户验收通过” - 验收记录提交:`eca9289` - 本工单按项目流程关闭
ila closed this issue 2026-08-11 10:58:28 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: chengma/cmautobuy#110