32 · 扫描与列表刷新:全量重建的世界
核心文件:
controller/MiCarAvailableWifiListController.kt(108 行)、controller/MiCarSavedWifiListController.kt(101 行)、controller/MiCarBaseWifiEntryController.kt(339 行,基类)、MiWifiTrackerUtils.kt代码基线:fuben_micarsettings @ dev,f3 flavor 关联:21-架构篇(数据从哪来)、137881 重建风暴详解(本篇机制的性能后果深挖)
0. 一句话总纲
车机 WLAN 列表没有 diff/增量更新——每一波 tracker 回调都是”子线程重新取数 → 主线程 removeAll+全量重建”;“看起来没闪”靠两层补丁:PreferenceComparisonCallback(RecyclerView 级 diff)+ guardTransientEmpty(空波延迟复核)。理解了这一点,列表类 bug(闪空/残留/错序)就都有了分析框架。
1. 端到端时序:从扫描广播到屏幕
sequenceDiagram participant WS as WifiService participant T as WifiPickerTracker(worker) participant L as MiWifiTrackerUtils.Listener participant BC as MiCarBaseWifiEntryController participant TH as SINGLE_ASYNC线程 participant MH as 主线程Handler participant UI as PreferenceGroup(屏幕) WS-->>T: SCAN_RESULTS_AVAILABLE 广播 T->>T: handleScanResultsAvailableAction:365 T->>T: getScanResults→ScanResultUpdater合并<br/>→updateStandardWifiEntryScans(建/复用entry) T->>T: updateWifiEntries:609(拆快照+排序) T->>L: notifyOnWifiEntriesChanged:1554(main post) L->>BC: onWifiEntriesChanged → refreshUi BC->>BC: updateState(preference):96<br/>(先撤旧异步任务:98-101) BC->>TH: fillWifiEntries(清空+重填 mAllWifiEntries,synchronized) TH->>MH: sendMessage(UPDATE_MESSAGE_WHAT=0x101 :50) MH->>UI: 子类 updateState(pref, list) UI->>UI: guardTransientEmpty:142(空波?→挂500ms) UI->>UI: preference.removeAll():45/:69 UI->>UI: 逐条 createWifiEntryPreference+addPreference(全量重建)
代码调用链(对照上图):
WifiPickerTracker#notifyOnWifiEntriesChanged:1554-1558(mainHandler.post)
→ MiWifiTrackerUtils$mTrackerListener#onWifiEntriesChanged:51-55(扇出给 mListeners)
→ MiCar(Saved/Available)WifiListController#onWifiEntriesChanged:93-96/:69-72 → refreshUi()
→ MiCarBaseWifiEntryController#updateState(preference):96-120
├ wifi关 → isVisible=false + removeAll(:103-107)
├ ThreadPoolUtils.SINGLE_ASYNC:清 mAllWifiEntries + fillWifiEntries(:108-112,synchronized)
└ mHandler.sendMessage(0x101):113-119
→ 子类#updateState(preference, list) [主线程]
├ guardTransientEmpty(:41-44 首行) → 空波则 return 不动 UI
├ removeAll()(:45 可用组 / :69 已保存组)
└ createWifiEntryPreference(it, index++) × N(:48)2. 可用列表:MiCarAvailableWifiListController 精读
2.1 填充与过滤(fillWifiEntries :54-67,子线程)
mStartIndex = 0
mWifiTrackerTools?.getAllWifiEntries()?.forEach {
if (it.isSaved) { mStartIndex++; return@forEach } // 已保存跳过,只计语音序号偏移
wifiEntries.add(it)
}
if (getConnectedWifiWifiEntry() != null) mStartIndex++数据源 = getWifiEntries(false) → tracker 的 mWifiEntries(天然不含当前连接条目)。App 层过滤(MiWifiTrackerUtils.kt:122-134 isNeedAddInList):
- 本地热点排除(
HotspotUtils.isHotspot反射比对 MicarSoftApInfo BSSID,:123-125); - 可达性
level != WIFI_LEVEL_UNREACHABLE(:126); - 已保存不进可用组——实现在 Controller:
MiCarAvailableWifiListController.fillWifiEntries:57-59(if (it.isSaved) { mStartIndex++; return@forEach });MiWifiTrackerUtils.kt:127-131 的 isSaved 分支是已保存列表的过滤条件(可用分支 :130 只判 reachable); - wifi 关闭整表返回空(:108)。
没有 5G/band 过滤;没有”已保存去重”(去重在 tracker 层 ScanResultKey,见 21 篇 §2.4);隐藏 SSID 排除也在 tracker(过滤空 SSID,WifiPickerTracker.java:885-887)——隐藏网络未连接时不出现。
2.2 排序
App 层不排序,顺序 = tracker WIFI_PICKER_COMPARATOR(WifiEntry.java:239-252)。可用组排除 saved/connected 后,实际等效信号 level 降序、同 level 按 SSID 字典序。
2.3 扫描触发(两条)
- 列表组头部刷新按钮:
handlePreferenceChanged(REFRESH)→startScan()(:89-95 → :74-80):开扫描动画、10s 自停、MiCarWifiUtils.startScan()(绕过 tracker 直发 WifiManager)。 - WiFi 使能瞬间:
onWifiStateChanged(ENABLED)→startScan()(:97-103)。
3. 已保存列表:MiCarSavedWifiListController 精读
// fillWifiEntries :77-91(子线程)
val connectedWifiEntry = getConnectedWifiWifiEntry() // tracker.connectedWifiEntry
if (connectedWifiEntry != null) wifiEntries.add(connectedWifiEntry) // 连接条目永远第一
val savedWifiEntries = getSavedWifiEntries() // reachable && isSaved 过滤
// TODO:排序需要后期优化(:86)
wifiEntries.addAll(savedWifiEntries)重要推论:已保存列表 = “在扫描范围内的 saved 条目 + 连接条目”。不在范围内的已保存网络不显示(没有用全量配置建表);忘网后无任何专门刷新逻辑,纯靠广播链(见 34 篇)。
当前连接的视觉标记在 preference 层:refreshSavedEntryDisplay(MiCarWifiEntryPreference.kt:100-159)isConnected()||isConnecting() → 五套色 + micar_ui_list_item_bg_selected 背景;右侧详情按钮仅 isSaved && mIsProvisioned 显示(:90-97)。
4. 基类:MiCarBaseWifiEntryController 职责表
| 职责 | 位置 | 说明 |
|---|---|---|
| tracker 获取 | :91-92 | InstanceGetter 拿宿主 fragment 的实例 |
| listener 注册/反注册 | :176-184 | onStartInternal/onStopInternal |
| 双线程刷新框架 | :96-120 | 先撤旧异步任务(:98-101)防乱序;子线程 fill → 主线程重建 |
| 瞬态空防护 | :142-164 | guardTransientEmpty(§5) |
| preference 创建 | :71-84 | 每轮 new,不复用 |
| 条目点击三分支 | :205-236 | 见 33-连接流程 §1 |
| 密码弹窗管理 | :282-309 | 复用 mConnectDialog 防堆叠 |
| 销毁清理 | :186-198 | dismiss/撤 handler/entry 解监听 |
局部刷新通道(不重排时):MiCarWifiEntryPreference 实现 WifiEntry.WifiEntryCallback,onBindViewHolder 里 wifiEntry.setListener(this)(:84);entry 状态变化 → onUpdated()(:187-192)只改 icon/title/summary;onDetached 反注册(:265-269)。
5. 防闪两件套
5.1 guardTransientEmpty(:142-164,commit 7e1970bee,修 MICOCKPIT-146231)
EMPTY_DEFER_MS = 500L // :57;注释:详情页返回重建风暴空窗实测 63~249ms- 非空波到达 → 取消挂起(:143-148);
- 复核到期仍空 → 放行真清空(:149-153);
- UI 本就空 / wifi 关 → 不防护直接走(:154-157);
- 其余空波 → 挂 500ms,本次 return true,子类不动 UI(:158-163)。
- Provision 页没有这层防护(MiCarProvisionWifiListController.kt:38-45 无条件 removeAll)——引导页复现同类问题不受保护。
5.2 PreferenceComparisonCallback(MiCarWifiListFragment.kt:49-73)
RecyclerView 层 diff:同类 + title/summary 相同 → 判定内容相同不重绑。这是”全量重建但看起来不闪”的另一半——preference 对象虽是每轮 new 的,绑定层靠内容比较兜住。
6. 为什么”风暴”会伤性能(引子)
每波回调 = 子线程遍历全部 entry + 主线程 removeAll+new N 个 preference + adapter 通知。当事件密集(开关/忘网/回页面)时,tracker 会连发多波(见 34 篇 §2 的 2~3 波,以及 137881 的 monkey 场景风暴),波与波之间无合批(弱折叠管线),每波都全价重建。性能后果(O(N²) adapter、notify 洪流、no-focused-window ANR)在 WLAN列表重建风暴与ANR机制详解 深挖,此处只立机制事实。
7. 易混淆点速查
getWifiEntries()返回 mWifiEntries 副本(不含连接条目),getConnectedWifiEntry()单取(WifiPickerTracker.java:216-239)。- mWifiEntries 包含在范围内的已保存条目(供 Saved 组用,:665-705),Available 组在 App 层再滤掉 isSaved。
- 一个 SSID+安全族一个 StandardWifiEntry(双频同 SSID 合一)。
- wifi 关闭瞬间
handleWifiStateChangedAction → clearAllWifiEntries()(:356-363)是唯一全缓存清空点。
[待验证]:框架侧广播顺序需 logcat 实证;两个竞态窗口(34 篇)实车时长未测。