多进程的多语言切换

背景是最近海外app有个后台进程的多语言支持。

测试同学发现UI进程切换语言之后,在后台进程的消息通知栏的语言没有变化。

这里其实是2个问题:

  1. 后台进程中没有主动调用notification的刷新
  2. 我使用的是application的context去获取string,这个application的context不会主动刷新多语言的资源。

修改方法是这样的:

  1. Application/Service 监听到 onConfigurationChanged 回调,主动触发了notification的刷新

  2. 在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 应用内切换 ✅ 最推荐
相关推荐
Yang-Never2 小时前
Android 内存泄漏 -> ViewModel持有Activity/Fragment导致的内存泄漏
android·java·开发语言·kotlin·android studio
Android_xiong_st2 小时前
(原创)Android遍历文件方法walk函数介绍
android
Yang-Never3 小时前
Android 内存泄漏 -> LiveData如何解决ViewMode和Activity/Fragment之间的内存泄漏
android·java·开发语言·kotlin·android studio
HeDongDong-3 小时前
Kotlin 协程(Coroutines)详解
android·开发语言·kotlin
allk553 小时前
Android APK 极限瘦身:从构建链优化到架构演进
android·架构
啊西:3 小时前
SuperMap iMobile for Android中模型按照指定路径运动
android
码农101号3 小时前
Ansible - Role介绍 和 使用playbook部署wordPress
android·ansible
a_eastern3 小时前
linux electron-forge离线打包关键配置
android·linux·electron
城东米粉儿4 小时前
Dynamic Feature Modules 笔记
android