# 110 使用持久设备工作线程跨任务复用 uiautomator2 会话 - 类型:重构 - 父级大工单:#1 - 所属 MVP / 版本:MVP 后续 / 任务执行性能优化 - 状态:已完成(待验收) - 日期:2026-08-10 - Gitea 工单: ## 背景与目标 Client 原来每轮自动获取都会新建 Worker 和 QThread,正式采集、采购和核单适配器 也会重新调用 `uiautomator2.connect()`。目标是让同一 Android 设备由固定工作线程 串行控制,并在 90 秒内只复用 Device 连接;页面、控件树、商品、价格、规格和采购 安全状态仍必须每个任务重新读取。 #109 测得连接本身只占端到端约 1.4%,原结论是 No-Go。用户随后明确要求继续实施 #110,因此工单已记录该决策变更;性能门槛保留为结果评价和回退依据,不再作为 开始实施的阻塞条件。 ## 最终方案 - `PDDTaskPageEvent` 在首次设备任务时懒创建一个固定 QThread 和长生命周期 `ClaimTaskWorker`。自动获取、手动重新采集、采购和不可逆阶段后的只读核单都通过 Qt 队列信号串行进入该 Worker;不在 Worker 内运行无限循环。 - `PersistentPddDeviceService` 只缓存同一 serial 的 Device 连接。每次 `connect()` 都重新调用 `app_current()` 做轻量健康检查,并返回全新的任务会话。旧连接失效时 丢弃后最多重连一次,不缓存 XML、Selector、坐标或业务字段。 - 官方采集、dry-run 采购、live 采购和核单工厂从当前执行上下文取得同一个持久设备 服务;自定义测试工厂和原有每任务服务边界仍可使用,方便整体回退。 - 停止自动获取、保存或删除 Android 设备、serial 变化、90 秒空闲到期和窗口关闭时, 都把释放命令排入设备线程。窗口关闭先协作停止当前命令,再释放并执行 `quit()/wait()`,不使用 `terminate()`。 - 设置页保存/删除设备后发出稳定信号,由主窗口装配到任务页的释放入口。 - 批量重新上报继续使用原来的独立网络 Worker,不访问 Android 设备。 - 未修改数据库、Admin 接口、任务状态或 `pdd_data`;真实下单默认开关和不可逆阶段 只核单的硬边界保持不变。 与建单方案的差异:没有新增单独的执行器文件,而是在现有 `ClaimTaskWorker/PDDTaskPageEvent` 边界内改成长生命周期实现,减少初级程序员需要追踪 的类和文件。#109 已证明连接复用无法达到原性能目标,因此本次只确认连接复用和 线程安全成立,不把端到端性能项写成通过。 ## 改了哪些 - `client/src/pdd_device_service.py`:持久连接、TTL、每任务健康检查、单次重连和线程归属。 - `client/src/pdd_ui_event.py`:固定 QThread、串行命令路由、停止/释放和窗口关闭流程。 - `client/src/collect_task_service.py`:正式采集使用当前设备线程绑定的连接服务。 - `client/src/pdd_u2_purchase_adapter.py`:dry-run/live 采购使用同一线程内连接服务。 - `client/src/pdd_u2_purchase_reconcile_adapter.py`:不可逆阶段核单使用同一线程内连接服务。 - `client/src/settings_ui_event.py`、`client/src/ui_main.py`:设备配置变化后通知任务页释放旧连接。 - `client/test/test_pdd_device_service.py`:连接复用、TTL、serial 变化、单次重连和跨线程拒绝测试。 - `client/test/test_pdd_ui_event.py`:连续自动获取复用同一固定 QThread 测试。 - `client/test/test_settings_ui_event.py`:设备配置变化信号测试。 - `docs/client/02-architecture.md`:记录持久设备线程、90 秒 TTL 和安全释放规则。 ## 验收结果 | 验收标准 | 结果 | |---|---| | #109 基线和用户覆盖原 No-Go 门槛的决定已记录 | 通过 | | 同一 serial 在 90 秒内连续任务只调用一次 `uiautomator2.connect()` | 通过,单元测试及真机只读 5 次验证 | | Device 的创建、调用和释放固定在同一线程 | 通过 | | 自动获取、手动重试、采购和核单不会并发控制同一设备 | 通过 | | 每个任务重新读取应用状态,不复用页面和业务数据 | 通过 | | TTL、停止、设备配置变化、serial 变化和窗口关闭安全释放 | 通过 | | 缓存连接健康检查失败后最多重连一次 | 通过 | | 不可逆阶段后只允许核单,不重新下单 | 通过,既有采购恢复测试覆盖 | | 热启动阶段中位耗时降低 30% 或 1.5 秒 | 未通过;主要耗时仍是每任务必须执行的 `app_current()` | | 保留可回退边界,未改变数据库、Admin 和任务数据 | 通过 | | Client 全量测试、语法和离屏冒烟通过 | 通过 | | 真实下单仍默认关闭,未执行下单或付款 | 通过 | ## 测试 - 执行的命令: - `C:/Python310/python.exe -m py_compile src/collect_task_service.py src/pdd_device_service.py src/pdd_u2_purchase_adapter.py src/pdd_u2_purchase_reconcile_adapter.py src/pdd_ui_event.py src/settings_ui_event.py src/ui_main.py` - `C:/Python310/python.exe -m unittest discover -s test -p "test_*.py"` - `$env:QT_QPA_PLATFORM="offscreen"; C:/Python310/python.exe -c "from src.ui_main import MainWindow; from PyQt5.QtWidgets import QApplication; app=QApplication([]); w=MainWindow(); print('OK'); w.pddTaskPageEvent.shutdown(); w.settingsPage.eventBinder.shutdown(); app.quit()"` - `adb devices` - 真机只读脚本:同一 Wi-Fi serial 连续创建 5 个 `PersistentPddDeviceService` 任务会话,只读取 `app_current()`。 - `git diff --check` - 结果:Client 全量 316 项通过;语法、差异检查和离屏主窗口冒烟通过;ADB 设备 `192.168.0.173:5555` 在线;5 个真机会话只有 1 次底层连接,线程 ID 始终为 22788。5 次会话健康检查耗时为 11730.6、10756.5、10589.1、10738.6、 10826.3 ms,设备均报告当前包名 `com.android.settings`。 - **没验证到的部分**:没有领取真实 Admin 任务连续执行完整采集或采购;没有执行 真实下单、创建订单或付款;没有在真机等待 90 秒观察 QTimer 到期,而是由单元测试 覆盖 TTL;没有在真机主动制造断连、验证码、风控、登录失效或不可逆阶段崩溃。 ## 遗留问题 连接复用已经成立,但没有达到原性能验收值。当前主要瓶颈是该 OPPO/ColorOS 上 `app_current()` 每次约 10.6 秒且可能错误报告系统设置页。不能为了提速删除每任务 应用和页面安全校验;若继续优化,应另建应用状态检测诊断工单。 ## 相关提交 - `70cedc3` refactor: 复用持久设备工作线程 (#110)