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 扫描触发(两条)

  1. 列表组头部刷新按钮:handlePreferenceChanged(REFRESH)startScan()(:89-95 → :74-80):开扫描动画、10s 自停、MiCarWifiUtils.startScan()(绕过 tracker 直发 WifiManager)。
  2. 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-92InstanceGetter 拿宿主 fragment 的实例
listener 注册/反注册:176-184onStartInternal/onStopInternal
双线程刷新框架:96-120先撤旧异步任务(:98-101)防乱序;子线程 fill → 主线程重建
瞬态空防护:142-164guardTransientEmpty(§5)
preference 创建:71-84每轮 new,不复用
条目点击三分支:205-23633-连接流程 §1
密码弹窗管理:282-309复用 mConnectDialog 防堆叠
销毁清理:186-198dismiss/撤 handler/entry 解监听

局部刷新通道(不重排时):MiCarWifiEntryPreference 实现 WifiEntry.WifiEntryCallback,onBindViewHolderwifiEntry.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 篇)实车时长未测。