Design §8.4.1:补 DeviceKit 9 项接口实现骨架

覆盖 §3.1 异步 handler 6 项 + §3.2 反向 callback 3 项,含本轮全文 audit
新发现的 3 项反向 callback(前一轮 audit 漏列):

§3.1 异步 handler 6 项(DeviceHandlers.register):
  - 【10】vibrator           — AudioServicesPlaySystemSound + cb "vibrator"
  - 【11】repeatvibrator     — 同 vibrator(msext RootVC.m:1820 沿用历史)
  - 【12】canclevibrator     — AudioServicesDisposeSystemSoundID + cb "canclevibrator"
  - 【13】gamepastetext      — UIPasteboard.general.string → cb 返回内容
  - 【14】gameCopytext       — UIPasteboard.general.string = data + cb "gameCopytext"
  - 【21】getphoneInfo       — cb "getphoneInfo"(大写 I)+ bridge.call("getphoneinfo"
                              小写 i!,data: 表 A 6 字段)
                              ※ 大小写 i 不一致是 msext 历史契约,严格保持

§3.2 反向 callback 3 项(DeviceHandlers.bindReverseCallbacks):
  - 【2】 getBattery   — UIDevice.batteryLevelDidChangeNotification
                        data: String(format: "%.2f", level) 0~1
  - 【3】 getnetwork   — NWPath 变化 data: "1"无网/"2"WiFi/"3"蜂窝
  - 【4】 appservice   — App 前后台 data: "1"=后台/"2"=前台(命名错位沿用)

辨析 polyfill 与 callback 同名问题:契约 §3.4.1 polyfill 的同步 getter
`getbattery()` / `getnetwork()` 与 §3.2 反向 `getBattery` / `getnetwork`
名字重合但语义不同(前者 H5 同步问当前值,后者 Native 事件推变化),
两条路径并存互不矛盾。在 §8.4.1 新增辨析段澄清。

支撑组件骨架:BatteryMonitor / AppLifecycleMonitor(NetworkMonitor 已在
§8.4 现有);§8.4.2 与 msext 6 维度差异表。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 07:28:34 +08:00
co-authored by Claude Opus 4.7
parent 82bad8a85d
commit c6221d30ca
+202
View File
@@ -2139,6 +2139,208 @@ public final class NetworkMonitor {
用 `Network.framework` 替换 `AFNetworkReachabilityManager`。
#### 8.4.1 H5 device handlers + 系统事件反向 callback 实现骨架
DeviceKit 共承载 **9 项契约接口**:
- §3.1 异步 handler 6 项:`vibrator` / `repeatvibrator` / `canclevibrator` / `gamepastetext` / `gameCopytext` / `getphoneInfo`
- §3.2 反向 callback 3 项:`getBattery`(电量变化推送)/ `getnetwork`(网络变化推送)/ `appservice`(前后台切换推送)
注:契约 §3.2 [2][3]的反向 `getBattery` / `getnetwork` 名字与 §3.4.1 polyfill 的同步 getter `getbattery()` / `getnetwork()` 重合但语义不同:
- **polyfill** = H5 同步问"当前是什么"(一次性快照,注入到 `window.settings`)
- **callback** = Native 异步推"刚刚变成了什么"(事件驱动,通过 `bridge.call(...)` 推给 H5)
- 两条路径并存、互不矛盾,H5 根据使用场景选其一
```swift
// Source/Bridge/Handlers/DeviceHandlers.swift
import UIKit
import AudioToolbox
@MainActor
public struct DeviceHandlers {
let bridge: BridgeProtocol
let pasteboard: UIPasteboard = .general
// BatteryMonitor / NetworkMonitor / AppLifecycleMonitor 在 register() 内启动
// 并把回调钩到 bridge.call(...),构成"事件 → callback"的反向链路
let batteryMonitor: BatteryMonitor
let networkMonitor: NetworkMonitor
let appLifecycle: AppLifecycleMonitor
public func register() {
// ─── §3.1 异步 handler ──────────────────────────────
bridge.register("vibrator", handler: vibrator)
bridge.register("repeatvibrator", handler: repeatVibrator)
bridge.register("canclevibrator", handler: cancelVibrator)
bridge.register("gamepastetext", handler: gamePasteText)
bridge.register("gameCopytext", handler: gameCopyText)
bridge.register("getphoneInfo", handler: getPhoneInfo)
// ─── §3.2 反向 callback 钩子 ────────────────────────
bindReverseCallbacks()
}
// MARK: - 【10】 vibrator — 单次振动
//
// 契约 §3.1 [10]:入参忽略 → AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
// responseCallback: "vibrator"(注意:vibrator 系列的 callback 是
// handler 名字本身,不是 "Response from xxx",
// 与 audio 系列约定不同;参考 msext Bridge.m 约定)
//
// 当前实现见 Source/Bridge/Handlers/VibratorHandler.swift(Phase 1.15 已落地,
// 本节是把它放进 DeviceHandlers 聚合时的等价骨架)
private func vibrator(_ data: BridgeData?, _ cb: BridgeCallback?) async {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
cb?(.string("vibrator"))
}
// MARK: - 【11】 repeatvibrator — 重复振动(实际同 vibrator)
//
// 契约 §3.1 [11]:入参 int 但被忽略,原 msext RootVC.m:1820 实现就是单次 vibrate
// 沿用历史,不真的循环
private func repeatVibrator(_ data: BridgeData?, _ cb: BridgeCallback?) async {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
cb?(.string("repeatvibrator"))
}
// MARK: - 【12】 canclevibrator — 释放系统振动音
//
// 契约 §3.1 [12]:AudioServicesDisposeSystemSoundID(kSystemSoundID_Vibrate)
// 实际意义有限(kSystemSoundID_Vibrate 是系统常量,dispose 也不会真的"取消"),
// 但 H5 仍会调用,必须 noop 实现 + callback 维持契约
private func cancelVibrator(_ data: BridgeData?, _ cb: BridgeCallback?) async {
AudioServicesDisposeSystemSoundID(kSystemSoundID_Vibrate)
cb?(.string("canclevibrator"))
}
// MARK: - 【13】 gamepastetext — 读剪贴板
//
// 契约 §3.1 [13]:
// 入参 : 忽略
// responseCallback: 剪贴板字符串内容(不是 "gamepastetext"),nil 时回空串
//
// 注意:iOS 14+ 读剪贴板会弹"已粘贴自 XX"系统提示,无法关闭,是平台行为
// 不算契约违反;如未来 H5 业务希望静默读,需 H5 端配合改用 UIPasteboard 检测 API
private func gamePasteText(_ data: BridgeData?, _ cb: BridgeCallback?) async {
let text = pasteboard.string ?? ""
cb?(.string(text))
}
// MARK: - 【14】 gameCopytext — 写剪贴板
//
// 契约 §3.1 [14]:
// 入参 data : string 要写入剪贴板的文本
// responseCallback: "gameCopytext"
private func gameCopyText(_ data: BridgeData?, _ cb: BridgeCallback?) async {
if let text = data?.asString {
pasteboard.string = text
}
cb?(.string("gameCopytext"))
}
// MARK: - 【21】 getphoneInfo(大写 I)— 请求设备信息
//
// 契约 §3.1 [21]:
// 入参 : 忽略
// responseCallback: "getphoneInfo"(注意大写 I)
// 反向调用: bridge.call("getphoneinfo"(小写 i!), data: 表 A, callback: nil)
//
// 大小写 i 不一致是 msext 历史遗留契约,必须严格保持:
// - H5 调原生时用大写:bridge.callHandler("getphoneInfo", ...)
// - 原生回调 H5 时用小写:bridge.callHandler("getphoneinfo", ...)
//
// 表 A 字段名固定(PhoneAdresseMAC / PhoneDeviceBrand / PhoneIMEI / PhoneModel /
// PhoneProvidersName / PhoneVersion),值统一为 string,详见 Contract §3.2 表 A
private func getPhoneInfo(_ data: BridgeData?, _ cb: BridgeCallback?) async {
cb?(.string("getphoneInfo"))
// 反向 push 设备信息快照(注意 handler 名小写 i)
let snapshot = DeviceInfo.snapshot
bridge.call("getphoneinfo",
data: .object(snapshot.mapValues { .string($0) }),
callback: nil)
}
// MARK: - §3.2 反向 callback 钩子(事件驱动)
private func bindReverseCallbacks() {
// 【2】 getBattery — UIDeviceBatteryLevelDidChangeNotification
// 数据:字符串 "%.2f" 形式的小数电量(0~1)
UIDevice.current.isBatteryMonitoringEnabled = true
batteryMonitor.onChange = { [weak bridge] level in
let str = String(format: "%.2f", level)
bridge?.call("getBattery", data: .string(str), callback: nil)
}
batteryMonitor.start()
// 【3】 getnetwork — NWPath 变化
// 数据:字符串 "1"=无网 / "2"=WiFi / "3"=蜂窝
networkMonitor.stateDidChange = { [weak bridge] state in
bridge?.call("getnetwork", data: .string(state.rawValue), callback: nil)
}
networkMonitor.start()
// 【4】 appservice — App 前后台切换
// 数据:字符串 "1"=进入后台 / "2"=回到前台(命名错位,沿用历史)
appLifecycle.onBackground = { [weak bridge] in
bridge?.call("appservice", data: .string("1"), callback: nil)
}
appLifecycle.onForeground = { [weak bridge] in
bridge?.call("appservice", data: .string("2"), callback: nil)
}
appLifecycle.start()
}
}
// MARK: - 支撑监控器(Source/DeviceKit/)
@MainActor
public final class BatteryMonitor {
public var onChange: ((Float) -> Void)?
public func start() {
NotificationCenter.default.addObserver(
forName: UIDevice.batteryLevelDidChangeNotification,
object: nil, queue: .main
) { [weak self] _ in
self?.onChange?(UIDevice.current.batteryLevel)
}
}
}
@MainActor
public final class AppLifecycleMonitor {
public var onBackground: (() -> Void)?
public var onForeground: (() -> Void)?
public func start() {
let nc = NotificationCenter.default
nc.addObserver(forName: UIApplication.didEnterBackgroundNotification,
object: nil, queue: .main) { [weak self] _ in
self?.onBackground?()
}
nc.addObserver(forName: UIApplication.willEnterForegroundNotification,
object: nil, queue: .main) { [weak self] _ in
self?.onForeground?()
}
}
}
```
#### 8.4.2 与原 msext 的差异
| 维度 | msext 现状(不要照抄) | 新外壳决策 |
|------|--------------------|----------|
| 振动取消 | `AudioServicesDisposeSystemSoundID(kSystemSoundID_Vibrate)` 同款 | 同款(契约硬约束,结构性意义有限但 H5 仍依赖 callback) |
| 剪贴板 | `[UIPasteboard generalPasteboard].string` | `UIPasteboard.general.string`(行为等价;iOS 14+ "已粘贴"提示是平台行为,不算契约违反) |
| 电量监听 | `UIDeviceBatteryLevelDidChangeNotification` + `[NSString stringWithFormat:@"%.2f"]` | `Notification.batteryLevelDidChangeNotification` + `String(format:)`(等价) |
| 网络监听 | `AFNetworkReachabilityManager` | `Network.framework` `NWPathMonitor`(ADR-006,无 AF 依赖) |
| 前后台 | `UIApplicationDidEnterBackgroundNotification` + `UIApplicationWillEnterForegroundNotification` 各自挂大厅 VC | `AppLifecycleMonitor` 集中处理,多 VC 复用 |
| getphoneInfo 大小写 | 大写 I 入 / 小写 i 出 | 严格保持(契约硬约束,msext Bridge.m 沿用) |
### 8.5 微信 / QQ 多发起方派发(关键设计)
#### 问题背景