本篇文章已授權微信公眾號 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的官方文件有下面一句話:
也就是說,在裝置配置發生變化的時候,會回撥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;
}
}
複製程式碼
自己手動判斷一下橫豎屏即可。
如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:
我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)。