Android多语言切换以及如何改BUG(微笑)

引言

事情是这样的,我们接到一个需求,是要为我们的应用做多语言版本并且提供多语言切换。事后证明,这个事情还真的是很蛋疼的一件事。

在android系统中,应用的语言环境会跟随系统环境。如果在resource文件夹中,如果设置了对应语言环境的资源文件夹,那么在使用资源的时候会由AssetManager到对应的资源文件夹中取出展示。

如果想让应用不跟随系统环境,而是能使用自己的语言配置呢?这也不难,只要将context.getResoure().getConfiguration().locale改成设置的语言环境即可。

如何设置应用的app locale

  1. 添加多语言文本文件
    resource文件下增加不同语言的value文件夹,例如英文的添加value-en文件夹,繁体中文添加value-zh-rTW文件夹

  2. 更新configurationlocale属性
    android中,configuration包含了activity所有的配置信息,包括屏幕密度,屏幕宽度,语言设置等等。修改应用的configuration使应用根据configuration中配置的语言环境来展示资源。

public class LanguageUtil {
  /**
   * 设置应用语言类型
   */
    @SuppressWarnings("deprecation")
    public static void setAppLocale(Locale locale) {
      if (locale!=null) {
        Configuration configuration = context.getResources().getConfiguration();
        configuration.locale = locale;
      }
    }
}
  1. 重新启动activity
    已经启动了的activity当然不会自己把页面全部换一遍,最简单粗暴的方法当然是重新启动他们。把栈里的activity统统干掉,重新启动第一个activity
    如何能够保持所有的activity不需要重新启动?这是另一个问题了,这里不作讨论
public static void startMainNewTask(Context context) {
    Intent intent = new Intent(context, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
  }
  1. 持久化存储应用locale配置
    使用sharePreference或者什么的存一下应用的语言设置,在下次启动应用的时候重新恢复一下即可。这里就不赘述了。

这样就OK了吗?

并没有。上述方法看起来很美,网上很多能搜到的资料也基本都止步于此。但是我们改的configuration,真的不会再变了吗?

事实上,configuration在很多的情况下会被系统所修改,比如在切换系统语言的时候、在横竖屏切换的时候等等。当configuration发生改变的时候,ActivityThread会拷贝一份系统的configuration,覆盖到appContext里。如果在configuration change发生之后,页面更新数据并且通过resource去获取文字的话,会以系统的locale为准去获取文字资源。

于是我们开始研究怎么改这个bug。

通过调试,发现每一次横竖屏切换过后,Application$onConfigurationChanged(Configuration newConfig)方法都会被调用一次。于是很自然地,我们想到,如果在这里我们把newConfig在调用super方法之前改掉,是不是就能够解决这个问题了?

很不幸,不是的。在Application$onConfigurationChanged(Configuration newConfig)被调用的时候,Resource#mResourceImpl#mConfiguration已经被修改了。从下面这段代码可以看出来:


public final class ActivityThread {
  
 ......

 final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {
        ......
        synchronized (mResourcesManager) {
            ......
            // 这个方法最终会调用Resource$updateConfiguration方法,导致locale被覆盖
            mResourcesManager.applyConfigurationToResourcesLocked(config, compat);
            ......
        }
        
        ......

        if (callbacks != null) {
            final int N = callbacks.size();
            for (int i=0; i<N; i++) {
                ComponentCallbacks2 cb = callbacks.get(i);
                if (cb instanceof Activity) {
                    // If callback is an Activity - call corresponding method to consider override
                    // config and avoid onConfigurationChanged if it hasn't changed.
                    Activity a = (Activity) cb;
                    performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()),
                            config, REPORT_TO_ACTIVITY);
                } else {

                    // 这个方法会调用Application$onConfigurationChanged
                    performConfigurationChanged(cb, null, config, null, REPORT_TO_ACTIVITY);
                }
            }
        }
    }
}

同时,在Application$onConfigurationChanged方法里修改Configuration#locale会引起另外一个bug:Activity会不断地重启。表现在视觉上就是这个Activity启动之后一直在闪烁。这个是什么原因?

原因在于当Orientation发生改变的时候,ActivityManagerService会去检查新启动的ActivityConfiguration是否是一致的,否则会重新启动Activity,关键的代码是:


final class ActivityStack {

......

    /**
     * Make sure the given activity matches the current configuration. Returns false if the activity
     * had to be destroyed.  Returns true if the configuration is the same, or the activity will
     * remain running as-is for whatever reason. Ensures the HistoryRecord is updated with the
     * correct configuration and all other bookkeeping is handled.
     */
    boolean ensureActivityConfigurationLocked(
            ActivityRecord r, int globalChanges, boolean preserveWindow) {
        ......
       
        // Figure out how to handle the changes between the configurations.
        if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                "Checking to restart " + r.info.name + ": changed=0x"
                + Integer.toHexString(changes) + ", handles=0x"
                + Integer.toHexString(r.info.getRealConfigChanged()) + ", newConfig=" + newConfig
                + ", taskConfig=" + taskConfig);

        if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) {
            // Aha, the activity isn't handling the change, so DIE DIE DIE.
            r.configChangeFlags |= changes;
            r.startFreezingScreenLocked(r.app, globalChanges);
            r.forceNewConfig = false;
            preserveWindow &= isResizeOnlyChange(changes);
            if (r.app == null || r.app.thread == null) {
                if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                        "Config is destroying non-running " + r);
                destroyActivityLocked(r, true, "config");
            } else if (r.state == ActivityState.PAUSING) {
                // A little annoying: we are waiting for this activity to finish pausing. Let's not
                // do anything now, but just flag that it needs to be restarted when done pausing.
                if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                        "Config is skipping already pausing " + r);
                r.deferRelaunchUntilPaused = true;
                r.preserveWindowOnDeferredRelaunch = preserveWindow;
                return true;
            } else if (r.state == ActivityState.RESUMED) {
                // Try to optimize this case: the configuration is changing and we need to restart
                // the top, resumed activity. Instead of doing the normal handshaking, just say
                // "restart!".
                if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                        "Config is relaunching resumed " + r);

                if (DEBUG_STATES && !r.visible) {
                    Slog.v(TAG_STATES, "Config is relaunching resumed invisible activity " + r
                            + " called by " + Debug.getCallers(4));
                }

                relaunchActivityLocked(r, r.configChangeFlags, true, preserveWindow);
            } else {
                if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                        "Config is relaunching non-resumed " + r);
                relaunchActivityLocked(r, r.configChangeFlags, false, preserveWindow);
            }

            // All done...  tell the caller we weren't able to keep this activity around.
            return false;
        }

        // Default case: the activity can handle this new configuration, so hand it over.
        // NOTE: We only forward the task override configuration as the system level configuration
        // changes is always sent to all processes when they happen so it can just use whatever
        // system level configuration it last got.
        r.scheduleConfigurationChanged(taskConfig, true);
        r.stopFreezingScreenLocked(false);

        return true;
    }
}

既然在Application$onConfigurationChanged方法里无法修改locale。那么我们考虑在Activity$onResume方法里再更新一次locale行不行呢?在所有页面的基类BaseActivity执行如下测试代码:

public class BaseActivity extend AppCompatActivity {
  @Override
  protected void onResume() {
    super.onResume();
    // 语言状态检测
    recoverLanguage();
  }

  private void recoverLanguage() {
    // 系统语言是中文环境,将configuration#locale强制改成英文环境进行测试
    getResource().getConfiguration().locale = Locale.ENGLISH;   
  }
}

Activity$onResume里执行这样的代码,已经规避了Activity启动流程中对于Configuration的校验了,因为在Activity$onResume被执行的时候,校验已经结束了。

我们可以看到,Activity已经不再重复循环地去relaunch了。那么Configuration#locale修改成功了吗?修改成功了,但未起作用。通过调试,我们发现:


从断点数据中我们可以看到,mResource.mResourceImpl.mConfiguration.locale已经是Locale.ENGLISH了。

但是通过执行Context$getString方法我们却发现,取出来的文字是中文。这就耐人寻味了,为何原本修改Resource中的locale可以修改语言环境,而现在修改又不行了呢?

还是通过源码来探究一下。

从这三段源码可以看到,Context$getString方法实际上,是通过AssetManager来获取StringRes的,那是不是说,AssetManager里面也有一个locale呢?

是的!通过查看源码,我们发现AssetManager里有一个native方法:

原因就很明确了,虽然我们修改了Resourcelocale,却没有修改这里的,所以修改不生效。至此,解决办法就剩下:

  1. 通过反射,拿到'AssetManager'的这个方法,将locale设置进去。
  2. 通过寻找调用了这个方法的别的API,然后通过调用此API,更新进去。

反射的方法我并不喜欢,原因是这一个方法的参数列表太长了,反射的话写起来会很痛苦(微笑)

所以最终的解决办法是:

我们在Resourse里发现了一个方法:

    public void updateConfiguration(Configuration config, DisplayMetrics metrics,
                                    CompatibilityInfo compat) {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration");
        try {
            synchronized (mAccessLock) {
                ......
                mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
                        adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
                        mConfiguration.orientation,
                        mConfiguration.touchscreen,
                        mConfiguration.densityDpi, mConfiguration.keyboard,
                        keyboardHidden, mConfiguration.navigation, width, height,
                        mConfiguration.smallestScreenWidthDp,
                        mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
                        mConfiguration.screenLayout, mConfiguration.uiMode,
                        Build.VERSION.RESOURCES_SDK_INT);

                ......
            }
            synchronized (sSync) {
                if (mPluralRule != null) {
                    mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().get(0));
                }
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

那么只需要在Activity$onResume里添加如下代码:

public class BaseActivity extend AppCompatActivity {
  @Override
  protected void onResume() {
    super.onResume();
    // 语言状态检测
    recoverLanguage();
  }

  /**
   * 通过updateConfiguration方法修改Resource的Locale,连带修改Resource内Asset的Local.
   */
  private void recoverLanguage() {
    Resources resources = getContext().getResources();
    Configuration configuration = resources.getConfiguration();
    DisplayMetrics metrics = resources.getDisplayMetrics();
    // 从Preference中取出语言设置
    configuration.locale = PreferenceUtil.getCustomLanguageSetting();
    resources.updateConfiguration(configuration, metrics);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 最近公司的 App 里需要用到多语言切换,简单来说,就是如果用户没有选择语言选项时,App 默认跟随系统语言,如果...
    牙锅子阅读 6,217评论 0 20
  • 这里的多语言切换专指应用内的多语言切换,不涉及直接通过应用修改系统语言设置的功能。比如微信里面的 我 -> 设置 ...
    apkcore阅读 4,615评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 我偏爱的一个题材是魔幻类小说。印象最深的是《悟空传》。如下是一个简评。 如果最初的梦想,最后没有到达…… ...
    戴言007阅读 146评论 1 0
  • 计算机发展(推断) 简单工具(功能固定化) 周围的大多数工具都是固定功能。比如,书架---functon放置东西,...
    想太多的猫阅读 174评论 0 0