多进程的多语言切换

背景是最近海外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 应用内切换 ✅ 最推荐
相关推荐
耶叶15 分钟前
Android 开发:基于Scaffold的电子邮件App
android·android-studio
三少爷的鞋19 分钟前
是时候告别业务层 Manager 了:Android 架构升级到 UseCase + Repository
android
erniu22222221 分钟前
android dex2oat 编译dex文件分析
android
恋猫de小郭28 分钟前
Flutter 3.41.6 版本很重要,你大概率需要更新一下
android·前端·flutter
野生的码农9 小时前
放过自己,降低预期,及时行乐
android·ai编程
huwuhang9 小时前
索尼PS3游戏合集【中文游戏】8.12T 1430个游戏+PS3模拟器
android·游戏·智能手机·游戏机·电视
Grackers11 小时前
Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
android
踩着两条虫11 小时前
AI驱动的Vue3应用开发平台深入探究(十):物料系统之内置组件库
android·前端·vue.js·人工智能·低代码·系统架构·rxjava
sam.li11 小时前
JADX MCP 原理与使用部署
android·逆向·jadx
冬奇Lab12 小时前
Android 15音频子系统(五):AudioPolicyService策略管理深度解析
android·音视频开发·源码阅读