App 深色模式切换流程简述(api32)及相关bug

0 阅读5分钟

1. Application 深色模式切换

系统发送 ConfigurationChangeItem

调用到App端后

记住黄色这里部分:

如果App进程状态在Cache状态,则不会立即更新

由于Activity存在OverrideConfig,所以对于Activity的resources这里会OverrideConfig被覆盖,因此Activity的这里不会更新深色模式资源。

资源更新完了之后,来看回调config变化这部分,includeUiContexts参数为false,所以这里也不会回调给Activity。

image.png

  1. 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

image.png

特别提醒:

  1. ConfigurationChangeItem会比ActivityConfigurationChangeItem先发送,即app端会先收到App的config,再收到Activity的config,但是: 进程在Cache状态时即使收到App的config也不会立即处理。

Android13以上版本已变更为:进程在Cache状态时不向其发送App的config,等变为非Cache时再发。

  1. 由于Activity和Application的resource更新机制不一样,在创建系统层级弹框时,不要使用Activity的Context

    1. Activity的生命周期进入后台,resource不会更新。

    2. Activity可能被系统销毁,但由于context被系统层级弹框持有,造成泄漏。

3. WindowContext 深色模式切换

直接binder调用过来

可以看出是和Activity相同的方式更新resource,根据token,替换关联的resourcesImpl

image.png

  1. 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的使用建议:

  1. AppCompatActivity在onConfigurationChanged中,通知View的config变化
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    window.decorView.dispatchConfigurationChanged(resources.configuration)
}
  1. 监听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变化。)