美文网首页
Android多语言切换以及如何改BUG(微笑)

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

作者: luminos | 来源:发表于2017-05-19 10:29 被阅读0次

    引言

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

    在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);
      }
    }
    

    相关文章

      网友评论

          本文标题:Android多语言切换以及如何改BUG(微笑)

          本文链接:https://www.haomeiwen.com/subject/fwiotxtx.html