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_DEVICEmHandlerMap.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)写入后会同步广播给 mMemberDevicesmSubDevice(: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 已列,这里补排障视角):

  1. 事件粒度极粗onDeviceAttributesChanged() 不带参数——不告诉你哪个属性变了。UI 收到后全量重读(refreshDeviceUi 把名字/摘要/图标/bond 全部重查一遍)。简单粗暴,但每次数十次方法调用。
  2. 双注册双发:同一 callback 走两个 API 注册会收两次。
  3. 通道①线程不定:调用者通常是 BEM 的广播线程 → 回调落在广播线程,接收方必须自己保证线程安全

4. 第 5 跳:UI 侧的三层回调注册(谁在哪听)

车机 UI(micarConnectionSettings)在三个层次注册回调,各听各的:

注册/注销听什么(接口)
Fragment 层BluetoothMainEntrySettingsFragmentonStart :216 getEventManager().registerCallback(this) / onStop :223-224 注销BluetoothCallback(只实现 onBluetoothStateChanged :158-168,OFF 弹回主页)
Controller 层BluetoothPreferenceController(基类,:49-50 实现 BluetoothCallback)onStartInternal :115 / onStopInternal :123onDeviceAdded/DeletedonProfileConnectionStateChangedonBluetoothStateChanged 等全局事件
设备条目层NewBluetoothDevicePreferenceonAttached :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:171setEnabledWithDebounce(!isBusy && !mIsConnectingState) 300ms 防抖置灰(3205)
isConnected(缓存 mIsConnected):174-175变化时经 OnDeviceStateChanged 通知 controller(:189-193)
getDevice()adapter.getBondedDevices().contains(...):179bond 实时校验——不信 CBD 缓存,直接问 adapter(车机 UI 的防御习惯)
getBtClass()(经 MiBluetoothUtils.getBtDrawable kt:328-353):158-161CoD → 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状态与自动回连:配对状态机 + 那些耐心窗口的完整机制。