Android | Configuration详解及其新旧更新方式对比

一、Configuration介绍

Configuration主要用于描述当前设备的各种配置信息,比如屏幕尺寸、方向、字体缩放、语言、UI模式等。系统会在这些配置发生变化时(比如旋转屏幕、切换语言、调整字体大小等)自动通知应用,从而触发 Activity 的 onConfigurationChanged() 或重新创建 Activity。Configuration通常作用如下:

a、描述设备当前状态,如语言、屏幕方向、密度、输入设备等; b、辅助资源选择,决定加载哪些资源(如 layout-sw600dp、values-zh-rCN 等); c、监听配置变更,当配置变化时应用可做出响应; d、设置/覆盖配置,可以用来自定义展示行为(如禁止字体跟随系统变化)。

常用字段解释(部分)

字段名 含义
fontScale 字体缩放比例,例如 1.0 表示正常字体,1.5 表示放大 50%
orientation 屏幕方向:ORIENTATION_PORTRAIT(竖屏)、ORIENTATION_LANDSCAPE(横屏)
screenWidthDp 当前屏幕的可用宽度(单位:dp)
screenHeightDp 当前屏幕的可用高度(单位:dp)
smallestScreenWidthDp 最小宽度(设备在任意方向下的最小宽度)
locale 当前语言环境(如 zh_CN、en_US)
mcc 移动网络运营商的国家代码 如:460 → 中国、310 → 美国、440 → 日本
mnc 运营商代码 如:11 代表中国电信、01 代表中国联通、00 代表中国移动
screenLayout 屏幕的尺寸、长宽比、方向等组合字段
uiMode UI 模式,如夜间模式、车载模式、电视模式等
densityDpi 屏幕的像素密度(DPI),即每英寸像素数。一些常见的DPI级别: ldpi(120dpi)、mdpi(160dpi)、hdpi(240dpi)、xhdpi(320dpi)、xxhdpi(480dpi)、xxxhdpi(640dpi)

二、Configuration更新

2.1、getResources().updateConfiguration(configuration, displayMetrics)

updateConfiguration()方法中除了configuration,还有个displayMetrics。DisplayMetrics 用于描述屏幕显示属性(如像素宽高、密度、dpi 等)的工具类,常用于适配 UI 显示和计算尺寸。比如dp、sp和px之间的转换,就会用到DisplayMetrics:

kotlin 复制代码
/**
 * 将sp值转换为px值,保证文字大小不变
 */
fun sp2px(context: Context, spValue: Float): Int {
    val fontScale = context.resources.displayMetrics.scaledDensity
    return (spValue * fontScale + 0.5f).toInt()
}

fun dp2px(context: Context, dp: Float): Int {
    val scale = context.resources.displayMetrics.density
    return (dp * scale + 0.5f).toInt()
}

sp会跟随系统字体大小设置变化,dp不会。比如现在想全局设置项目中字体不跟随系统大小设置变化,可以如下设置:

kotlin 复制代码
fun unableFontSizeChange(context: Context) {
    val res = context.resources
    val configuration = res.configuration.apply {
        if (fontScale != 1.0f) {
            //强制设置文字大小不跟随系统字体变化
            fontScale = 1.0f
        }
    }
    //更新configuration
    res.updateConfiguration(configuration, res.displayMetrics)
}

这样调用之后就会全局设置了。咦?等会!这里好像只改变了configuration.fontScale,而没有修改displayMetrics.scaledDensity这个值呀,难道设置configuration#fontSize也会影响displayMetrics.scaledDensity的值吗?带着这个疑问,我们看下updateConfiguration的源码。getResources().updateConfiguration()内部最终会调用到ResourcesImpl#updateConfiguration()

kotlin 复制代码
//Resources.java
private ResourcesImpl mResourcesImpl;

public void updateConfiguration(Configuration config, DisplayMetrics metrics) {
    updateConfiguration(config, metrics, null);
}

public void updateConfiguration(Configuration config, DisplayMetrics metrics,
CompatibilityInfo compat) {
    mResourcesImpl.updateConfiguration(config, metrics, compat);
}
scss 复制代码
//ResourcesImpl.java
public void updateConfiguration(Configuration config, DisplayMetrics metrics, CompatibilityInfo compat) {
    try {
        synchronized (mAccessLock) {
            // 更新 DisplayMetrics,会影响 dp、sp 单位的计算
            if (metrics != null) {
                mMetrics.setTo(metrics);
            }

            // 比较配置变更类型(比如 locale、fontScale、屏幕方向)
            final @Config int configChanges = calcConfigChanges(config);

            // 如果 locales 为空,设置默认 locale
            LocaleList locales = mConfiguration.getLocales();
            if (locales.isEmpty()) {
                locales = LocaleList.getDefault();
                mConfiguration.setLocales(locales);
            }

            // 1、Locale改变:重新匹配最合适的语言
            if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
            // 如果 locale 数量 > 1,尝试匹配最佳可用 locale
            if (locales.size() > 1) {
                String[] availableLocales = mAssets.getNonSystemLocales();
                if (LocaleList.isPseudoLocalesOnly(availableLocales)) {
                    availableLocales = mAssets.getLocales(); // 使用系统语言列表
                }

                if (availableLocales != null) {
                    final Locale bestLocale = locales.getFirstMatchWithEnglishSupported(availableLocales);
                    if (bestLocale != null && bestLocale != locales.get(0)) {
                        mConfiguration.setLocales(new LocaleList(bestLocale, locales));
                    }
                }
            }
        }

            // 2、设置密度,影响 dp 计算
            if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
                mMetrics.densityDpi = mConfiguration.densityDpi;
                mMetrics.density = mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
            }

            // 3、设置字体缩放密度 scaledDensity,影响 sp 字体大小计算
            mMetrics.scaledDensity = mMetrics.density *
                    (mConfiguration.fontScale != 0 ? mConfiguration.fontScale : 1.0f);

            // 4、设置AssetManager的配置信息(语言、方向、尺寸等)
            mAssets.setConfiguration(
                mConfiguration.mcc,
                mConfiguration.mnc,
                adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
                mConfiguration.orientation,
                mConfiguration.touchscreen,
                mConfiguration.densityDpi,
                mConfiguration.keyboard,
                // 软键盘状态
                (mConfiguration.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO &&
                        mConfiguration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES)
                ? Configuration.KEYBOARDHIDDEN_SOFT
            : mConfiguration.keyboardHidden,
            mConfiguration.navigation,
            // 宽高像素
            Math.max(mMetrics.widthPixels, mMetrics.heightPixels),
            Math.min(mMetrics.widthPixels, mMetrics.heightPixels),
            // 屏幕 dp 相关
            mConfiguration.smallestScreenWidthDp,
            mConfiguration.screenWidthDp,
            mConfiguration.screenHeightDp,
            mConfiguration.screenLayout,
            mConfiguration.uiMode,
            mConfiguration.colorMode,
            Build.VERSION.RESOURCES_SDK_INT
            );

            // 5、清空各种资源缓存(Drawable、颜色、动画等),确保资源按新 config 重新加载
            mDrawableCache.onConfigurationChange(configChanges);
            mColorDrawableCache.onConfigurationChange(configChanges);
            mComplexColorCache.onConfigurationChange(configChanges);
            mAnimatorCache.onConfigurationChange(configChanges);
            mStateListAnimatorCache.onConfigurationChange(configChanges);
            flushLayoutCache();
        }
    }
}
}

这个ResourcesImpl通常是被全局共享,所以ResourcesImpl#updateConfiguration()会影响整个App里的资源加载、字体大小、语言环境等 。看上面3处的代码 mMetrics.scaledDensity = mMetrics.density * (mConfiguration.fontScale != 0 ? mConfiguration.fontScale : 1.0f),果然,Configuration.fontScale会影响mMetrics.scaledDensity 的最终生成,当mConfiguration.fontScale不变时,mMetrics.scaledDensity也不会发生变化了。

2.2、Context.createConfigurationContext()

createConfigurationContext(Configuration overrideConfiguration) 是基于原始 Context 创建一个带有指定Configuration的新Context 实例。这个新 Context 中的 Resources 会根据我们传入的 overrideConfiguration 生效,关键代码如下:

kotlin 复制代码
//ContextImpl.java
public Context createConfigurationContext(Configuration overrideConfiguration) {
    //...省略其他部分...
    //创建新的Context
    ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams,
        mAttributionSource.getAttributionTag(),
        mAttributionSource.getNext(),
        mSplitName, mToken, mUser, mFlags, mClassLoader, null);
    context.mIsConfigurationBasedContext = true;

    final int displayId = getDisplayId();
    final Integer overrideDisplayId = mForceDisplayOverrideInResources
    ? displayId : null;

    //给Context设置Resources
    context.setResources(createResources(mToken, mPackageInfo, mSplitName, overrideDisplayId,
        overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo(),
        mResources.getLoaders()));
    return context;
}

新的Context并不是修改了原来的Configuration,而是生成了一个新的带配置Configuration的上下文环境,使用示例:

kotlin 复制代码
override fun attachBaseContext(newBase: Context?) {
    val config = Configuration(newBase?.resources?.configuration).apply {
        fontScale = 1.0f // 固定字体大小
    }
    val context = newBase?.createConfigurationContext(config)
    super.attachBaseContext(context)
}

createConfigurationContext() 不会影响全局 Resources,只是针对这个新 Context 的资源发生变化,是局部生效的

2.3、两者对比

通过一个示例来看下两者的区别,先给出4个对应的中文、英文资源:

第一个页面里引用了前两个文案,代码如下:

kotlin 复制代码
class MutableContextWrapperFragment : BaseFragment() {
    private val mContainer1: FrameLayout by id(R.id.fl_container1)
    private val mContainer2: FrameLayout by id(R.id.fl_container2)

    override fun getLayoutId(): Int {
        return R.layout.layout_mutable_context_fragment
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val context = requireContext()
        addChild(context, mContainer1, R.string.button_text_1)
        addChild(context, mContainer2, R.string.button_text_2)
    }

    private fun addChild(context: Context, parentView: ViewGroup, textId: Int) {
        val button = LayoutInflater.from(context).inflate(R.layout.layout_child_button, parentView, false)
        (button as? Button)?.run {
            setText(textId)
            setOnClickListener {
                MutableContextWrapperFragment2::class.java.canonicalName?.let {
                    CommonFragmentsActivity.start(requireActivity(), it, "MutableContextWrapper示例New")
                }
            }
        }
        parentView.addView(button)
    }
}

第一个页面执行结果如下:

点击其中任何一个按钮会跳到第二个页面:

kotlin 复制代码
class MutableContextWrapperFragment2 : BaseFragment() {
    private val mContainer1: FrameLayout by id(R.id.fl_container1)
    private val mContainer2: FrameLayout by id(R.id.fl_container2)

    override fun getLayoutId(): Int {
        return R.layout.layout_mutable_context_fragment
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val context = requireContext()
        addChild(context, mContainer1, R.string.button_text_3)

        //通过createConfigurationContext修改Configuration
        val configuration = Configuration().apply {
            setLocale(Locale.ENGLISH)
        }
        val wrapperNew = context.createConfigurationContext(configuration)
        addChild(wrapperNew, mContainer2, R.string.button_text_4)
    }

    private fun addChild(context: Context, parentView: ViewGroup, textId: Int) {
        val button = LayoutInflater.from(context).inflate(R.layout.layout_child_button, parentView, false)
        (button as? Button)?.setText(textId)
        parentView.addView(button)
    }
}

可以看到在第二个页面中,底部这个按钮通过createConfigurationContext()修改了Configuration中的Locale.ENGLISH,让其优先加载英文资源。执行结果:

此时退回前一个页面再重进,效果不变,说明createConfigurationContext()的修改是局部的,并不会全局App生效。重新修改一下页面2的代码:

kotlin 复制代码
//...其他不变...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val context = requireContext()
    addChild(context, mContainer1, R.string.button_text_3)

    //修改这里!改为updateConfiguration方式,这个会引起全局修改
    val wrapper = MutableContextWrapper(context).apply {
        val configuration = Configuration().apply {
            setLocale(Locale.ENGLISH)
        }
        resources.updateConfiguration(configuration, resources.displayMetrics)
    }
    addChild(wrapper, mContainer2, R.string.button_text_4)
}

可以看到改为updateConfiguration()方式去改变Configuration,其他不变。同样从页面2退到页面1 后再重进页面2,效果如下:

咦,怎么都变成英文了?这是因为前一次在页面2时,通过resources.updateConfiguration已经全局改变了Configuration,导致所有Button都优先去加载英文资源了。此时把页面1、页面2都退了,再重新进页面1时,会发现页面1也受到了影响:

说明resources.updateConfiguration 只要设置一次就会全局更改Configuration。

三、总结

Configuration 是处理多样化设备和国际化的核心类之一。它在系统和开发者之间起到桥梁作用,帮助应用更智能地适配不同环境。更新Configuration时,如果需要更新整个App的范围,考虑使用resources.updateConfiguration()方式;如果只是局部修改,那么选择Context.createConfigurationContext()方式

相关推荐
雨白2 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹4 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空5 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭6 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日7 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安7 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑7 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟11 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡12 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0012 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体