一行代码引发的疑问
写 Android 12 启动画面时,很多人都会顺手加上这行代码,但对它的工作原理其实并不清楚——尤其它会让人误以为这是”响应式监听”。这篇文章把它拆开讲清楚。
splashScreen.setKeepOnScreenCondition { !webViewReady }
这行代码看起来像在说:”盯着 webViewReady,它一变你就自动隐藏启动画面。” 于是很自然就会产生一个疑问:SplashScreen 是怎么知道 webViewReady 变了、并把自己隐藏掉的?它既没有传 Listener,webViewReady 也不是什么特殊类型,就是个普通 var。
答案可能和你想的不一样:它根本没有”监听”任何东西,而是在每一帧绘制前反复”轮询”。
一个常见的误解:以为它是响应式的
如果你刚接触 Compose,很容易把它和 mutableStateOf 的机制混为一谈。先明确一点:
private var webViewReady = false // 普通 Kotlin 字段
这不是 mutableStateOf,也不是 StateFlow,它变化时不会触发任何重组、任何回调、任何通知。setKeepOnScreenCondition 和 Compose 的响应式机制之间,没有任何关系。
那 SplashScreen 是怎么”感知”到它变化的?答案在于传入的到底是什么。
传入的其实是一个”条件函数”
看方法签名:
fun setKeepOnScreenCondition(condition: KeepOnScreenCondition)
// typealias KeepOnScreenCondition = () -> Boolean
你传进去的是一个 lambda(函数),不是值。它的语义是:
我把”判断要不要继续显示启动画面”这件事,交给你这个函数。我每需要判断一次,就调用你一次;返回 true 就继续显示,返回 false 就退出。
{ !webViewReady } 翻译过来就是:
webViewReady还是 false → 返回 true → 继续显示启动画面webViewReady变成 true → 返回 false → 可以退出了
到这里,问题的关键就只剩一个:库”每需要判断一次”的频率是多少?
核心机制:每一帧绘制前都查一次
core-splashscreen 库的做法是,在 Activity 的 decorView 上注册一个 ViewTreeObserver.OnPreDrawListener(”每一帧即将绘制”的回调),然后在里面反复调用你传的这个 lambda:
每一帧绘制前:
result = condition() // 即调用 { !webViewReady }
├─ result == true → 取消这一帧绘制,启动画面继续挂着
└─ result == false → 放行这一帧,执行退出动画,移除启动画面
OnPreDrawListener.onPreDraw() 的返回值本身就有这个语义:返回 false 会取消本次绘制,等下一个 vsync 信号再问一次。库正是利用了这个机制——只要条件不满足,就一直”拦”住这一帧;直到条件满足,才放行并让启动画面退出。
所以答案是:它不是装了个”门铃”(变量一变就响),而是派了个保安每 16ms 扫一眼你门口那盏灯——灯从红变绿的下一瞬间,保安就看到了,然后放行。
时序串起来看
class MainActivity : ComponentActivity() {
private var ready = false
override fun onCreate(savedInstanceState: Bundle?) {
val splash = installSplashScreen()
super.onCreate(savedInstanceState)
splash.setKeepOnScreenCondition { !ready }
// 假设这是个异步加载,完成后把 ready 置 true
loadSomethingAsync {
ready = true // ← 灯变绿(仅此而已,没有通知任何"监听者")
}
}
}
帧 1:condition() → !false = true → 取消绘制,启动画面继续
帧 2:condition() → !false = true → 取消绘制,启动画面继续
...(异步加载中,ready 仍是 false)
loadSomethingAsync 完成 → ready = true
帧 N:condition() → !true = false → 放行,启动画面隐藏
启动画面的隐藏,就发生在 ready 置 true 之后的下一帧。
这也能解释:为什么”超时兜底”同样能生效
既然它只是每帧查一次值,那它根本不关心这个值是”加载完成”还是”超时”改的。所以一个常见的防卡死写法——给启动画面加超时上限——只需直接改这个字段即可:
splash.setKeepOnScreenCondition { !ready }
// 兜底:不管加载多慢,最多 3 秒强制放行
window.decorView.postDelayed({ ready = true }, 3000)
3 秒后 ready 被置 true,下一帧 preDraw 一查,!true = false,启动画面照样退出。它只认每次查到的结果,不认结果是怎么来的。
一句话总结
setKeepOnScreenCondition 传入的是一个”条件函数”,core-splashscreen 会在每一帧绘制前反复调用它来决定是否继续显示启动画面。所以它既不是响应式监听,也不需要你手动通知——改一个普通的 var 就够了,下一帧轮询自然就生效。
延伸
想验证的话,可以去翻 core-splashscreen 的源码,重点看 SplashScreenViewProvider 与 OnPreDrawListener 那一段,核心就是”preDraw 拦截 + 每帧检查条件”这一套。
