多进程的多语言切换

背景是最近海外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 应用内切换 ✅ 最推荐
相关推荐
shuaiqinke4 小时前
【分享】OrbitV工具箱| 手表手环全能适配 |表盘应用一键装
android·智能手机
子非吾喵4 小时前
HBuilder X本地打包的资源放到Android Studio本地打包的记录
android·ide·android studio
simplepeng16 小时前
我们都知道但总是忽略的5个Jetpack Compose细节
android·android jetpack
刮风那天16 小时前
Android 如何提高进程优先级避免被查杀?
android
修行者对66618 小时前
安卓阿里云镜像
android
刮风那天18 小时前
Android AMS创建进程不用Binder而用Socket?
android·java·binder
知行合一。。。20 小时前
Python--05--面向对象(继承,多态)
android·开发语言·python
张小潇21 小时前
AOSP15 WMS/AMS系统开发 -窗口动画源码分析
android
程序员陆业聪1 天前
Shadow核心原理:壳子Activity与代理机制的精妙设计
android