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閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

  • 最近公司的 App 里需要用到多語言切換,簡單來說,就是如果用戶沒有選擇語言選項時,App 默認跟隨系統語言,如果...
    牙鍋子閱讀 7,066評論 0 20
  • 這里的多語言切換專指應用內的多語言切換,不涉及直接通過應用修改系統語言設置的功能。比如微信里面的 我 -> 設置 ...
    apkcore閱讀 5,004評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,692評論 25 708
  • 我偏愛的一個題材是魔幻類小說。印象最深的是《悟空傳》。如下是一個簡評。 如果最初的夢想,最后沒有到達…… ...
    戴言007閱讀 164評論 1 0
  • 計算機發展(推斷) 簡單工具(功能固定化) 周圍的大多數工具都是固定功能。比如,書架---functon放置東西,...
    想太多的貓閱讀 198評論 0 0