第三章 设备级作用域设计:PhoneScope、TabletScope、WatchScope

在多设备连接场景中,手机、平板、手表各自拥有独立的生命周期、资源约束和功能边界。Hilt 的作用域机制为每一类设备提供专属的依赖容器,确保设备断开时所有关联对象被一次性释放,避免内存泄漏与状态污染。本章围绕 PhoneScope、TabletScope、WatchScope 三个设备级作用域,从边界定义、模块分层到落地验收展开完整设计。

3.1 作用域边界定义

三个设备作用域的生命周期均绑定于对应设备的连接会话:

  • PhoneScope —— 从手机设备建立连接开始,至手机断开连接或应用主动释放为止。容器内持有蓝牙/RF 通信句柄、手机专属配置、通话状态代理等依赖。
  • TabletScope —— 从平板设备配对成功开始,至平板离线或用户切换设备为止。容器管理大屏布局参数、外设控制器、多窗口同步服务等对象。
  • WatchScope —— 从手表激活通信通道开始,至手表进入休眠或解除绑定为止。容器约束最严格,仅缓存低功耗传感器数据、轻量通知代理、心率/步数订阅等最小依赖集。
边界规则: 任一设备作用域内的对象不允许被其他设备作用域直接引用。跨设备数据交换必须通过 DeviceManager 委托,不得绕过作用域边界传递实例。
作用域 生命周期起点 生命周期终点 典型依赖规模 资源敏感度
PhoneScope 手机连接建立 手机断开 / 主动释放 中等(10–20 个绑定) 中
TabletScope 平板配对成功 平板离线 / 设备切换 较大(20–35 个绑定) 低(宽松)
WatchScope 手表通信激活 休眠 / 解除绑定 极小(3–8 个绑定) 高(严格)

3.2 模块分层与组件职责

设备级作用域在模块组织上分为三层,每层只关注单一职责:

  • 设备 Scope 定义层 —— 使用 @Scope 元注解声明三个自定义作用域注解,作为 Hilt 组件的身份标识。
  • 设备 Module 层 —— 每个设备对应一个 @Module,内部按功能拆分 Sub-module(如通信、传感、显示),统一标注所属作用域。
  • 设备 Component 层 —— 每个作用域绑定一个 @Component,声明父组件为 ApplicationComponent,确保应用级单例可被设备组件继承。
组件命名规范: PhoneComponent 只安装 PhoneScope 的 Module;TabletComponent 只安装 TabletScope 的 Module;WatchComponent 只安装 WatchScope 的 Module。禁止交叉安装,违例在编译期即报错。
// 作用域注解声明示例(三个设备各一个)
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneScope {}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface TabletScope {}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface WatchScope {}

3.3 组件职责矩阵

三个组件在职责上遵循“专服专用”原则,下表列出核心职责与禁止行为:

组件 核心职责 禁止行为
PhoneComponent 管理通话状态、手机蓝牙连接、SIM 卡信息代理 持有平板分屏布局、手表传感器订阅
TabletComponent 管理多窗口配置、外设键盘映射、画布同步服务 持有手机通话句柄、手表心率缓存
WatchComponent 管理传感器订阅、轻量通知队列、电池状态监控 持有蓝牙音频流、大屏渲染资源

3.4 数据流与控制流

控制流自上而下由 Application 驱动 DeviceManager 创建或销毁设备组件;数据流自下而上由设备组件内的 Repository 向 ViewModel 推送状态,最终反映到 UI 层。

// 设备 Module 配置示例(PhoneModule)
@Module
@InstallIn(PhoneComponent.class)
public abstract class PhoneModule {
    @PhoneScope
    @Binds
    abstract PhoneConnection providePhoneConnection(PhoneConnectionImpl impl);

    @PhoneScope
    @Provides
    static CallStateAgent provideCallStateAgent() {
        return new CallStateAgent();
    }
}
  1. 控制流:Application.onCreate() → DeviceManager.init() → 检测已配对设备 → 按设备类型创建 PhoneComponent / TabletComponent / WatchComponent。
  2. 数据流:设备回调数据 → Repository(作用域内) → 通过 Flow/LiveData 推送至 ViewModel(作用域内或 ViewModelScoped) → UI 层渲染。
  3. 销毁流:设备断开 → DeviceManager 调用 component.release() → Hilt 自动清理该作用域内所有绑定 → 触发 @Destroy 回调。
风险提示: 如果 ViewModel 被标注为 @ActivityScoped 而非设备作用域,设备断开后 ViewModel 可能持有过期的设备依赖引用。建议将设备相关的 ViewModel 也绑定到对应设备作用域,或使用 @HiltViewModel 配合 SavedStateHandle 主动清除。

3.5 信任链路与安全边界

设备作用域的建立必须经过三重验证,形成信任链路:

  • 设备认证层 —— DeviceManager 在创建组件前校验设备的配对凭证与签名,拒绝未授权设备。
  • 作用域绑定层 —— 组件创建时注入设备唯一标识(deviceId),确保内部所有依赖持有该标识作为上下文。
  • 数据隔离层 —— 同一设备作用域内的 Repository 自动添加 deviceId 过滤,跨设备查询走 DeviceManager 的授权代理。
信任链路完整性: 设备认证通过 → 作用域创建 → 依赖注入 deviceId → 数据层自动隔离。任何一环失败则作用域创建失败,设备标记为不可信。

3.6 选型权衡

维度 独立作用域方案(当前选择) 合并作用域方案 结论
隔离性 强,每个设备独立容器 弱,所有设备共享容器 独立方案胜出
内存开销 中等,每个容器 3–8 KB 元数据 低,单容器 2–3 KB 合并方案略优,但差距可忽略
状态清理 设备断开即整体释放,无残留 需手动注销设备相关绑定,易遗漏 独立方案显著安全
编译期校验 Hilt 编译期检查作用域匹配 运行时条件判断,不可靠 独立方案更早暴露问题
权衡结论: 在多设备场景下,独立作用域带来的隔离收益远大于微小的内存开销。合并方案仅适用于设备数量超过 20 且内存极度受限的嵌入式变体,本课程不推荐。

3.7 风险点与应对

  • 作用域泄漏 —— 设备断开后未及时调用 Component.release()。应对:在 DeviceManager 中建立“断开 → 释放”的强制回调链,配合 @OnDestroy 注解做二次保障。
  • 跨设备依赖误引用 —— 开发者在 Module 中错误注入其他设备的代理。应对:在 Component 的 @Component.Builder 中限定只接收当前设备的 Module 类型,编译期类型校验。
  • WatchScope 资源超限 —— 手表作用域内意外创建了重型对象(如 Bitmap 缓存)。应对:在 WatchModule 中增加 @Provides 方法的耗时/内存断言,CI 门禁拦截超标配置。
  • 碎片化兼容 —— 不同平板厂商的外设实现差异导致 Module 绑定失败。应对:为 TabletScope 预留扩展点,通过 @Qualifier 区分厂商实现,运行时按设备型号选择。

3.8 落地步骤

  1. 定义三个作用域注解 —— 创建 @PhoneScope、@TabletScope、@WatchScope,复制代码段 3.2 中的注解模板。
  2. 创建设备 Component —— 每个作用域对应一个 Hilt 组件,父组件统一指向 ApplicationComponent,并在 @Component.Builder 中声明设备 ID 参数。
  3. 编写设备专属 Module —— 按职责矩阵将通信、传感、显示等绑定分散到不同 @Module,并用 @InstallIn 指定所属组件。
  4. 实现 DeviceManager —— 维护一个 Map<String, DeviceComponent>,key 为设备唯一 ID,value 为组件实例;提供 getOrCreate(deviceId, type) 方法。
  5. 注入点适配 —— 在 ViewModel 或 Fragment 中通过 @Inject 获取设备代理时,确保它们处于正确的设备作用域内。
  6. 断线清理验证 —— 模拟设备断开,检查 DeviceManager 是否在 500ms 内调用 Component.release(),并确认 Hilt 的 Component 被 GC 回收。
关键检查点: 步骤 5 最容易出错。如果 ViewModel 的 @Inject 构造函数中出现了设备作用域依赖,但 ViewModel 本身未被标注为 @PhoneScope(或其他设备作用域),Hilt 会在编译期报错。遇到此错误时,将 ViewModel 也加入对应设备作用域即可。

3.9 验收标准

验收条目 验证方法 预期结果
作用域隔离正确 同时连接手机+手表,在 PhoneScope 内创建的对象在 WatchScope 中不可见 WatchScope 内无法 @Inject PhoneScope 的依赖,编译期报错
设备断开自动释放 断开平板连接,手动触发 GC,观察 TabletComponent 实例 Component 被回收,无泄漏(可使用 LeakCanary 验证)
设备重连状态重置 断开手机后重新连接,检查通话状态代理是否为初始值 PhoneComponent 重新创建,所有状态归零
跨设备数据隔离 手机与平板同时写入同一 Repository(按 deviceId 隔离),查询各自数据 手机只读到手机数据,平板只读到平板数据
WatchScope 资源约束 在 WatchModule 中注入大内存对象,编译期断言拦截 CI 构建失败,提示超出约束上限
验收红线: 任意验收条目不通过,该设备作用域设计不得合并至主分支。尤其是“设备断开自动释放”必须通过 LeakCanary 或 Dump Heap 的 100% 无泄漏证明。
设备级作用域层次结构 设备级作用域层次结构 Application Scope DeviceManager PhoneScope TabletScope WatchScope 设备级作用域边界 每个设备作用域拥有独立容器,生命周期绑定于设备连接会话 跨设备数据交换必须通过 DeviceManager 委托,禁止直接引用

3.10 章节小结

PhoneScope、TabletScope、WatchScope 构成了多设备连接场景下的核心依赖隔离单元。通过独立作用域、严格模块分层和组件职责矩阵,每个设备的依赖树在生命周期内保持纯净,断开时完整释放。信任链路与选型权衡为架构决策提供了依据,落地步骤和验收标准则确保设计可执行、可度量。下一层将在此基础上,进一步探讨设备作用域之间的通信机制与共享状态管理。