65 · 属性刷新与回调分发 — 数据怎么”活”起来、UI 怎么知道变了
源码基线 f3dif。交叉引用:21-settingslib架构与事件流 §广播总线、34-已配对列表摘要状态机(摘要消费链细节在那篇)、68 §1 2403(图标链)、68 §2 3187(回调污染) 本篇讲清:一个字段变了,经过几跳,屏幕上的像素才变。
0. 总线图:一次属性变化的完整旅程
flowchart LR FW[framework/蓝牙APK<br/>状态变化] -->|广播| BEM[BluetoothEventManager<br/>BroadcastReceiver 收到<br/>查 mHandlerMap 分发 :327-329] BEM -->|handler.onReceive| CBD[CachedBluetoothDevice<br/>onXxxChanged 写字段] CBD -->|值变才调| REFRESH[refresh :877-899<br/>后台预取图标 → 主线程] REFRESH --> DISP[dispatchAttributesChanged :1347-1353<br/>双通道派发] DISP -->|通道①调用线程直调| CB1[Callback.onDeviceAttributesChanged] DISP -->|通道②注册的executor| CB2[Callback.onDeviceAttributesChanged] CB2 --> PREF[NewBluetoothDevicePreference<br/>refreshDeviceUi :154-194] PREF --> PIX[屏幕像素更新]
记住这个数字:从广播到像素,5 跳。 排障时逐跳 grep,哪跳断了修哪跳。
1. 第 1-2 跳:广播进厂,handler 分拣
BluetoothEventManager 的分拣结构是一张表、两本账:
- 一张表:
mHandlerMap(Map<String action, Handler>,BEM:72)——adapter 级和 profile 级广播共用; - 两本账:
mAdapterIntentFilter(:71)与mProfileIntentFilter(:71),两个 receiver 都是同一个内部类BluetoothBroadcastReceiver(:320-332); addHandler(action, handler)(:314-318)= 写表 + 记 adapter 账;addProfileHandler(:196-200)= 写表 + 记 profile 账。
onReceive(:324-329)三步:取 EXTRA_DEVICE → mHandlerMap.get(action) → handler.onReceive(context, intent, device)。查不到 action 就静默丢弃——日志里没有对应输出,这是”广播没到”和”广播到了没人认领”难以区分的原因。
注册时机有个天然时差:adapter receiver 在构造尾(:154)就注册了,profile receiver 要等 updateLocalProfiles() 最后一行(LBPM:287)触发 registerProfileIntentReceiver(:171-173)——profile 广播的监听晚于 adapter 广播。两个 receiver 都显式 RECEIVER_EXPORTED(:187-188)——A17/API37 不写 EXPORTED 会被默认 NOT_EXPORTED 滤掉跨 uid 广播(MIICOSBUG-2930 的教训对照)。
2. 第 3 跳:字段写入与”值变才通知”
CachedBluetoothDevice 的 setter 们遵守一个统一纪律——值变才 dispatch(天然防抖):
| setter | 行号 | 值不变时 |
|---|---|---|
setRssi(short) | :991-996 | 不写不通知(RSSI 抖动不刷 UI) |
setJustDiscovered(boolean) | :901-903 | 同上 |
onActiveDeviceChanged(isActive, profile) | :918-951 | 同上(4 个 active 位) |
setHearingAidInfo | :511 | 写后必 dispatch |
而 onProfileStateChanged / onBondingStateChanged / onAclStateChanged 这类大事件回调则是无条件处理(状态机推进本身就意味着变化)。
2.1 名字与图标:两种截然不同的策略
名字 = 读时取。getName()(:753)每次实时 mDevice.getAlias(),字段里不存名字;refreshName()(:815)只做 dispatch——通知 UI”名字可能变了,自己再查”。setName()(:762)写入后会同步广播给 mMemberDevices 与 mSubDevice(:770-775,组员共用名字)。
图标 = 预取缓存。refresh()(:877-899):
后台线程:isAdvancedDetailsHeader 时,
把 METADATA_MAIN_ICON 的 BitmapDrawable 塞进 mDrawableCache
主线程:dispatchAttributesChanged()
触发点:mHandler 看门狗超时(:200)、onBondingStateChanged 尾部(:1186/:1208)、getDrawableWithDescription(:2603)缓存 miss(:2616)、组管理器换壳路径。消费端 getDrawableWithDescription:缓存命中 → advanced header;miss → rainbow 兜底 + 触发 refresh。
68 §1 MANNPROB-2403 就断在这条链的上游:adapter OFF 窗口内
getBtClass()透传返回 null,MiBluetoothUtils.getBtDrawable(kt:328-353,null 兜底 else 在 :349-350)对 null 走 else 出通用图标——UI 层”本地三态 + CoD 映射”根本拿不到正确的 CoD。
3. 第 4 跳:dispatchAttributesChanged 双通道
// :1347-1353(通道②为原文形态)
public void dispatchAttributesChanged() {
for (Callback callback : mCallbacks) { // 通道①deprecated
callback.onDeviceAttributesChanged(); // 调用线程直调!
}
mCallbackExecutorMap.forEach((callback, executor) -> // 通道②
executor.execute(callback::onDeviceAttributesChanged)); // 指定executor
}三个特性(62 §5 已列,这里补排障视角):
- 事件粒度极粗:
onDeviceAttributesChanged()不带参数——不告诉你哪个属性变了。UI 收到后全量重读(refreshDeviceUi把名字/摘要/图标/bond 全部重查一遍)。简单粗暴,但每次数十次方法调用。 - 双注册双发:同一 callback 走两个 API 注册会收两次。
- 通道①线程不定:调用者通常是 BEM 的广播线程 → 回调落在广播线程,接收方必须自己保证线程安全。
4. 第 5 跳:UI 侧的三层回调注册(谁在哪听)
车机 UI(micarConnectionSettings)在三个层次注册回调,各听各的:
| 层 | 类 | 注册/注销 | 听什么(接口) |
|---|---|---|---|
| Fragment 层 | BluetoothMainEntrySettingsFragment | onStart :216 getEventManager().registerCallback(this) / onStop :223-224 注销 | BluetoothCallback(只实现 onBluetoothStateChanged :158-168,OFF 弹回主页) |
| Controller 层 | BluetoothPreferenceController(基类,:49-50 实现 BluetoothCallback) | onStartInternal :115 / onStopInternal :123 | onDeviceAdded/Deleted、onProfileConnectionStateChanged、onBluetoothStateChanged 等全局事件 |
| 设备条目层 | NewBluetoothDevicePreference | onAttached :116 mCachedDevice.registerCallback(mDeviceCallback) / onDetached :123 注销;换设备先注销旧再注册新(:104-111) | CachedBluetoothDevice.Callback(设备级,单方法),mDeviceCallback = this::refreshDeviceUi(:60) |
设备级回调是”一对一”的:每个列表条目只注册自己设备的 CBD——这层设计本不会跨设备污染。68 §2 MANNPROB-3187 的污染发生在 Controller 层:onProfileConnectionStateChanged(BondedController:459)回调里遍历所有 preference 而不用回调参数过滤——教训”广播型回调必须用回调参数过滤目标”。(修复后 :483-489 已按 cachedDevice.equals(...) 过滤。)
4.1 条目重渲染读什么(refreshDeviceUi :154-194 全清单)
| 读的方法 | 行 | 用途 |
|---|---|---|
getName() | :155 | 标题;:172 无名设备按 mShowDevicesWithoutNames 决定显隐 |
isConnected → 摘要 | :157 → MiCarBluetoothBondedDevicePreference.kt:97-113 | 本地三态文案:已连接 / 正在连接(isBusy && mIsConnectingState)/ 已保存(else 兜底) |
isBusy | :171 | setEnabledWithDebounce(!isBusy && !mIsConnectingState) 300ms 防抖置灰(3205) |
isConnected(缓存 mIsConnected) | :174-175 | 变化时经 OnDeviceStateChanged 通知 controller(:189-193) |
getDevice() → adapter.getBondedDevices().contains(...) | :179 | bond 实时校验——不信 CBD 缓存,直接问 adapter(车机 UI 的防御习惯) |
getBtClass()(经 MiBluetoothUtils.getBtDrawable kt:328-353) | :158-161 | CoD → micarx_icon_phone/earphone_fill/laptop/...;:163-168 统一 tint(132591 修复) |
MiCar 弃用了 AOSP 的两个现成 API:摘要入口
getConnectionSummary()(f3dif CBD 仍存在,:1441 公开版 / :1652 私有重载)与图标链BluetoothUtils.getBtClassDrawableWithDescription(:157)——车机 UI 模块对两者 grep 均 0 命中,摘要走本地三态、图标走自研 CoD 映射(注意:不存在getBtClassDrawableWithFallback这个方法名,别按网上老资料找)。AOSP vs MiCar 摘要设计的完整对比见 34 篇与对比视角文档。
4.2 取数链:Controller 怎么拿设备列表
BluetoothBondedDevicesPrefController.innerUpdateState(:342-390):后台线程 getCachedDeviceManager().getCachedDevicesCopy()(:353-355)→ MiBluetoothUtils.bondedDeviceFilter 过滤(:360;MiBluetoothUtils.kt:189-197——用 adapter.bondedDevices 实时查询过滤,不信任 cachedDevice.bondState,又一次”不信缓存”的防御)→ post 回主线程 removeAll + addPreference 重建。首次打开还有 BLUETOOTH_DATA_LAZY_TIME=200ms 延迟(:327-334)。
5. profile 级事件:与设备级的双轨
除了设备级 dispatchAttributesChanged,profile 状态变化还有第二条轨(64 §4):
StateChangedHandler.onReceiveInternal(LBPM:344-453)
→ cachedDevice.refresh()(:449)
→ mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, state, profileId)(:450-451)
→ 遍历 BluetoothCallback.onProfileConnectionStateChanged(BEM:232-254)
两轨并行:设备级(粗粒度,条目自刷)+ profile 级(带 profileId 和设备引用,Controller 用它驱动”正在连接”状态)。3187 案的 mIsConnectingState 就是 Controller 在 profile 级轨道上维护的旁路状态位(34 篇 §双通道)。
电量是第三条小轨:mBatteryMetadataListener(:204-224)监听 12 种电量 metadata key 变化 → 也汇入 dispatchAttributesChanged(:222)。
6. 排障:五跳逐断点法
| 症状 | 先查哪跳 | 锚点 |
|---|---|---|
| UI 完全不动 | 第 1 跳:广播到没到 | bugreport grep action 字符串;“无条件日志零命中 = 事件未送达”(68 §6 2960 的铁证法) |
| 广播到了设备没变 | 第 2 跳:handler 认领了吗 | mHandlerMap 静默丢弃无日志;看 handler 里第一条 log |
| 字段变了 UI 不刷 | 第 3-4 跳:值变守卫/双通道 | 确认值真的变了(防抖拦截);回调注册没注册(onAttached 配对) |
| 条目刷了内容不对 | 第 5 跳:读的方法对不对 | 68 §1 2403:读的是透传方法且时机在 OFF 窗口 |
| 别的设备被连累刷新 | 第 5 跳:回调过滤了吗 | 3187 模式:Controller 遍历全体无过滤 |
7. 自测
- 从广播到像素几跳?各跳在哪?(BEM 收 → handler → CBD 字段 → refresh/dispatch → UI 回调 → 渲染)
- 名字和图标的缓存策略有何不同?(读时取 vs 预取进 LruCache)
-
onDeviceAttributesChanged带不带参数?UI 怎么应对?(不带;全量重读) - UI 三层回调分别在哪个类注册、听什么?(Fragment/Controller/设备条目)
- 车机 UI 有哪两处”不信 CBD 缓存、直接问 adapter”?(bondedDeviceFilter、条目 bond 实时校验 :179)
- 设备级与 profile 级两条通知轨各驱动什么?(条目自刷 / Controller 旁路状态位)
下一篇 66-bond状态与自动回连:配对状态机 + 那些耐心窗口的完整机制。