Android8+后台接收ACTION_LOCALE_CHANGED时Activity资源未更新
我在Activity的onCreate()生命周期方法中注册了针对ACTION_LOCALE_CHANGED的广播接收器,当接收器接收到对应广播时会调用localeChanged()方法。我尝试通过以下两个API获取变更后的语言区域配置:
- A)
getApplicationContext().getResources().getConfiguration() - B)
this.getResources().getConfiguration()
测试发现,通过API A可以正常获取到正确的最新语言区域配置,但通过API B始终无法获取更新后的正确配置,一直返回变更前的旧语言区域值。
问题复现步骤:
- 打开测试应用,此时应用语言区域为"en"(系统默认语言区域也为"en")
- 按下Home键,将应用切至后台运行
- 将系统语言区域从"en"修改为"ja"
- 系统全局界面已全部切换为日语"ja"
- 重新打开测试应用,将应用从后台切回前台
- 应用内语言未同步更新,仍显示变更前的旧语言内容
上述localeChanged()方法是应用处于后台状态时,接收到ACTION_LOCALE_CHANGED广播后触发调用的。
在Android 6、7版本中,调用this.getResources().getConfiguration()可正常获取正确的资源与语言区域配置,但Android 8及以上版本在后台接收ACTION_LOCALE_CHANGED广播时会出现上述语言不更新的问题,在Android官方模拟器上也可稳定复现该问题。
上述场景下,通过onConfigurationChanged()回调可以正常获取到更新后的正确语言区域配置。
虽然通过onConfigurationChanged()可以拿到最新的正确语言配置,但需要明确:为何通过ACTION_LOCALE_CHANGED广播触发逻辑时,调用this.getResources().getConfiguration() API无法获取到更新后的语言区域配置,Android 7.1与Android 8版本之间是否存在相关API差异?
onCreate()中注册广播的代码:
@Override protected void onCreate(Bundle savedInstanceState) { Log.i(TAG,"onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); IntentFilter testLocaleChangedFilter = new IntentFilter(); testLocaleChangedFilter.addAction(Intent.ACTION_LOCALE_CHANGED); getApplicationContext().registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { localeChanged(); } }, testLocaleChangedFilter); }
广播接收后调用的localeChanged()方法、以及onConfigurationChanged()回调代码:
private void localeChanged() { Configuration config = getApplicationContext().getResources().getConfiguration(); Log.i(TAG, "localeChanged - getApplicationContext().getResources().getConfiguration():" + config); Configuration config2 = this.getResources().getConfiguration(); Log.i(TAG, "localeChanged - this.getResources().getConfiguration():" + config2.locale); ((TextView)findViewById(R.id.text_language)).setText(R.string.language_text); Log.i(TAG,"getResources().getString(R.string.language_text):" + getResources().getString(R.string.language_text)); } @Override public void onConfigurationChanged(android.content.res.Configuration newConfig) { Log.i(TAG, "onConfigurationChanged"); super.onConfigurationChanged(newConfig); String locale = Locale.getDefault().toString(); Log.i(TAG, "onConfigurationChanged(Locale):" + locale); Configuration config2 = this.getResources().getConfiguration(); Log.i(TAG, "onConfigurationChanged-this.getResources().getConfiguration():" + config2.locale); }
这个现象是Android 8.0(API 26)对组件资源更新逻辑做了后台优化调整导致的,核心逻辑如下:
两类Context的资源实例差异
getApplicationContext()返回的是全局Application级Context,它持有的Resources实例是进程全局共享的,系统在发送ACTION_LOCALE_CHANGED广播前,就已经完成了这个全局实例的Configuration更新,任何时候通过它读取配置都能拿到最新值。- Activity持有的Context是
ContextThemeWrapper包装的独立实例,绑定了Activity专属的主题与窗口资源,Resources是单独缓存的,不会和全局Application的Resources自动同步。
跨版本行为差异根源
- Android 7.1及更低版本:系统派发
ACTION_LOCALE_CHANGED广播前,会主动遍历进程内所有存活组件(包括后台状态的Activity),统一更新所有组件持有的Resources缓存配置,所以广播回调里调用Activity的getResources()能直接拿到最新配置。 - Android 8.0及更高版本:系统收紧了后台组件的资源更新逻辑:仅当组件处于前台活跃状态时,才会同步更新它的Resources缓存;处于后台
onStop()状态的Activity,不会在广播派发阶段触发资源更新。
应用切到后台后,目标Activity已经进入停止状态,此时系统触发语言变更,只会更新全局Application的Resources,不会主动刷新后台Activity的缓存资源,所以广播回调里调用Activity的getResources()拿到的是变更前的旧值。
等应用切回前台,系统才会给该Activity派发onConfigurationChanged()回调(未配置configChanges属性的话会直接重建Activity),这一步才会同步更新Activity绑定的Resources配置,所以在onConfigurationChanged()回调里能读取到正确的最新语言配置。
适配方案
- 读取全局系统配置(包括语言区域、屏幕参数等)时,统一使用Application Context的
Resources实例,不要依赖后台Activity Context的缓存资源。 - 语言变更后的UI刷新逻辑,不要在后台广播回调里直接执行,放到
onConfigurationChanged()回调、或者Activity回到前台的onStart()生命周期里处理即可,这两个时机点Activity的Resources已经完成更新,能正确加载对应语言的字符串资源。 - 不建议在广播回调里手动给Activity调用
applyOverrideConfiguration()或者手动创建Resources实例强制更新,这类操作容易和系统后续的配置派发逻辑冲突,引发资源显示错乱、内存泄漏等问题。
内容的提问来源于stack exchange,提问作者user2756322

