AOSP vs 小米车机(MiCar)蓝牙已配对列表设计对比
2026-08-11 · 锚点:MANNPROB-3187 / MANNPROB-3205 · 配套知识库:
30-模块流程篇/34-已配对列表摘要状态机-AOSP与MiCar.mdv2(2026-09-07)增量锚点:MIICOSBUG-2471 —— 双源结构被证出第二种失败模式(同设备跨 profile 覆写,§2.3/§4/§5);补 AOSP 被忽视的第二支柱「后台线程算摘要」(§2.4)。 结论先行:两套实现在”已连接/正在连接”上功能等价;差异集中在”空闲态怎么呈现”(AOSP 空白 vs MiCar「已保存」)和”正在连接的判据结构”(AOSP 状态机单源 vs MiCar 状态机×旁路开关双源)。后者是 3187/3205 两个 bug 的结构性土壤。
1. 设计哲学对比
| 维度 | AOSP 原生 | MiCar 车机 |
|---|---|---|
| 空闲态呈现哲学 | 静默:已配对未连接 = 无摘要,行只显示设备名 | 显式:给空闲态一个明确标签「已保存」 |
| 状态推导哲学 | 单源:摘要 100% 从 profile 状态机实时推导,无缓存无旁路 | 双源:状态机(isBusy/isConnected)× Controller 旁路(mIsConnectingState)联合判据 |
| 过渡态粒度 | 四态全呈现:连接中/断开中是两个文案 | 压缩为三态:断开中不呈现,isBusy 混含 DISCONNECTING |
| 交互保护 | 无防抖无守卫(置灰即时,靠状态机天然正确) | 300ms 置灰防抖 + isBusy 点击守卫(PS3) |
| 连接失败反馈 | 无 toast(摘要停在断开文案) | mStartToConnect 失败 toast |
| 信息量 | 电量/使用中(active)变体丰富 | 精简三态 + 排序置顶 + 蓝色选中态 |
| 重计算线程模型 | 后台线程算摘要+主线程贴结果(Settings 侧 ThreadUtils.postOnBackgroundThread) | 主线程直接算(139360 主线程 binder 风暴的土壤) |
一句话:AOSP 追求”状态机的忠实投影,不该说话时沉默”;MiCar 追求”每个状态都有明确标签”,为此引入了旁路状态位——信息量的提升是以状态双源化为代价的,而双源必然产生同步窗口,所有相关 bug 都长在这个窗口上。
2. 状态模型逐项对比
2.1 状态集合
flowchart LR subgraph AOSP 四态+空白 A1[空白 null] A2[正在连接…] A3[正在断开…] A4[已连接] A5[正在配对…] end subgraph MiCar 三态 M1[已保存] M2[正在连接...] M3[已连接] end
| 协议栈真实状态 | AOSP 显示 | MiCar 显示 | 评价 |
|---|---|---|---|
| 已配对,全空闲 | 空白 | 已保存 | MiCar 信息更明确;代价是栈层回连延迟被可视化 |
| 任一 profile CONNECTING | 正在连接… | 正在连接…(+动画) | 等价;MiCar 多动画但多旁路风险 |
| 任一 profile DISCONNECTING | 正在断开… | 已连接→已保存 之间跳变(3205 前)/ 已保存(3205 后) | AOSP 更精确;MiCar 有意压缩,3205 修掉了跳变 |
| 任一 profile CONNECTED | 已连接(+电量/使用中) | 已连接(+蓝色+置顶) | 等价 |
| BOND_BONDING | 正在配对… | (配对中行走未配对列表) | 场景不同不比较 |
2.2 判据结构
| 态 | AOSP 判据 | MiCar 判据 | 结构性风险 |
|---|---|---|---|
| 已连接 | 任一 profile CONNECTED(单源实时) | 同左(单源实时) | 两边都安全 |
| 正在连接 | 任一 profile CONNECTING(单源实时) | isBusy && mIsConnectingState(双源联合) | MiCar 独有:双源可不同步 |
| 空闲 | (兜底 null) | else → 已保存(兜底) | MiCar 的 else 承接所有双源失败 |
mIsConnectingState 旁路的三个写入点(MiCar 独有):
- preference 重建时默认 false(Adapter ON → innerUpdateState removeAll+re-add)
- Controller
setConnecting(state==CONNECTING)(profile 回调驱动) !isBusy时自动清 false(refreshDeviceUi kt:75)
旁路存在的理由:MiCar 把 DISCONNECTING 也纳入 isBusy,若只用 isBusy 单条件,断开时会误显示「正在连接」——旁路开关用来区分”连接中”与”断开中”。AOSP 不需要这个旁路,因为它给断开中一个独立文案。
2.3 双源结构的两种失败模式(v2 增补)
| 失败模式 | 机理 | 状态 |
|---|---|---|
| ①跨设备写入污染(3187) | Controller 回调循环无设备过滤,A 设备的回调清掉 B 设备行的旁路开关 | ✅ 已修(362953 设备匹配过滤) |
| ②同设备跨 profile 覆写(2471) | 过滤到设备后,setConnecting(state==CONNECTING) 仍按”最近一次回调”整体覆写单布尔;同设备 HFP(16)/A2DP(11)/MAP(18)/PBAP(17) 并行连接时,某 profile 真实首试失败 DISCONNECTED(栈内 70 | ❌ 未修,dev/dev_a17_0610/rls-xcd-x-ee-2608-2609 全带 |
写入点可枚举不等于写入安全:§6 建议 2 说”旁路写入点必须可枚举”,2471 证明即使只有 setConnecting 一个主动写入点,只要写入粒度(设备级布尔)小于事件粒度(profile 级回调),覆写污染就是结构性的。AOSP 免疫不是因为写入点少,而是根本没有旁路——聚合在读取侧现算。
2.4 AOSP 的第二支柱:后台线程算摘要(v2 增补)
AOSP 状态单源解决”算得对”,但 getConnectionSummary() 是逐 profile 活 binder 调用(profile.getConnectionStatus(mDevice) = mService.getConnectionState(device)),手机 Settings 怎么扛住 binder 成本?答案是换线程,不是缓存状态:
// AOSP packages/apps/Settings BluetoothDevicePreference.onPreferenceAttributesChanged()
ThreadUtils.postOnBackgroundThread(() -> {
String connectionSummary = getConnectionSummary(); // 后台线程:binder 链随便调
boolean isBusy = mCachedDevice.isBusy();
...
ThreadUtils.postOnMainThread(() -> {
setSummary(connectionSummary);
setEnabled(!isBusy);
});
});对比 MiCar:refreshDeviceUi()/getDeviceSummary() 全部主线程直算,遇 60+ 设备排序刷新风暴即成 N801PROB-139360 的主线程 binder 风暴病灶。AOSP 两大支柱:读取侧聚合(不算错)× 后台线程计算(不算卡);MiCar 两个都没抄到 —— 705515c51 自造旁路开关破坏了前者,主线程直算放弃了后者。
3. 时序行为对比(关开蓝牙自动回连,实测)
sequenceDiagram autonumber participant U as 用户 participant ST as 协议栈 participant AOSP as AOSP UI participant MC as MiCar UI U->>ST: 关蓝牙 ST-->>AOSP: 已连接→(正在断开…)→空白 ST-->>MC: 已连接→已保存 U->>ST: 开蓝牙 Note over ST: 建链耗时(广播实测 40~77ms 不慢;HFP 1.2~3.8s/台,双设备串行 4~5.5s;A14 更快→栈侧问题) ST-->>AOSP: 空白(无感) ST-->>MC: 已保存(可见!用户感知"闪") ST->>ST: 首个 profile CONNECTING ST-->>AOSP: 正在连接… ST-->>MC: 正在连接...(旁路置位) ST->>ST: 首个 profile CONNECTED ST-->>AOSP: 已连接 ST-->>MC: 已连接(置顶+蓝色)
| 阶段 | AOSP | MiCar | 差异本质 |
|---|---|---|---|
| OFF 后 | 空白 | 已保存 | 文案策略 |
| ON → 栈开始连(广播 40 | 空白 | 已保存 | 唯一被用户感知的差异窗口 |
| 连接中 | 正在连接… | 正在连接…+动画 | 等价 |
| 回连中途他设备回调 | 无影响(无旁路) | 3187 修复前:中途回退已保存;PS3 后:无影响 | MiCar 旁路风险之一,已修(362953) |
| 连接途中同设备他 profile 失败回调 | 无影响(读取侧逐 profile 聚合,单 profile DISCONNECTED 不影响其余 CONNECTING 的判定) | 回落「已保存」160~200ms(2471 实证:07-20 iPhone 两次、07-07 K90 一次) | 旁路风险之二,未修 |
| 断开中 | 正在断开… | 已保存(3205 前:置灰波动) | MiCar 有意压缩粒度 |
4. 缺陷史对照(为什么 bug 只长在 MiCar 侧)
| Bug | MiCar 机理 | AOSP 免疫的结构性原因 |
|---|---|---|
| MANNPROB-3187(双设备回连先已保存) | Controller 回调循环无设备过滤,旁路开关被跨设备清零,双条件失败落 else | 无旁路、无「已保存」、无 Controller 全量刷新 |
| MANNPROB-3205(断开采灰波动) | isBusy 含 DISCONNECTING 误驱动画+setEnabled(!isBusy) 翻转 | 断开有独立文案与状态呈现,不需要 isBusy 混用驱动视觉 |
| 「已保存」等待窗口(测试误报为 bug) | 「已保存」else 兜底把栈层建链耗时(串行 4~5.5s)可视化 | 空闲=空白,等待窗口无感 |
| MIICOSBUG-2471(连接中回落已保存) | 同设备跨 profile 覆写:MAP(18) 首试失败 DISCONNECTED 清掉设备级单布尔,isBusy 被其余 profile 撑住 → 双条件失败落 else;iPhone MAP 8 试 0 成(含栈侧自发重试)使其从偶发变必现 | 无旁路;聚合在读取侧现算,单 profile 失败不清其余 profile 的 CONNECTING 判定 |
共性教训:MiCar 的三处定制(「已保存」文案 / mIsConnectingState 旁路 / isBusy 混含断开)每一个单独看都是合理的产品决策,但三者叠加形成”状态双源+兜底可见”的结构,凡协议栈时序抖动(多 profile 异步、双设备交叉、回连延迟)都会在该结构上放大为可见 UI 异常。
5. 修复现状与残留语义问题
| 项 | 状态 |
|---|---|
| 3187 跨设备污染(中途回退已保存) | ✅ 已修(362953 PS3,设备匹配过滤)——只修跨设备;同设备跨 profile 残余=2471,未修 |
| 3205 断开采灰波动 | ✅ 已修(动画判据对齐+300ms 防抖+同目标不重计时) |
| ON→全连完的「已保存」窗口(广播 40 | ⚠️ 语义问题,非 bug:栈层建链耗时的忠实呈现(A14 实测更快 → 协议栈性能差异,转栈侧优化)。要消灭需乐观标记(ON 时已配对设备置 connecting),代价=离线设备先”正在连接”再失败回退,待产品裁决 |
| DISCONNECTING 无独立文案 | ⚠️ 设计压缩,3205 后视觉正确;若产品要”正在断开”文案可映射 DISCONNECTING 单独显示 |
| 2471 同设备跨 profile 覆写 | ❌ 未修。方案 A(推荐):Controller 回调改喂 per-profile 连接中集合(setProfileConnecting(profile, state==CONNECTING)),Set 替代单布尔,summary/动画/置灰三消费点同步 isNotEmpty(),!isBusy 终态 clear —— 本质是用回调缓存复刻 AOSP 读取侧聚合语义,零新增 binder。方案 B(彻底对齐):摘要改调 cachedDevice.getConnectionSummary() + AOSP 式后台线程计算 —— 需配合 139360 刷新预算/快照基础设施,留给排序风暴整改统一做。禁止:回退 isBusy 单判(断开中误显)/ 主线程现查 getProfileConnectionState(binder 风暴) |
6. 给后续开发的设计建议
- 不要再给摘要加第四个状态文案——每加一个状态,双源同步窗口就多一类;先评估能否用现有三态映射
- 旁路状态位的写入点必须可枚举——mIsConnectingState 全仓 3 个写入点(默认值/setConnecting/!isBusy 自清),新增写入点要过评审(3187 的教训:写入点失控=污染源)
- 跨设备操作必须带设备过滤——Controller 遍历列表的循环,默认第一行就写
cachedDevice.equals(...)守卫 - 防抖/延时逻辑警惕重计时(P14):持续事件流中”每次都重新计时”会把生效时刻无限推迟;同目标不重计时
- 状态位粒度必须 ≥ 事件粒度(2471 新增):设备级布尔承接 profile 级回调必然被覆写;要么聚合读取(AOSP),要么 per-profile 集合缓存(方案 A),不允许”最近一次回调”整体覆写
- binder 链计算的 AOSP 答案是换线程不是缓存(2471 新增):
getConnectionSummary()/isBusy()这类逐 profile 活 binder 调用,AOSP 一律postOnBackgroundThread后台算、主线程贴;车机排序风暴整改(139360)应对齐此模型 - 改这片代码前必读:
30-模块流程篇/34-已配对列表摘要状态机-AOSP与MiCar.md+ jira-analyze LESSONS #24