背景是最近海外app有个后台进程的多语言支持。
测试同学发现UI进程切换语言之后,在后台进程的消息通知栏的语言没有变化。
这里其实是2个问题:
- 后台进程中没有主动调用notification的刷新
- 我使用的是application的context去获取string,这个application的context不会主动刷新多语言的资源。
修改方法是这样的:
-
Application/Service 监听到 onConfigurationChanged 回调,主动触发了notification的刷新
-
在onConfigurationChanged中主动刷新 context的多语言资源
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Resources resources = app.getResources(); resources.updateConfiguration(config, resources.getDisplayMetrics()); String text = app.getString(R.string.app_name); }else{ Context localizedContext = createConfigurationContext(newConfig); String text = localizedContext.getString(R.string.app_name); }}
AI 总结
为什么不推荐直接使用 Application Context 获取多语言资源:
核心原因
1. Application Context 的生命周期问题
java
// Application 是全局单例,在整个应用生命周期中只创建一次
// 它的 Configuration 不会像 Activity 那样自动重建
Application app = getApplication();
String text = app.getString(R.string.app_name); // ⚠️ 可能是旧配置
2. Configuration 更新时机不同
| Context 类型 | 语言切换时的行为 |
|---|---|
| Activity Context | Activity 会被销毁并重建,自动获取新 Configuration |
| Application Context | 不会重建,需要手动更新 Resources |
3. 实际代码对比
java
public class MainActivity extends AppCompatActivity {
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// ❌ Application Context - 可能获取旧语言
String appText = getApplication().getString(R.string.app_name);
// ✅ Activity Context - 总是获取当前语言
String activityText = getString(R.string.app_name);
Log.d("Test", "App: " + appText); // 可能输出 "English"
Log.d("Test", "Activity: " + activityText); // 输出 "中文"
}
}
为什么会这样?
Android 系统的更新机制
java
// 当系统语言改变时:
// 1. Activity 的处理:
// - onDestroy() → onCreate() 重建
// - 或触发 onConfigurationChanged()
// - Resources 自动使用新的 Configuration
// 2. Application 的处理:
// - 只触发 onConfigurationChanged()
// - Resources **不会**自动更新(除非手动处理)
源码层面的差异
java
// Activity.java (简化版)
@Override
public Resources getResources() {
// Activity 每次重建都会创建新的 Resources
return mResources; // 这是新创建的 Resources
}
// Application.java (简化版)
@Override
public Resources getResources() {
// Application 使用的是全局共享的 Resources
return mBase.getResources(); // 可能是旧的
}
解决方案
方案 1:优先使用 Activity Context
java
// ✅ 推荐
class MyViewModel {
void loadData(Activity activity) {
String text = activity.getString(R.string.hint);
}
}
方案 2:手动更新 Application Resources
java
public class MyApplication extends Application {
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 手动更新 Application 的 Resources
updateResources(newConfig);
}
private void updateResources(Configuration config) {
Resources resources = getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
resources.updateConfiguration(config, dm);
}
}
为什么需要传入 DisplayMetrics?
java
resources.updateConfiguration(config, dm);
// ↑ ↑
// 语言配置 显示配置
DisplayMetrics 确实和多语言无关 ,但它存储了屏幕密度相关的信息:
java
public final class DisplayMetrics {
public float density; // 屏幕密度(如 3.0)
public int densityDpi; // 密度 DPI(如 480)
public int widthPixels; // 屏幕宽度
public int heightPixels; // 屏幕高度
// ...
}
原理解析
java
// Resources 内部需要同时处理两类资源:
// 1. 语言相关:strings.xml(通过 Configuration)
// 2. 密度相关:drawable-hdpi、layout-sw600dp(通过 DisplayMetrics)
resources.updateConfiguration(config, dm);
// 等同于告诉 Resources:
// "请使用新的语言配置(config) + 保持原来的屏幕信息(dm)"
如果不传 DisplayMetrics 会怎样?
java
// ❌ 错误示例
resources.updateConfiguration(config, null);
// 结果:可能导致资源加载错误
// - 无法正确选择 drawable-hdpi/xhdpi
// - 无法正确选择 layout-sw600dp
为什么废弃?
java
/**
* @deprecated Use {@link #createConfigurationContext(Configuration)}
* and {@link Context#getResources()} instead.
*
* updateConfiguration() 会影响全局 Resources,可能导致其他组件的资源异常
*/
@Deprecated
public void updateConfiguration(Configuration config, DisplayMetrics dm) {
// ...
}
方案 3:使用 createConfigurationContext
java
// ✅ 创建带有新 Configuration 的 Context
Context newContext = application.createConfigurationContext(newConfig);
String text = newContext.getString(R.string.app_name);
代码总结
✅ 推荐的现代解决方案
方案一:使用 createConfigurationContext()(推荐)
java
// Android 7.0+ (API 24+)
private Context createLocalizedContext(Configuration newConfig) {
// 创建一个新的 Context,不影响原有 Resources
Context context = createConfigurationContext(newConfig);
return context;
}
// 使用示例
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 创建新的本地化 Context
Context localizedContext = createConfigurationContext(newConfig);
// 使用新 Context 获取资源
String text = localizedContext.getString(R.string.app_name);
}
方案二:完整的 Application 多语言更新
java
public class MyApplication extends Application {
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 同步 Locale.getDefault()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList locales = newConfig.getLocales();
if (!locales.isEmpty()) {
Locale.setDefault(locales.get(0));
}
} else {
Locale.setDefault(newConfig.locale);
}
// ✅ 现代做法:不直接更新 Resources
// 而是在需要时动态创建 Context
}
// 提供一个获取本地化字符串的方法
public String getLocalizedString(@StringRes int resId) {
Configuration config = getResources().getConfiguration();
Context localizedContext = createConfigurationContext(config);
return localizedContext.getString(resId);
}
}
方案三:兼容旧版本的完整方案
java
public class LocaleHelper {
/**
* 更新 Context 的语言配置
* 兼容 API 17+
*/
public static Context updateLocale(Context context, Locale locale) {
Configuration config = new Configuration(context.getResources().getConfiguration());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Android 7.0+
config.setLocale(locale);
config.setLocales(new LocaleList(locale));
return context.createConfigurationContext(config);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Android 4.2+
config.setLocale(locale);
return context.createConfigurationContext(config);
} else {
// Android 4.1 及以下(已不推荐支持)
config.locale = locale;
Resources resources = context.getResources();
resources.updateConfiguration(config, resources.getDisplayMetrics());
return context;
}
}
/**
* 在 Application 中使用
*/
public static void updateApplicationLocale(Application app, Locale locale) {
// 1. 同步 Locale.getDefault()
Locale.setDefault(locale);
// 2. 更新配置
Configuration config = new Configuration(app.getResources().getConfiguration());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.setLocales(new LocaleList(locale));
} else {
config.locale = locale;
}
// 3. 对于 API < 24,仍需使用 updateConfiguration
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Resources resources = app.getResources();
resources.updateConfiguration(config, resources.getDisplayMetrics());
}
// API 24+ 不需要手动更新 Application Resources
}
}
最佳实践总结
1. Activity 中:无需手动处理
java
// ✅ Activity 会自动重建,直接使用即可
String text = getString(R.string.app_name);
2. Application 中:提供获取本地化资源的方法
java
public class MyApplication extends Application {
// 获取当前语言的字符串
public String getLocalizedString(@StringRes int resId) {
Configuration config = getResources().getConfiguration();
Context context = createConfigurationContext(config);
return context.getString(resId);
}
}
3. 手动切换语言:使用 AppCompatDelegate
java
// ✅ 推荐:使用 AndroidX 的方法
public class MainActivity extends AppCompatActivity {
private void changeLanguage(String languageCode) {
// 使用 AppCompatDelegate(支持 API 14+)
LocaleListCompat locales = LocaleListCompat.forLanguageTags(languageCode);
AppCompatDelegate.setApplicationLocales(locales);
// 系统会自动重建 Activity
}
}
总结
| 使用场景 | 推荐做法 |
|---|---|
| UI 相关 | 使用 Activity/Fragment Context |
| 后台任务 | 使用 createConfigurationContext() |
| 全局单例 | 避免缓存 Application Context |
| 必须用 Application | 手动更新 Resources |
记住:Application Context 的 Configuration 不会自动同步系统语言变化,这就是为什么不推荐直接使用它来获取多语言资源的根本原因。
| 方法 | 适用场景 | 是否废弃 |
|---|---|---|
updateConfiguration() |
API < 17 | ✅ 已废弃 |
createConfigurationContext() |
API 17+ | ✅ 推荐 |
AppCompatDelegate.setApplicationLocales() |
AndroidX 应用内切换 | ✅ 最推荐 |