第三章 设备级作用域设计: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();
}
}
- 控制流:Application.onCreate() → DeviceManager.init() → 检测已配对设备 → 按设备类型创建 PhoneComponent / TabletComponent / WatchComponent。
- 数据流:设备回调数据 → Repository(作用域内) → 通过 Flow/LiveData 推送至 ViewModel(作用域内或 ViewModelScoped) → UI 层渲染。
- 销毁流:设备断开 → 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 落地步骤
- 定义三个作用域注解 —— 创建
@PhoneScope、@TabletScope、@WatchScope,复制代码段 3.2 中的注解模板。 - 创建设备 Component —— 每个作用域对应一个 Hilt 组件,父组件统一指向
ApplicationComponent,并在@Component.Builder中声明设备 ID 参数。 - 编写设备专属 Module —— 按职责矩阵将通信、传感、显示等绑定分散到不同
@Module,并用@InstallIn指定所属组件。 - 实现 DeviceManager —— 维护一个
Map<String, DeviceComponent>,key 为设备唯一 ID,value 为组件实例;提供getOrCreate(deviceId, type)方法。 - 注入点适配 —— 在
ViewModel或Fragment中通过@Inject获取设备代理时,确保它们处于正确的设备作用域内。 - 断线清理验证 —— 模拟设备断开,检查
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% 无泄漏证明。
3.10 章节小结
PhoneScope、TabletScope、WatchScope 构成了多设备连接场景下的核心依赖隔离单元。通过独立作用域、严格模块分层和组件职责矩阵,每个设备的依赖树在生命周期内保持纯净,断开时完整释放。信任链路与选型权衡为架构决策提供了依据,落地步骤和验收标准则确保设计可执行、可度量。下一层将在此基础上,进一步探讨设备作用域之间的通信机制与共享状态管理。