前言
在手機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的官方文檔有下面一句話:

也就是說,在設(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