Android 原始碼分析之旅3 4 onConfigurationChanged

小楠總發表於2017-12-21

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出 本人小楠——一位勵志的Android開發者。

###前言

在手機APP開發的時候,一般預設會適配豎屏,遊戲開發除外。但是在Android平板電腦開發中,螢幕旋轉的問題比較突出,可以這樣說,平板電腦的最初用意就是橫屏使用的,比較方便,使用者會經常旋轉我們裝置的螢幕。

在我的《螢幕旋轉的適配問題以及遇到的一些坑》這篇文章中提到了一些坑,包括View的測量不準確,onConfigurationChanged的回撥不確定,今天主要分析一下onConfigurationChanged呼叫的不確定性因素。

關於這個問題,筆者在網上搜尋了一下關於為什麼onConfigurationChanged的方法不會被呼叫,基本都是說清單檔案裡面沒有正確配置,因為在Android2.3以後需要增加screenSize這個配置,完整的配置如下:

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
複製程式碼

但是完全搜尋不到關於我的問題的搜尋結果,畢竟做Android平板的並不多,因此寫下來記錄自己的學習過程。

###關於官方文件

我們知道,在Activity、View(ViewGroup)、Fragment、Service、Content Provider等等在裝置的配置發生變化的時候,會回撥onConfigurationChanged的方法。實質上主要是Activity中收到AMS的通知,回撥,然後把事件分發到Window、Fragment、ActionBar等。

下面我們可以通過Activity的onConfigurationChanged方法原始碼可以看到:

public void onConfigurationChanged(Configuration newConfig) {
    mCalled = true;

    //分發到Activity中的所有Fragment
    mFragments.dispatchConfigurationChanged(newConfig);

    //分發到Activity的Window物件
    if (mWindow != null) {
        // Pass the configuration changed event to the window
        mWindow.onConfigurationChanged(newConfig);
    }

    //分發到Activity的ActionBar
    if (mActionBar != null) {
        mActionBar.onConfigurationChanged(newConfig);
    }
}
複製程式碼

這裡我們討論的是為什麼當我們的介面在裝置配置發生變化的時候(螢幕旋轉),有時候並不會回撥onConfigurationChanged呢?

關於Activity的官方文件有下面一句話:

Activity官方文件.png

也就是說,在裝置配置發生變化的時候,會回撥onConfigurationChanged,但是前提條件是當你的Activity(元件)還在執行的時候。

這就很明顯了,說明一旦你的介面暫停以後就不會回撥這個方法了。但是這樣會導致一個問題,就是你的介面跳轉到其他介面的時候(當前介面暫停),然後發生了一次螢幕旋轉,再返回的時候,你的介面雖然旋轉了,但是並沒有回撥onConfigurationChanged方法,並沒有執行你的UI適配程式碼。

###原始碼分析

想到四大元件,我們第一時間應該會想到AMS(ActivityManagerService),沒錯,今天我們的始發站就是AMS。

在AMS裡面搜尋了一下關鍵字Configuration,發現了updateConfigurationLocked這個方法(沒有說明的情況下,都是隻給出省略版): 相信眼尖的朋友一定會看出來,在這裡由AMS建立了Configuration物件,然後通過程式間通訊,通知我們的app程式。

private boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,
        boolean initLocale, boolean persistent, int userId, boolean deferResume) {
    int changes = 0;

    if (mWindowManager != null) {
        mWindowManager.deferSurfaceLayout();
    }
    if (values != null) {
        //建立Configuration物件
        Configuration newConfig = new Configuration(mConfiguration);
        changes = newConfig.updateFrom(values);
        if (changes != 0) {
            for (int i=mLruProcesses.size()-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                try {
                    if (app.thread != null) {
                        //通過程式間通訊,通知我們的app程式
                        app.thread.scheduleConfigurationChanged(configCopy);
                    }
                } catch (Exception e) {
                }
            }
        }
    }
}
複製程式碼

thread是一個IApplicationThread物件,繼承了IInterface介面,也就是說是一個AIDL物件,實際上這個介面的實現類是ActivityThread裡面的內部類ApplicationThread。

public interface IApplicationThread extends IInterface {
}
複製程式碼

那麼就是說這時候AMS通過IApplicationThread進行了程式間通訊,實際上呼叫了我們APP所在的程式的ActivityThread裡面的內部類ApplicationThread的scheduleConfigurationChanged方法:

public void scheduleConfigurationChanged(Configuration config) {
    updatePendingConfiguration(config);
    sendMessage(H.CONFIGURATION_CHANGED, config);
}
複製程式碼

這個方法很簡單,就是傳送訊息給我們應用程式的系統Handler,然後由它來處理訊息,下面繼續分析處理訊息的過程:

case CONFIGURATION_CHANGED:
    mCurDefaultDisplayDpi = ((Configuration)msg.obj).densityDpi;
    mUpdatingSystemConfig = true;
    handleConfigurationChanged((Configuration)msg.obj, null);
    mUpdatingSystemConfig = false;
    break;
複製程式碼

這裡繼續呼叫了handleConfigurationChanged方法:

final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {

    //收集需要回撥onConfigurationChanged的元件資訊
    ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config);

    if (callbacks != null) {
        final int N = callbacks.size();
        for (int i=0; i<N; i++) {
            ComponentCallbacks2 cb = callbacks.get(i);
            if (cb instanceof Activity) {
                //如果當前迴圈的元件是Activity,那麼回撥Activity的onConfigurationChanged
                Activity a = (Activity) cb;
                performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()),
                        config, REPORT_TO_ACTIVITY);
            } else {
                //如果當前迴圈不是Activity,比如說是Service等,也需要回撥
                performConfigurationChanged(cb, null, config, null, REPORT_TO_ACTIVITY);
            }
        }
    }
}
複製程式碼

這個方法首先收集需要回撥onConfigurationChanged的元件資訊,如果當前迴圈的元件是Activity,那麼通過呼叫performConfigurationChangedForActivity方法回撥Activity的onConfigurationChanged。 如果當前迴圈不是Activity,比如說是Service等,也需要performConfigurationChanged進行相應回撥。

下面我們先看performConfigurationChangedForActivity這個方法:

private void performConfigurationChangedForActivity(ActivityClientRecord r,
                                                    Configuration newBaseConfig,
                                                    boolean reportToActivity) {
    r.tmpConfig.setTo(newBaseConfig);
    if (r.overrideConfig != null) {
        r.tmpConfig.updateFrom(r.overrideConfig);
    }
    performConfigurationChanged(r.activity, r.token, r.tmpConfig, r.overrideConfig,
            reportToActivity);
    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
}
複製程式碼

實際上也會呼叫performConfigurationChanged方法,這裡最終會回撥Activity的onConfigurationChanged方法:

private void performConfigurationChanged(ComponentCallbacks2 cb,
                                         IBinder activityToken,
                                         Configuration newConfig,
                                         Configuration amOverrideConfig,
                                         boolean reportToActivity) {
    Activity activity = (cb instanceof Activity) ? (Activity) cb : null;

    if (shouldChangeConfig) {

        if (reportToActivity) {
            final Configuration configToReport = createNewConfigAndUpdateIfNotNull(
                    newConfig, contextThemeWrapperOverrideConfig);

            //回撥Activity的onConfigurationChanged方法
            cb.onConfigurationChanged(configToReport);
        }

        //這裡有個注意點,就是我們需要先呼叫super的onConfigurationChanged方法,父類的方法中會把mCalled置為true。
        //因為上文提到,父類的方法需要進行一次分發。否則就會丟擲SuperNotCalledException。
        if (activity != null) {
            if (reportToActivity && !activity.mCalled) {
                throw new SuperNotCalledException(
                        "Activity " + activity.getLocalClassName() +
                        " did not call through to super.onConfigurationChanged()");
            }
            activity.mConfigChangeFlags = 0;
            activity.mCurrentConfig = new Configuration(newConfig);
        }
    }
}
複製程式碼

這裡有個注意點,就是我們需要先呼叫super的onConfigurationChanged方法,父類的方法中會把mCalled置為true。 因為上文提到,父類的方法需要進行一次分發。否則就會丟擲SuperNotCalledException。

我們的問題還沒有解決,就是為什麼我們的元件在暫停以後並不會回撥呢?問題的核心程式碼就出在收集元件資訊的時候,我們回到ActivityThread的系統Handler的handleConfigurationChanged方法中:

//收集需要回撥onConfigurationChanged的元件資訊
ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config);
複製程式碼

這裡收集了元件的資訊,下面我們點進去collectComponentCallbacks這個方法繆一眼:

ArrayList<ComponentCallbacks2> collectComponentCallbacks(
        boolean allActivities, Configuration newConfig) {

    //初始化一個ArrayList用於儲存需要回撥的元件資訊
    ArrayList<ComponentCallbacks2> callbacks
            = new ArrayList<ComponentCallbacks2>();

    synchronized (mResourcesManager) {
        //拿到所有Application物件
        final int NAPP = mAllApplications.size();
        for (int i=0; i<NAPP; i++) {
            callbacks.add(mAllApplications.get(i));
        }
        final int NACT = mActivities.size();
        for (int i=0; i<NACT; i++) {
            //拿到所有Activity
            ActivityClientRecord ar = mActivities.valueAt(i);
            Activity a = ar.activity;
            if (a != null) {
                Configuration thisConfig = applyConfigCompatMainThread(
                        mCurDefaultDisplayDpi, newConfig,
                        ar.packageInfo.getCompatibilityInfo());
                if (!ar.activity.mFinished && (allActivities || !ar.paused)) {
                    // If the activity is currently resumed, its configuration
                    // needs to change right now.
                    //如果當前的Activity是resumed狀態的時候,需要馬上回撥
                    callbacks.add(a);
                } else if (thisConfig != null) {
                    ar.newConfig = thisConfig;
                }
            }
        }
        //收集所有的Service資訊
        final int NSVC = mServices.size();
        for (int i=0; i<NSVC; i++) {
            callbacks.add(mServices.valueAt(i));
        }
    }
    //收集所有Content Provider
    synchronized (mProviderMap) {
        final int NPRV = mLocalProviders.size();
        for (int i=0; i<NPRV; i++) {
            callbacks.add(mLocalProviders.valueAt(i).mLocalProvider);
        }
    }

    return callbacks;
}
複製程式碼

這個方法初始化一個ArrayList用於儲存需要回撥的元件資訊,然後收集了當前應用的所有Application物件(多程式的時候可能就會有多個),Activity,Service,Content Provider資訊,然後進行下一步回撥。

###關鍵是在收集Activity的時候,進行了一次判斷:

if (!ar.activity.mFinished && (allActivities || !ar.paused)) {
    // If the activity is currently resumed, its configuration
    // needs to change right now.
    //如果當前的Activity是resumed狀態的時候,需要馬上回撥
    callbacks.add(a);
複製程式碼

經過原始碼的分析,已經可以得出這個結論就是: 當Activity已經Finish掉或者已經暫停的時候,並不會把這個Activity新增進來,這樣做是為了保證系統的效率,只去處理那些活躍(resume)的Activity,其他的不處理。

###解決辦法

辦法一:

我們可以《螢幕旋轉的適配問題以及遇到的一些坑》這篇文章中提到的,通過自定義廣播的方式去接收android.intent.action.CONFIGURATION_CHANGED這個廣播。 注意這個廣播只能夠在Java程式碼中註冊才會有效果。

辦法二:

重寫Activity的onRestart代替onConfigurationChanged方法,只不過需要判斷一下當前的螢幕方向。

@Override
protected void onRestart() {
    super.onRestart();
    if (isLandOrientation()) {
        //橫屏

    } else {
        //豎屏

    }
}

public boolean isLandOrientation() {
    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
        return true;
    } else {
        return false;
    }
}
複製程式碼

自己手動判斷一下橫豎屏即可。

如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:

公眾號:Android開發進階

我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)

相關文章