1. Application 深色模式切换
系统发送 ConfigurationChangeItem
调用到App端后
记住黄色这里部分:
如果App进程状态在Cache状态,则不会立即更新
由于Activity存在OverrideConfig,所以对于Activity的resources这里会OverrideConfig被覆盖,因此Activity的这里不会更新深色模式资源。
资源更新完了之后,来看回调config变化这部分,includeUiContexts参数为false,所以这里也不会回调给Activity。
- Activity的OverrideConfig从哪里来的?
启动Activity时,系统在LaunchActivityItem中传来的,创建Activity的context时就会使用这个overrideConfig创建Resources
2. Activity 深色模式切换
系统发送ActivityConfigurationChangeItem
注意:当Activity在后台时,系统不会发送这个通知(较低Android版本是先发送,但app进程收到后暂不处理,等到前台时再处理)
调用到App端后
重点看一下updateResourcesForActivity这个方法:
- 会根据activityToken,找出关联的activityResource、resources
- 会根据overrideConfig和activityResource创建新的ResourcesKey
- 根据newKey,查找/创建(如果缓存中没有找到)resourcesImpl
- 把resourcesImpl设置到resources中
可以看出,Activity的resources更新,和Application不一样:
- Application是resourcesImpl不变,在resourcesImpl内部更新Assets;
- Activity是替换新的resourcesImpl
特别提醒:
- ConfigurationChangeItem会比ActivityConfigurationChangeItem先发送,即app端会先收到App的config,再收到Activity的config,但是: 进程在Cache状态时即使收到App的config也不会立即处理。
Android13以上版本已变更为:进程在Cache状态时不向其发送App的config,等变为非Cache时再发。
-
由于Activity和Application的resource更新机制不一样,在创建系统层级弹框时,不要使用Activity的Context。
-
Activity的生命周期进入后台,resource不会更新。
-
Activity可能被系统销毁,但由于context被系统层级弹框持有,造成泄漏。
-
3. WindowContext 深色模式切换
直接binder调用过来
可以看出是和Activity相同的方式更新resource,根据token,替换关联的resourcesImpl
- WindowContext与其他Context,调用registerComponentCallbacks的区别
WindowContext内部维护了一个ComponentCallbacksController,注册时会注册到这个mCallbacksController,
收到newConfig时直接分发出去。
ContextWrapper没有重写这个方法,Context是默认注册到Application中了。
因此如果使用ContextWrapper包装WindowContext,再去registerComponentCallbacks,也会注册到Application。
如果想注册到WindowContext中,必须先找到WindowContext对象,用WindowContext直接注册。
fun Context.findWindowContext(): Context? {
if (this.javaClass.name == "android.window.WindowContext") {
return this
}
if (this is ContextWrapper) {
return baseContext.findWindowContext()
}
return null
}
4. 使用AppCompatActivity时bug: 深色模式异常
已在Android12,13,14,15等版本测试验证,全部存在问题,必现。(其他版本未测试)
复现步骤:应用A在AppCompatActivity的onConfigurationChanged时使用当前context重新加载资源,把应用A退到后台进入stop状态,再打开几个其他应用,使应用A进程进入cache状态,然后切换深色模式,之后再回到应用A。
前面在介绍Application的切换流程中讲到:如果App进程状态在Cache状态,则不会立即更新资源。
当App退到后台进入Cache状态后,再启动Activity回到前台,App进程状态变为非Cache状态后才会更新Application的资源。
实践中发现:在Cache状态,切换深色模式,然后再启动Activity到前台时,系统会先通知Activity config变化,然后再更新进程状态为非Cache。
这种场景下:
Activity 先更新资源,回调Activity的config变化;然后再更新Application资源,回调Application的config变化。
而AppCompatActivity在onConfigurationChanged时默认又会读取Application中config的uimode
(此时由于进程状态还在cache,Application的config还没有更新),
然后把Activity的resource的深色模式更新成Application一样,造成Activity的深色模式错误。
(先走原生流程更新成正确的资源,然后经过AppCompatDelegate又更新回旧的模式了)
等到后续Application中config更新后,也没有再次通知AppCompatActivity更新,导致AppCompatActivity深色模式一直是错误的状态。
当AppCompatActivity进入后台再回来时,经过onStart,这里会再次更新深色模式(与上面流程一样,默认更新成与Application一致)
那么这里会不会就恢复正常了呢? 其实并不完全是。
可以看到更新后的结果只会通知Activity,并不会通知View.
(很多深色模式切换的逻辑是写View的onConfigurationChanged中,因此View并没有真正换肤)
因此对于AppCompatActivity的使用建议:
- AppCompatActivity在onConfigurationChanged中,通知View的config变化
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
window.decorView.dispatchConfigurationChanged(resources.configuration)
}
- 监听Application的config变化,然后通知AppCompatActivity更新
import android.app.Activity
import android.app.Application
import android.content.ComponentCallbacks
import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
class AppCompatActivityPatch : Application.ActivityLifecycleCallbacks, ComponentCallbacks {
private val atLeastStartedActivityList = mutableListOf<AppCompatActivity>()
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
}
override fun onActivityStarted(activity: Activity) {
if (activity is AppCompatActivity) {
atLeastStartedActivityList.add(activity)
}
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
if (activity is AppCompatActivity) {
atLeastStartedActivityList.remove(activity)
}
}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle
) {
}
override fun onActivityDestroyed(activity: Activity) {
}
override fun onConfigurationChanged(newConfig: Configuration) {
atLeastStartedActivityList.forEach { activity ->
val localNightMode = activity.delegate.localNightMode
val nightMode =
if (localNightMode != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED) localNightMode else AppCompatDelegate.getDefaultNightMode()
if (nightMode == AppCompatDelegate.MODE_NIGHT_UNSPECIFIED || nightMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) {
activity.delegate.applyDayNight()
}
}
}
override fun onLowMemory() {
}
}
5. 更新资源导致的颜色异常(updateConfiguration)
updateConfiguration,这个方法其实早已废弃不推荐使用了,但是发现目前还有不少人在用,而且使用这个修改config之后,可能会导致读取资源错误。例如强制改深色模式,但是后续读出来还是有部分浅色模式的资源。
这是因为:config更新之后没有刷新主题
可以参考AppCompatDelegateImpl中的处理逻辑,强制刷新主题.
但是对于一般的context,我们是不知道当前themeResId的,查看Context源码,里面存在getThemeResId方法,public的,但是hide了,因此我们可以反射获取。
fun reapplyTheme(context: Context) {
val methodGetThemeResId = Class.forName("android.content.Context").getDeclaredMethod("getThemeResId")
val themeResId: Int = methodGetThemeResId.invoke(context) as Int
context.setTheme(themeResId)
context.theme.applyStyle(themeResId, true)
}
另外需要注意的点:
updateConfiguration,修改的是resource,可能有多个Context都共用这一个Context,因此需要每个Context都刷新一下主题。
比如基于一个context创建的各自ContextThemeWrapper,在没有OverrideConfiguration时,他们的resource都是同一个,这种情况下,修改了一个resource,影响了很多个Context。
而resource被context单向持有,可能无法根据resource找出间距影响了哪些Context。 (除非你能明确知道updateConfiguration之后影响了那些context,并且及时给使用了这个resource加载资源的view通知config变化。)