ViewModel初始化链路追踪:ViewModelProvider、Factory、构造函数耗时

当屏幕切换出现明显“白屏”或“黑屏”时,问题往往不在布局渲染,而在于ViewModel创建链阻塞了主线程。本章将沿着 ViewModelProvider.get() → Factory.create() → ViewModel.构造函数 这条链路逐级排查,定位延迟的精确阶段。

现象识别

  • 用户感知:导航至目标页面后,界面长时间显示空白或加载指示器(>200ms),随后才呈现完整 UI。
  • 系统响应:系统连续发送多次 InputMissed 或 DroppedFrame 通知;严重时直接弹出 ANR 对话框。
  • 堆栈特征:从 StrictMode 或 Trace 抓取的主线程堆栈中,频繁出现 ViewModelProvider.get()、ViewModelProvider.Factory.create() 或 YourViewModel.() 方法调用。
⚠ 易混淆点: 不要将“数据加载延迟”与“ViewModel 初始化延迟”混为一谈。前者是网络/数据库请求耗时,后者是对象创建本身阻塞主线程。本课程聚焦后者,但初始化链路常成为前者的“帮凶”。

数据采集

我们需要通过 3 个埋点来精确测量链路中各阶段的耗时。以下代码展示了如何在 Factory 和 构造函数 中插入 Trace 分段标记:

// 自定义 Factory 中的埋点示例
class LoggingFactory(
    private val userRepository: UserRepository,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        Trace.beginSection("Factory.create:${modelClass.simpleName}")
        val startTime = System.nanoTime()

        // 模拟真实创建逻辑
        val viewModel = when {
            modelClass.isAssignableFrom(UserViewModel::class.java) -> {
                UserViewModel(userRepository, ioDispatcher) as T
            }
            else -> throw IllegalArgumentException("Unknown class ${modelClass.name}")
        }

        val durationMs = (System.nanoTime() - startTime) / 1_000_000f
        Trace.endSection()

        // 通过 Logcat 输出耗时数据
        Log.d("ViewModelTrace", "[${modelClass.simpleName}] " +
                "Factory.create() took ${durationMs}ms")
        return viewModel
    }
}

// ViewModel 构造函数内的埋点示例
class UserViewModel(
    private val userRepository: UserRepository,
    private val ioDispatcher: CorollaryDispatcher
) : ViewModel() {

    init {
        Trace.beginSection("UserViewModel.init")
        val startTime = System.nanoTime()

        // 假设这里模拟了阻塞初始化
        // Thread.sleep(80) // 80ms 的阻塞调用

        val durationMs = (System.nanoTime() - startTime) / 1_000_000f
        Trace.endSection()
        Log.d("ViewModelTrace", "[UserViewModel] " +
                "Constructor/init block took ${durationMs}ms")
    }
}

采集指标清单:

阶段 采集工具 关键指标
ViewModelProvider.get() 系统 FrameMetrics / Android Vitals 主线程“冻结”总时长 > 16ms 即标记异常
Factory.create() 手动 Trace + Logcat 耗时 > 50ms 表示 Factory 内存在阻塞操作
ViewModel 构造函数 手动 Trace + Logcat 耗时 > 16ms 说明构造函数内有高频计算或锁竞争
init 块 / 协程启动 手动 Trace + StrictMode 出现 Thread.sleep() 或 runBlocking 即违规

原因定位

使用故障树分析法逐层下钻,快速定位阻塞根源。

ViewModel 初始化延迟故障树 ViewModel 初始化延迟 Factory.create() 耗时阻塞 构造函数 / init 块阻塞 DI 作用域/注入链路异常 create() 内执行网络/DB 大型对象解码/序列化 runBlocking 阻塞主线程 冻结的 Rx / 旧式回调 @Singleton 过作用域 Hilt 模块循环依赖 判断规则:首先对比 Factory.create() 与构造函数耗时。 若构造函数耗时 > 16ms,即诊断为“构造函数阻塞”。

判断规则:

  1. 优先对比 Factory 与构造函数耗时: 若 Factory.create() 耗时显著高于构造函数(例如 Factory 耗时 200ms vs 构造函数 5ms),则问题主要集中在 Factory 内的业务逻辑。
  2. 检查构造函数内的阻塞 API: 通过 StrictMode.getThreadPolicy() 或字节码查看是否存在 Thread.sleep()、CountDownLatch.await()、runBlocking 以及 BitmapFactory.decodeResource 等高频主线程调用。
  3. 利用“排查表”快速定责: 结合下方流程表,对照采集到的 Trace 数据,直接锁定责任阶段。
排查阶段 数据来源 判定标准 诊断结果
阶段 1 Factory.create() 耗时 > 50ms Factory 内存在主线程 I/O 或计算密集型任务
阶段 2 ViewModel 构造函数耗时 > 16ms 构造函数中存在阻塞调用或高复杂度初始化
阶段 3 init 块内部协程启动 使用了 runBlocking 立即报错,必须替换为 viewModelScope.launch

修复动作

🎯 核心原则: ViewModel 的创建必须做到“轻量同步”,即构造函数中只进行字段赋值,绝不执行任何可能阻塞的调用。所有耗时操作应推迟到 viewModelScope.launch 或协程中异步执行。
  1. 针对 Factory.create() 阻塞:
    • 将 Factory 内的网络请求、数据库查询、文件读写等操作移至 Repository 层,并通过协程 async 启动,在 ViewModel 的 init 块中 await。
    • 若必须同步获取数据(如参数合并),使用 runBlocking 的替代方案:SavedStateHandle 或 AssistedInject 传递原始 ID,在 ViewModel 内异步加载。
  2. 针对构造函数阻塞:
    • 移除所有 Thread.sleep()、LockSupport.park()、Object.wait() 等阻塞调用。
    • 将大型对象初始化(如 Bitmap、大型Array)延迟至 UI 显示之后,或使用 懒加载委托 lazy { } 包裹。
  3. 针对 DI 异常:
    • 检查 Hilt/Dagger 模块作用域,避免将 @Singleton 作用域用于 ViewModel 独有的依赖;优先使用 @HiltViewModel 或 @ViewModelScoped。
    • 避免在 DI 工厂内进行复杂的对象图计算;遇到循环依赖错误时,使用 Provider<T> 或 Lazy<T> 进行惰性注入。

复测

完成修复后,重复“数据采集”步骤,对比以下指标:

指标 修复前(示例值) 修复后(目标值)
Factory.create() 耗时 180ms < 5ms
构造函数 + init 块耗时 90ms < 2ms
从点击到界面渲染总时长 1200ms < 300ms

确保使用 Macrobenchmark 或 手动冷启动测试 验证至少 5 次,取中位数作为最终结果。

预防

📋 团队约定与自动化检查:
  • 架构决策记录 (ADR): 写入团队章程 —— “ViewModel 构造函数必须为同步操作,耗时 < 1ms。所有异步数据流一律使用 viewModelScope 或 lifecycleScope 启动”。
  • Lint 自定义规则: 添加 @MainThread 限制,在 CI 阶段扫描 ViewModel 构造函数是否存在 runBlocking、Thread.sleep、java.io 等包引用。
  • Code Review 检查清单:
    1. Factory.create() 内是否有网络/DB 调用?若有,驳回。
    2. init 块是否启动了未取消的全局协程?应使用 viewModelScope。
    3. 是否使用了 lazy { } 延迟重对象?