Android 源碼分析--onConfigurationChanged

前言

在手機APP開發(fā)的時候,一般默認會適配豎屏,游戲開發(fā)除外。但是在Android平板電腦開發(fā)中,屏幕旋轉(zhuǎn)的問題比較突出,可以這樣說,平板電腦的最初用意就是橫屏使用的,比較方便,用戶會經(jīng)常旋轉(zhuǎn)我們設(shè)備的屏幕。

今天主要分析一下onConfigurationChanged調(diào)用的不確定性因素。

關(guān)于這個問題,筆者在網(wǎng)上搜索了一下關(guān)于為什么onConfigurationChanged的方法不會被調(diào)用,基本都是說清單文件里面沒有正確配置,因為在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>

但是完全搜索不到關(guān)于我的問題的搜索結(jié)果,畢竟做Android平板的并不多,因此寫下來記錄自己的學習過程。

關(guān)于官方文檔

我們知道,在Activity、View(ViewGroup)、Fragment、Service、Content Provider等等在設(shè)備的配置發(fā)生變化的時候,會回調(diào)onConfigurationChanged的方法。實質(zhì)上主要是Activity中收到AMS的通知,回調(diào),然后把事件分發(fā)到Window、Fragment、ActionBar等。

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

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

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

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

    //分發(fā)到Activity的ActionBar
    if (mActionBar != null) {
        mActionBar.onConfigurationChanged(newConfig);
    }
}

這里我們討論的是為什么當我們的界面在設(shè)備配置發(fā)生變化的時候(屏幕旋轉(zhuǎn)),有時候并不會回調(diào)onConfigurationChanged呢?

關(guān)于Activity的官方文檔有下面一句話:

Activity官方文檔.jpg

也就是說,在設(shè)備配置發(fā)生變化的時候,會回調(diào)onConfigurationChanged,但是前提條件是當你的Activity(組件)還在運行的時候。

這就很明顯了,說明一旦你的界面暫停以后就不會回調(diào)這個方法了。但是這樣會導致一個問題,就是你的界面跳轉(zhuǎn)到其他界面的時候(當前界面暫停),然后發(fā)生了一次屏幕旋轉(zhuǎn),再返回的時候,你的界面雖然旋轉(zhuǎn)了,但是并沒有回調(diào)onConfigurationChanged方法,并沒有執(zhí)行你的UI適配代碼。

源碼分析

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

在AMS里面搜索了一下關(guān)鍵字Configuration,發(fā)現(xiàn)了updateConfigurationLocked這個方法(沒有說明的情況下,都是只給出省略版):
相信眼尖的朋友一定會看出來,在這里由AMS創(chuàng)建了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) {
        //創(chuàng)建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對象,實際上這個接口的實現(xiàn)類是ActivityThread里面的內(nèi)部類ApplicationThread。

public interface IApplicationThread extends IInterface {
}

那么就是說這時候AMS通過IApplicationThread進行了進程間通信,實際上調(diào)用了我們APP所在的進程的ActivityThread里面的內(nèi)部類ApplicationThread的scheduleConfigurationChanged方法:

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

這個方法很簡單,就是發(fā)送消息給我們應(yīng)用程序的系統(tǒng)Handler,然后由它來處理消息,下面繼續(xù)分析處理消息的過程:

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

這里繼續(xù)調(diào)用了handleConfigurationChanged方法:

final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {

    //收集需要回調(diào)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) {
                //如果當前循環(huán)的組件是Activity,那么回調(diào)Activity的onConfigurationChanged
                Activity a = (Activity) cb;
                performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()),
                        config, REPORT_TO_ACTIVITY);
            } else {
                //如果當前循環(huán)不是Activity,比如說是Service等,也需要回調(diào)
                performConfigurationChanged(cb, null, config, null, REPORT_TO_ACTIVITY);
            }
        }
    }
}

這個方法首先收集需要回調(diào)onConfigurationChanged的組件信息,如果當前循環(huán)的組件是Activity,那么通過調(diào)用performConfigurationChangedForActivity方法回調(diào)Activity的onConfigurationChanged。
如果當前循環(huán)不是Activity,比如說是Service等,也需要performConfigurationChanged進行相應(yīng)回調(diào)。

下面我們先看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));
}

實際上也會調(diào)用performConfigurationChanged方法,這里最終會回調(diào)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);

            //回調(diào)Activity的onConfigurationChanged方法
            cb.onConfigurationChanged(configToReport);
        }

        //這里有個注意點,就是我們需要先調(diào)用super的onConfigurationChanged方法,父類的方法中會把mCalled置為true。
        //因為上文提到,父類的方法需要進行一次分發(fā)。否則就會拋出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);
        }
    }
}

這里有個注意點,就是我們需要先調(diào)用super的onConfigurationChanged方法,父類的方法中會把mCalled置為true。
因為上文提到,父類的方法需要進行一次分發(fā)。否則就會拋出SuperNotCalledException。

我們的問題還沒有解決,就是為什么我們的組件在暫停以后并不會回調(diào)呢?問題的核心代碼就出在收集組件信息的時候,我們回到ActivityThread的系統(tǒng)Handler的handleConfigurationChanged方法中:

 callbacks = collectComponentCallbacks(false, config);

這里收集了組件的信息,下面我們點進去collectComponentCallbacks這個方法繆一眼:

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

    //初始化一個ArrayList用于存儲需要回調(diào)的組件信息
    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狀態(tài)的時候,需要馬上回調(diào)
                    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用于存儲需要回調(diào)的組件信息,然后收集了當前應(yīng)用的所有Application對象(多進程的時候可能就會有多個),Activity,Service,Content Provider信息,然后進行下一步回調(diào)。

關(guān)鍵是在收集Activity的時候,進行了一次判斷:

if (!ar.activity.mFinished && (allActivities || !ar.paused)) {
    // If the activity is currently resumed, its configuration
    // needs to change right now.
    //如果當前的Activity是resumed狀態(tài)的時候,需要馬上回調(diào)
    callbacks.add(a);

經(jīng)過源碼的分析,已經(jīng)可以得出這個結(jié)論就是:
當Activity已經(jīng)Finish掉或者已經(jīng)暫停的時候,并不會把這個Activity添加進來,這樣做是為了保證系統(tǒng)的效率,只去處理那些活躍(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:configChangesd 配置

列出 Activity 將自行處理的配置變更。在運行時發(fā)生配置變更時,默認情況下會關(guān)閉 Activity 并將其重啟,但使用該屬性聲明配置將阻止 Activity 重啟。相反,Activity 會保持運行狀態(tài),并且系統(tǒng)會調(diào)用其 onConfigurationChanged() 方法。

注意:應(yīng)避免使用該屬性,并且只應(yīng)在萬不得已的情況下使用。如需了解有關(guān)如何正確處理配置變更所致重啟的詳細信息,請閱讀處理運行時變更。

任何或所有下列字符串均是該屬性的有效值。若有多個值,則使用“|”進行分隔,例如“locale|navigation|orientation”。

說明
density 顯示密度發(fā)生變更 - 用戶可能已指定不同的顯示比例,或者有不同的顯示現(xiàn)處于活躍狀態(tài)。
在 API 級別 24 中引入。
fontScale 字體縮放系數(shù)發(fā)生變更 - 用戶已選擇新的全局字號。
keyboard 鍵盤類型發(fā)生變更 - 例如,用戶插入外置鍵盤。
keyboardHidden 鍵盤無障礙功能發(fā)生變更 - 例如,用戶顯示硬鍵盤。
layoutDirection 布局方向發(fā)生變更 - 例如,自從左至右 (LTR) 更改為從右至左 (RTL)。
在 API 級別 17 中引入。
locale 語言區(qū)域發(fā)生變更 - 用戶已為文本選擇新的顯示語言。
mcc IMSI 移動設(shè)備國家/地區(qū)代碼 (MCC) 發(fā)生變更 - 檢測到 SIM 并更新 MCC。
mnc IMSI 移動設(shè)備網(wǎng)絡(luò)代碼 (MNC) 發(fā)生變更 - 檢測到 SIM 并更新 MNC。
navigation 導航類型(軌跡球/方向鍵)發(fā)生變更。(這種情況通常不會發(fā)生。)
orientation 屏幕方向發(fā)生變更 - 用戶旋轉(zhuǎn)設(shè)備。
注意:如果應(yīng)用面向 Android 3.2(API 級別 13)或更高版本的系統(tǒng),則還應(yīng)聲明 "screenSize""screenLayout" 配置,因為當設(shè)備在縱向模式與橫向模式之間切換時,該配置也會發(fā)生變更。
screenLayout 屏幕布局發(fā)生變更 - 現(xiàn)處于活躍狀態(tài)的可能是其他顯示模式。
screenSize 當前可用屏幕尺寸發(fā)生變更。
在 API 級別 13 中引入。
該值表示目前可用尺寸相對于當前寬高比的變更,當用戶在橫向模式與縱向模式之間切換時,它便會發(fā)生變更。
smallestScreenSize 物理屏幕尺寸發(fā)生變更。
該值表示與方向無關(guān)的尺寸變更,因此它只有在實際物理屏幕尺寸發(fā)生變更(如切換到外部顯示器)時才會變化。對此配置所作變更對應(yīng) smallestWidth 配置的變化。
在 API 級別 13 中引入。
touchscreen 觸摸屏發(fā)生變更。(這種情況通常不會發(fā)生。)
uiMode 界面模式發(fā)生變更 - 用戶已將設(shè)備置于桌面或車載基座,或者夜間模式發(fā)生變更。如需了解有關(guān)不同界面模式的更多信息,請參閱 [UiModeManager](https://developer.android.google.cn/reference/android/app/UiModeManager)
在 API 級別 8 中引入。

所有這些配置變更都可能影響應(yīng)用所看到的資源值。因此,調(diào)用 onConfigurationChanged() 時,通常有必要再次檢索所有資源(包括視圖布局、可繪制對象等),以正確處理變更。

注意:如要處理所有多窗口模式相關(guān)的配置變更,請使用 "screenLayout""smallestScreenSize"。Android 7.0(API 級別 24)或更高版本的系統(tǒng)支持多窗口模式。

詳細請參考:https://developer.android.google.cn/guide/topics/manifest/activity-element

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容