90 · 蓝牙主页启动全流程 · 总览
从”桌面点蓝牙图标”到”页面完整展示”的端到端全景。先整体,后分章: 90-总览(本篇) → 91-Activity与Fragment启动骨架 → 92-XML解析与Controller装配 → 93-首屏数据:开关与已配对列表 → 94-扫描发现与可用设备上屏 行号基线:fuben_micarsettings dev HEAD(2026-09-13)。
0. 三十秒版本
点图标 → Activity建容器+标题栏 → Fragment commit(异步) → onAttach: 反射建Controller+建蓝牙单例+同步读已配对
→ onCreate: inflate Preference树+绑定 → onCreateView: 装RecyclerView+空态图 → onViewCreated: 挂语音监听
→ onStart: 注册蓝牙回调 → onResume: ★闸门放开,首刷开始 → 开关/已配对走200ms延迟 → 后台取数 → 主线程上屏
→ 触发扫描 → 设备逐台批处理上屏(500ms×10台) → 角标补齐 → 页面可交互
三条核心设计哲学贯穿全程:
| 哲学 | 体现 |
|---|---|
| 首帧感知优先 | 首开 200ms 延迟(BLUETOOTH_DATA_LAZY_TIME)让重数据给首帧让路;UI 先行(preHandleBtOff/enableBluetooth 先置态后等广播校准) |
| 事件驱动 + 闸门 | 生命周期观察者驱动 Controller;mIsViewInitialized 挡首刷;扫描闸门统一管控 |
| 主线程保护 | settingslib 广播全在主线程(持锁 dispatch!)→ App 侧重活全挪后台(取数/过滤/binder),UI 批处理限流(500ms×10台) |
1. 全景时序图(点击 → 首帧 → 可交互)
sequenceDiagram autonumber participant U as 用户(桌面/Dock) participant Act as BluetoothSettingsActivity participant FM as FragmentManager participant Frag as BluetoothMainEntrySettingsFragment participant Ctrl as 3个Controller participant LBM as LocalBluetoothManager participant BG as 后台线程 participant Stack as 蓝牙协议栈 U->>Act: startActivity → onCreate Note over Act: ROM校验→setContentView(仅dialog_container)<br/>→标题栏→UxRestrictions启动 Act->>FM: replace+addToBackStack+commit()<br/>⚠️异步,onCreate继续走 Act->>Act: resetScanFlag / 导航接管 / 注册关闭广播 FM->>Frag: onAttach Frag->>Frag: ★解析XML元数据→反射建3个Controller<br/>注册lifecycle observer (92章) Frag->>LBM: getLocalBtManager() 单例首创 LBM->>LBM: readPairedDevices(主线程,binder)<br/>→CachedBluetoothDeviceManager缓存就绪<br/>(此时无订阅者,纯建仓) FM->>Frag: onCreate → onCreatePreferences Note over Frag: inflate Preference树→controller.setPreference绑定 FM->>Frag: onCreateView Note over Frag: 装RecyclerView(渐变蒙版)+空态图叠放<br/>Adapter=HighlightablePreferenceGroupAdapter FM->>Frag: onViewCreated Frag->>Frag: 挂语音监听 setViewListener(36篇) FM->>Frag: onStart Frag->>LBM: registerCallback(Fragment自己) Ctrl->>LBM: registerCallback(各Controller, onStartInternal) FM->>Frag: onResume ★首刷闸门放开 Ctrl->>Ctrl: mIsViewInitialized=true → updateState par 开关(4跳全主线程) Ctrl->>Stack: adapter.getState() (binder) Ctrl->>Ctrl: handleStateChanged 直推开关UI and 已配对(7跳) Ctrl->>Ctrl: postDelayed 200ms (首开延迟) Ctrl->>BG: getCachedDevicesCopy+bondedDeviceFilter(binder) BG->>Ctrl: 回主线程 removeAll+逐个addPreference(排序插入) Note over Ctrl: onAttached→refreshDeviceUi 定title/三态摘要/图标 and 可用设备(94章) Ctrl->>Ctrl: 200ms后 innerUpdateState 清重建 Ctrl->>BG: triggerScan("update_state") BG->>Stack: startDiscovery end loop 扫描循环(扫完12s自动重扫) Stack-->>LBM: ACTION_FOUND (主线程!) LBM-->>Ctrl: onDeviceAdded (持锁dispatch) Ctrl->>Ctrl: 拦截/过滤→入缓冲队列 Ctrl->>Ctrl: 每500ms一批≤10台 addPreference上屏 end Note over Frag: 每次层级变化 → onPreferenceUpdateFinish<br/>→ 补齐语音序号角标 Note over U: 页面可交互
2. 分阶段总表
| # | 阶段 | 干什么 | 线程 | 章节 |
|---|---|---|---|---|
| S0 | Activity 启动 | ROM 校验、容器、标题栏、commit Fragment(异步) | 主 | 91 §2 |
| S1 | onAttach | 反射建 Controller + 注册生命周期观察者;LBM 单例首创 + 主线程同步 readPairedDevices 建仓 | 主 | 92 §1-3 / 93 §3 |
| S2 | onCreate/Preferences | inflate Preference 树 + controller.setPreference 绑定(类型校验+挂监听) | 主 | 92 §2 |
| S3 | onCreateView | RecyclerView(渐变蒙版)+ 空态图同容器叠放;Adapter 装配 | 主 | 91 §5 |
| S4 | onViewCreated | 挂语音监听;listView 设 ViewDescription;forEach controller.onViewCreated | 主 | 91 §3 / 36 |
| S5 | onStart | Fragment 与各 Controller 各自 registerCallback;开关设 summary | 主 | 91 §3 / 92 §6 |
| S6 | onResume | ★mIsViewInitialized=true 闸门放开;首开 200ms 延迟启动;空态按开关显隐;startLoading 动画起转 | 主 | 92 §5 / 93 §4 / 94 §1 |
| S7 | 首屏数据 | 开关:getState 直推(onCreate 已首刷);已配对:后台过滤→主线程排序插入→三态摘要 | 主+后台 | 93 |
| S8 | 扫描上屏 | triggerScan→ACTION_FOUND→拦截/过滤→缓冲→500ms×10台批处理上屏 | 主(回调)+后台(触发) | 94 |
| S9 | 角标补齐 | 每次层级变化 → onPreferenceUpdateFinish(去重到一帧一拍)→ 语音序号 | 主下一拍 | 91 §4 / 94 §6 |
| S10 | 退出 | onPause 停动画/停扫/清缓存/重置首开标志;onStop 注销回调 | 主+后台 | 94 §7 |
3. 五张”记住就不迷路”的要点卡
flowchart LR subgraph K1["① 两个200ms"] A["首开延迟 BLUETOOTH_DATA_LAZY_TIME=200ms<br/>两个列表各一次, onPause重置<br/>★每次进页都有,非仅冷启动"] end subgraph K2["② 首帧与广播无关"] B["已配对首帧=LBM构造时同步建仓<br/>开关首帧=onCreate直读adapter<br/>广播只管首帧后的增量修正"] end subgraph K3["③ 主线程上的雷"] C["settingslib广播回调全在主线程<br/>addDevice持锁dispatch(CBDM:135)<br/>→ App侧回调里只许O(1)入队"] end subgraph K4["④ 两道闸门"] D["mIsViewInitialized: 首刷挡到onResume<br/>BluetoothScanManager: 扫描闸门<br/>两道的放行点都要熟"] end subgraph K5["⑤ 批处理防卡顿"] E["onDeviceAdded不直接上屏<br/>缓冲队列+500ms×10台+锁外UI<br/>密集环境的保命设计"] end K1 --- K2 --- K3 --- K4 --- K5
4. 页面上有哪些东西(首帧完成态)
flowchart TD subgraph Page["蓝牙主页(dialog样式)"] T["标题栏: 蓝牙 + 关闭钮<br/>(BaseDialogActivity 提供)"] SW["开关行: MiCarConnectSwitchPreference<br/>summary='可被发现为<车机名>'"] G1["已配对分组 (pk_bluetooth_paired_devices)<br/>已连接排最前→字母序<br/>三态摘要: 已连接/正在连接/已保存"] G2["可用设备分组 (pk_bluetooth_available_devices_e2)<br/>尾部刷新动画(全程旋转)<br/>只显示手机类设备(有意过滤)"] TIP["空态图: 蓝牙关时叠在列表中央"] VB["语音角标: 可见即可说激活时<br/>左侧序号(连续编号)"] end T --> SW --> G1 --> G2 G1 -.叠放.- TIP G1 -.装饰.- VB G2 -.装饰.- VB
5. 学习地图:怎么读这个系列
flowchart LR Start["90 总览(本篇)"] --> N1["91 启动骨架<br/>答: 页面架子怎么搭起来的"] N1 --> N2["92 Controller装配<br/>答: XML怎么变成活的Controller"] N2 --> N3["93 首屏数据<br/>答: 开关和已配对从哪来"] N3 --> N4["94 扫描上屏<br/>答: 附近设备怎么一台台冒出来"] N4 --> Done["✅ 全链路闭环"] Done -.-> X1["深入: 01发现流程/02配对/03详情"] Done -.-> X2["排障: 40日志取证/42踩坑/68案例"] Done -.-> X3["语音: 36可见即可说"] style Start fill:#e1f5ff style Done fill:#e8f5e9
读完这个系列你应该能回答:
- 为什么打开蓝牙页,开关先出来、列表慢一拍?(200ms 首开延迟 + mIsViewInitialized 闸门)
- 为什么飞行模式开关蓝牙后回页面,已配对立刻有、附近设备要等?(缓存建仓 vs 真实扫描)
- 为什么展厅环境里设备是一台台冒出来而不是一次性全出?(500ms×10 批处理)
- 语音说”连接第 3 个”为什么能点中?(36 篇 + 91 §4 的补齐回调)
6. 与既有篇目的分工
| 主题 | 本系列 | 既有篇目 |
|---|---|---|
| 启动/首帧全景 | 90-94(本文) | — |
| 扫描机制深挖(闸门/循环/过滤规则) | 94 讲启动视角 | 01-发现流程 讲机制全量 |
| 配对流程 | — | 02-配对流程 |
| 已配对摘要状态机(运行期) | 93 §5 只讲首帧取值 | 34-摘要状态机 |
| CBD 缓存对象生命周期 | 93 §3 只讲建仓时机 | 60 系列 |
| 语音链路全解 | 91/94 只标挂点 | 36-可见即可说 |
| 性能/卡顿案例 | 94 §8 只列设计 | 68-案例走读 |
7. 已知未核实项(诚实清单)
- 外部库(micarx appcompat / car-ui PreferenceFragment / micarx.preference)无本仓源码:
Preference.onBindViewHolder的文本绑定、PreferenceGroupAdapter刷新细节按 androidx 语义理解;binarySearch排序插入经 javap 字节码核实(micarx.preference 3.2.6) VoiceSearchTrigger.uploadWidgetsAsync执行线程未核实- Fragment onAttach 为进程首创 LBM 的结论基于调用点枚举,未做运行时验证