天天看點

android源碼分析(一) - 語言切換機制

android語言切換是在packages/apps/Settings/com/android/settings/LocalePicker.java的updateLocale()函數中調用.

/**
     * Requests the system to update the system locale. Note that the system looks halted
     * for a while during the Locale migration, so the caller need to take care of it.
     */
    public static void updateLocale(Locale locale) {
        try {
            IActivityManager am = ActivityManagerNative.getDefault();
            Configuration config = am.getConfiguration();

            config.locale = locale;

            // indicate this isn't some passing default - the user wants this remembered
            config.userSetLocale = true;

            am.updateConfiguration(config);
            // Trigger the dirty bit for the Settings Provider.
            BackupManager.dataChanged("com.android.providers.settings");
        } catch (RemoteException e) {
            // Intentionally left blank
        }
    }
           

 從注釋可以看出, 隻要本地local改變就會調用該函數. 檢視ActivityManagerNative的getDefault()可以看到, 該函數傳回的是遠端服務對象ActivityManagerServices.java在本地的一個代理.  最終調用的是ActivityManagerService.java中的updateConfiguration()函數. 

public void updateConfiguration(Configuration values) {
        enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,
                "updateConfiguration()");

        synchronized(this) {
            if (values == null && mWindowManager != null) {
                // sentinel: fetch the current configuration from the window manager
                values = mWindowManager.computeNewConfiguration();
            }

            if (mWindowManager != null) {
                mProcessList.applyDisplaySize(mWindowManager);
            }

            final long origId = Binder.clearCallingIdentity();
            if (values != null) {
                Settings.System.clearConfiguration(values);
            }
            updateConfigurationLocked(values, null, false, false);
            Binder.restoreCallingIdentity(origId);
        }
    }
           

 該函數, 首先進行的是權限的校驗. 然後調用updateConfigurationLocked()函數. 

/**
     * Do either or both things: (1) change the current configuration, and (2)
     * make sure the given activity is running with the (now) current
     * configuration.  Returns true if the activity has been left running, or
     * false if <var>starting</var> is being destroyed to match the new
     * configuration.
     * @param persistent TODO
     */
    public boolean updateConfigurationLocked(Configuration values,
            ActivityRecord starting, boolean persistent, boolean initLocale) {
        int changes = 0;
        
        boolean kept = true;
        
        if (values != null) {
            Configuration newConfig = new Configuration(mConfiguration);
            changes = newConfig.updateFrom(values);
            if (changes != 0) {
                if (DEBUG_SWITCH || DEBUG_CONFIGURATION) {
                    Slog.i(TAG, "Updating configuration to: " + values);
                }
                
                EventLog.writeEvent(EventLogTags.CONFIGURATION_CHANGED, changes);

                if (values.locale != null && !initLocale) {
                    saveLocaleLocked(values.locale, 
                                     !values.locale.equals(mConfiguration.locale),
                                     values.userSetLocale, values.simSetLocale);
                }

                
                mConfigurationSeq++;
                if (mConfigurationSeq <= 0) {
                    mConfigurationSeq = 1;
                }
                newConfig.seq = mConfigurationSeq;
                mConfiguration = newConfig;
                Slog.i(TAG, "Config changed: " + newConfig);

                final Configuration configCopy = new Configuration(mConfiguration);

                AttributeCache ac = AttributeCache.instance();
                if (ac != null) {
                    ac.updateConfiguration(configCopy);
                }

                // Make sure all resources in our process are updated
                // right now, so that anyone who is going to retrieve
                // resource values after we return will be sure to get
                // the new ones.  This is especially important during
                // boot, where the first config change needs to guarantee
                // all resources have that config before following boot
                // code is executed.
                mSystemThread.applyConfigurationToResources(configCopy);

                if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {
                    Message msg = mHandler.obtainMessage(UPDATE_CONFIGURATION_MSG);
                    msg.obj = new Configuration(configCopy);
                    mHandler.sendMessage(msg);
                }
        
                for (int i=mLruProcesses.size()-1; i>=0; i--) {
                    ProcessRecord app = mLruProcesses.get(i);
                    try {
                        if (app.thread != null) {
                            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Sending to proc "
                                    + app.processName + " new config " + mConfiguration);
                            app.thread.scheduleConfigurationChanged(configCopy);
                        }
                    } catch (Exception e) {
                    }
                }
                Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                        | Intent.FLAG_RECEIVER_REPLACE_PENDING);
                broadcastIntentLocked(null, null, intent, null, null, 0, null, null,
                        null, false, false, MY_PID, Process.SYSTEM_UID);
                if ((changes&ActivityInfo.CONFIG_LOCALE) != 0) {
                    broadcastIntentLocked(null, null,
                            new Intent(Intent.ACTION_LOCALE_CHANGED),
                            null, null, 0, null, null,
                            null, false, false, MY_PID, Process.SYSTEM_UID);
                }
                
            }
        }
        
        if (changes != 0 && starting == null) {
            // If the configuration changed, and the caller is not already
            // in the process of starting an activity, then find the top
            // activity to check if its configuration needs to change.
            starting = mMainStack.topRunningActivityLocked(null);
        }
        
        if (starting != null) {
            kept = mMainStack.ensureActivityConfigurationLocked(starting, changes);
            // And we need to make sure at this point that all other activities
            // are made visible with the correct configuration.
            mMainStack.ensureActivitiesVisibleLocked(starting, changes);
        }
        
        if (values != null && mWindowManager != null) {
            mWindowManager.setNewConfiguration(mConfiguration);
        }
        
        return kept;
    }
           

整個語言切換就在這個函數中完成. 咋一看似乎沒感覺到該函數做了哪些事情. 我們首先來看注釋: Do either or both things: (1) change the current configuration, and (2)

 make sure the given activity is running with the (now) current. configuration大概意思是: 這個函數做了兩件事情. (1). 改變目前的configuration. 意思就是讓改變的configuration更新到目前configuration. (2) 確定所有正在運作的activity都能更新改變後的configuration.(這點是關鍵.) . 我們按照這個思路看看android是如何更新configuration. 檢視代碼 , 首先看到 這個函數首先判斷values是否為空, 這裡values肯定不為空的, 然後changes = newConfig.updateFrom(values); 我們看看updateFrom做了什麼操作.

/**
     * Copy the fields from delta into this Configuration object, keeping
     * track of which ones have changed.  Any undefined fields in
     * <var>delta</var> are ignored and not copied in to the current
     * Configuration.
     * @return Returns a bit mask of the changed fields, as per
     * {@link #diff}.
     */
    public int updateFrom(Configuration delta) {
        int changed = 0;
        ...
        if (delta.locale != null
                && (locale == null || !locale.equals(delta.locale))) {
            changed |= ActivityInfo.CONFIG_LOCALE;
            locale = delta.locale != null
                    ? (Locale) delta.locale.clone() : null;
            textLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(locale);
        }
        if (delta.userSetLocale && (!userSetLocale || ((changed & ActivityInfo.CONFIG_LOCALE) != 0)))
        {
            userSetLocale = true;
            changed |= ActivityInfo.CONFIG_LOCALE;
        }
        ...
        return changed;
    }
           

因為語言改變了, 那麼 (!locale.equals(delta.locale)) 是true. changed 大于0, 然後return changed. 回到ActivityManagerService.java的updateConfigurationLocked函數, 因為changed不為0 , 是以走if這個流程.  繼續看代碼

for (int i=mLruProcesses.size()-1; i>=0; i--) {
                    ProcessRecord app = mLruProcesses.get(i);
                    try {
                        if (app.thread != null) {
                            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Sending to proc "
                                    + app.processName + " new config " + mConfiguration);
                            app.thread.scheduleConfigurationChanged(configCopy);
                        }
                    } catch (Exception e) {
                    }
                }
           

首先看到的是mLurProcesses 是ArrayList<ProcessRecord>類型.  LRU : Least Recently Used儲存所有運作過的程序.  ProcessRecord程序類, 一個apk檔案運作時會對應一個程序. app.thread. 此處的thread代表的是ApplicationThreadNative.java類型.  然後調用其scheduleConfigurationChanged();  檢視該函數

public final void scheduleConfigurationChanged(Configuration config)
            throws RemoteException {
        Parcel data = Parcel.obtain();
        data.writeInterfaceToken(IApplicationThread.descriptor);
        config.writeToParcel(data, 0);
        mRemote.transact(SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION, data, null,
                IBinder.FLAG_ONEWAY);
        data.recycle();
    }
           

又是通過binder調用, 是以 , binder在android中是一個很重要的概念. 此處遠端調用的是ActivityThread.java中的私有内部内ApplicationThread

 private class ApplicationThread extends ApplicationThreadNative {
        private static final String HEAP_COLUMN = "%13s %8s %8s %8s %8s %8s %8s";
        private static final String ONE_COUNT_COLUMN = "%21s %8d";
        private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d";
        private static final String TWO_COUNT_COLUMNS_DB = "%21s %8d %21s %8d";
        private static final String DB_INFO_FORMAT = "  %8s %8s %14s %14s  %s";


        ...
        public void scheduleConfigurationChanged(Configuration config) {
            updatePendingConfiguration(config);
            queueOrSendMessage(H.CONFIGURATION_CHANGED, config);
        }
        ...
}
           

而ApplicationThread中的handler的CONFIGURATION_CHANGED是調用handleConfigurationChanged()

final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {

        ArrayList<ComponentCallbacks2> callbacks = null;
 
 	...			...
        applyConfigurationToResourcesLocked(config, compat);
        
        ...
        
        callbacks = collectComponentCallbacksLocked(false, config);
        ...
        
        if (callbacks != null) {
            final int N = callbacks.size();
            for (int i=0; i<N; i++) {
                performConfigurationChanged(callbacks.get(i), config);
            }
        }
}
           

這個函數首先是調用applyConfigurationToResourcesLocked(). 看函數名大概可以推測: 将configuration應用到resources.這裡configuration改變的是local 本地語言. 那而resources資源包含不就包含了語言, 圖檔這些資源嗎. 

final boolean applyConfigurationToResourcesLocked(Configuration config,
            CompatibilityInfo compat) {
        
        int changes = mResConfiguration.updateFrom(config);
        DisplayMetrics dm = getDisplayMetricsLocked(null, true);


        if (compat != null && (mResCompatibilityInfo == null ||
                !mResCompatibilityInfo.equals(compat))) {
            mResCompatibilityInfo = compat;
            changes |= ActivityInfo.CONFIG_SCREEN_LAYOUT
                    | ActivityInfo.CONFIG_SCREEN_SIZE
                    | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
        }

        ...

        Resources.updateSystemConfiguration(config, dm, compat);

        ...
        
        Iterator<WeakReference<Resources>> it =
            mActiveResources.values().iterator();
        while (it.hasNext()) {
            WeakReference<Resources> v = it.next();
            Resources r = v.get();
            if (r != null) {
                if (DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources "
                        + r + " config to: " + config);
                r.updateConfiguration(config, dm, compat);
                //Slog.i(TAG, "Updated app resources " + v.getKey()
                //        + " " + r + ": " + r.getConfiguration());
            } else {
                //Slog.i(TAG, "Removing old resources " + v.getKey());
                it.remove();
            }
        }
        
        return changes != 0;
    }
           

Resources.updateSystemConfiguration()清除一部分系統資源, 并且将config更新到Resources, 而Resources包含了一個AssetManager對象, 該對象的核心實作是在AssetManager.cpp中完成的. 然後循環清空mActivityResources資源. 再回到handleConfigurationChanged()函數, 執行完updateSystemConfiguration後, 會循環該程序的所有activity:

if (callbacks != null) {

            final int N = callbacks.size();

            for (int i=0; i<N; i++) {

                performConfigurationChanged(callbacks.get(i), config);

            }

        }

再來看performConfigurationChanged的實作:

private final void performConfigurationChanged(
            ComponentCallbacks2 cb, Configuration config) {
        // Only for Activity objects, check that they actually call up to their
        // superclass implementation.  ComponentCallbacks2 is an interface, so
        // we check the runtime type and act accordingly.
        Activity activity = (cb instanceof Activity) ? (Activity) cb : null;
        if (activity != null) {
            activity.mCalled = false;
        }

        boolean shouldChangeConfig = false;
        if ((activity == null) || (activity.mCurrentConfig == null)) {
            shouldChangeConfig = true;
        } else {

            // If the new config is the same as the config this Activity
            // is already running with then don't bother calling
            // onConfigurationChanged
            int diff = activity.mCurrentConfig.diff(config);
            if (diff != 0) {
                // If this activity doesn't handle any of the config changes
                // then don't bother calling onConfigurationChanged as we're
                // going to destroy it.
                if ((~activity.mActivityInfo.getRealConfigChanged() & diff) == 0) {
                    shouldChangeConfig = true;
                }
            }
        }

        if (DEBUG_CONFIGURATION) Slog.v(TAG, "Config callback " + cb
                + ": shouldChangeConfig=" + shouldChangeConfig);
        if (shouldChangeConfig) {
            cb.onConfigurationChanged(config);

            if (activity != null) {
                if (!activity.mCalled) {
                    throw new SuperNotCalledException(
                            "Activity " + activity.getLocalClassName() +
                        " did not call through to super.onConfigurationChanged()");
                }
                activity.mConfigChangeFlags = 0;
                activity.mCurrentConfig = new Configuration(config);
            }
        }
    }
           

該函數判斷configuration是否改變, 如果改變那麼shouldChangeConfig為true. 然後調用activity的onConfigurationChange(config);

/**
     * Called by the system when the device configuration changes while your
     * activity is running.  Note that this will <em>only</em> be called if
     * you have selected configurations you would like to handle with the
     * {@link android.R.attr#configChanges} attribute in your manifest.  If
     * any configuration change occurs that is not selected to be reported
     * by that attribute, then instead of reporting it the system will stop
     * and restart the activity (to have it launched with the new
     * configuration).
     * 
     * <p>At the time that this function has been called, your Resources
     * object will have been updated to return resource values matching the
     * new configuration.
     * 
     * @param newConfig The new device configuration.
     */
    public void onConfigurationChanged(Configuration newConfig) {
        mCalled = true;

        mFragments.dispatchConfigurationChanged(newConfig);

        if (mWindow != null) {
            // Pass the configuration changed event to the window
            mWindow.onConfigurationChanged(newConfig);
        }

        if (mActionBar != null) {
            // Do this last; the action bar will need to access
            // view changes from above.
            mActionBar.onConfigurationChanged(newConfig);
        }
    }
           

檢視注釋, 大概意思是:  如果你的activity運作 , 裝置資訊有改變(即configuration改變)時由系統調用. 如果你在manifest.xml中配置了configChnages屬性則表示有你自己來處理configuration change. 否則就重新開機目前這個activity.  而重新開機之前, 舊的resources已經被清空, 那麼就會裝載新的資源, 整個過程就完成了語言切換後 , 能夠讓所有app使用新的語言. 語言切換流程大概分為三步:

第一步:  判斷configuration的local是否已經改變, 如果改變則将local更新到目前的configuration

第二步: 清空舊的資源. 

第三步: 重新開機所有所有程序并加裝新資源.

由于個人知識水準有限, 有些地方不免有些纰漏, 希望大牛多多指點.  也希望有共同興趣愛好的人進行技術交流.