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

1. Application 深色模式切换

系统发送 ConfigurationChangeItem

调用到App端后

记住黄色这里部分:

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

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

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

  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

特别提醒:

  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

  1. WindowContext与其他Context,调用registerComponentCallbacks的区别

WindowContext内部维护了一个ComponentCallbacksController,注册时会注册到这个mCallbacksController,

收到newConfig时直接分发出去。

ContextWrapper没有重写这个方法,Context是默认注册到Application中了。

因此如果使用ContextWrapper包装WindowContext,再去registerComponentCallbacks,也会注册到Application。

如果想注册到WindowContext中,必须先找到WindowContext对象,用WindowContext直接注册。

kotlin 复制代码
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变化
kotlin 复制代码
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    window.decorView.dispatchConfigurationChanged(resources.configuration)
}
  1. 监听Application的config变化,然后通知AppCompatActivity更新
kotlin 复制代码
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了,因此我们可以反射获取。

kotlin 复制代码
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变化。)

相关推荐
峥嵘life2 小时前
深耕Android技术——2025年CSDN博客之星总评选深度总结
android
GoldenPlayer2 小时前
Android网络请求报错(直接请求http)
android
花卷HJ2 小时前
Android 多媒体文件工具类封装(MediaFileUtils)
android·java
csj502 小时前
安卓基础之《(11)—数据存储(1)共享参数SharedPreferences》
android
走在路上的菜鸟2 小时前
Android学Dart学习笔记第二十七节 异步编程
android·笔记·学习·flutter
哆啦安全2 小时前
Android智能调试分析工具V7.5
android
モンキー・D・小菜鸡儿2 小时前
Android 自定义粒子连线动画视图实现:打造炫酷背景效果
android·java
lxysbly2 小时前
安卓 PS1 模拟器,手机上也能玩经典 PlayStation 游戏
android·游戏·智能手机
sheji34162 小时前
【开题答辩全过程】以 基于安卓平台的景点导游系统的设计与实现为例,包含答辩的问题和答案
android