Android原始碼分析之為什麼在onCreate() 和 onResume() 獲取不到 View 的寬高
轉載自:https://www.jianshu.com/p/d7ab114ac1f7
先來看一段很熟悉的程式碼,可能在最開始接觸安卓的時候,大部分人都寫過的一段程式碼;即嘗試在 onCreate() 和 onResume() 方法中去獲取某個 View 的寬高資訊:
但是列印輸出後,我們會發現,在這兩個方法中根本獲取不到 View 的寬高資訊。
public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_test) private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTv.getHeight(); // 0 } @Override protected void onResume() { super.onResume(); mTv.getHeight(); // 0 } }
要弄清這個問題,首先需要知道程式碼中涉及到的方法具體做了什麼工作,以及具體 View 是在什麼時候完成測量的。
setContentView()
很明顯,我們在 onCreate() 方法中呼叫了 setContentView() 方法,而設定佈局這個動作會給你一種可以獲取到寬高的錯覺;那麼我們從原始碼的角度來看看,setContentView() 到底幹了點什麼。
// 1. AppCompatDelegate 的抽象方法,根據註釋,會呼叫到 Activity 的實現方法中 public abstract void setContentView(@LayoutRes int resId); // 2. Activity 的實現方法 public void setContentView(@LayoutRes int layoutResID) { // Window 是一個抽象類,其唯一實現類是 PhoneWindow getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } @Override public void setContentView(int layoutResID) { if (mContentParent == null) { // 3. 初始化 DecorView installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } ... ... } private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { // 4. 第一次載入視窗,mDecor 為空時,生成一個 DecorView 物件 // generateDecor(-1) : return new DecorView() mDecor = generateDecor(-1); ... ... } else { mDecor.setWindow(this); } if (mContentParent == null) { // 5. 初始化父佈局 mContentParent = generateLayout(mDecor); } } // 繼續跟蹤到 generateLayout(mDecor) 方法內部 protected ViewGroup generateLayout(DecorView decor) { // 此處根據設定的主題進行一些基礎設定,沒什麼決定性作用 TypedArray a = getWindowStyle(); ... ... // 接下來的一大段程式碼是根據各種主題設定預設佈局,篇幅原因,此處有大量原始碼刪減 int layoutResource; int features = getLocalFeatures(); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; setCloseOnSwipeEnabled(true); } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleDecorLayout, res, true); layoutResource = res.resourceId; } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar); } else { layoutResource = R.layout.screen_title; } } else { // 預設佈局樣式 layoutResource = R.layout.screen_simple; } // 6. 重點來了:將對應的佈局載入到 DecorView 中 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); return contentParent; } void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { // 載入資原始檔 final View root = inflater.inflate(layoutResource, null); ... ... // 7. 將 View 載入到當前 DecorView 中 addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } public void addView(View child, int index, LayoutParams params) { // 頁面發生變化的話,請求重新擺放佈局以及重新繪製 // 注意,此處的 requestLayout 是 View 的方法 requestLayout(); invalidate(true); addViewInner(child, index, params, false); }
說出來你可能不信,但是 setContentView() 到這裡就差不多結束了。
很明顯,我們並沒有發現任何關於 View 的測量的程式碼,最後的 requestLayout() 和 invalidate() 也和 View 的 measure() 關係不大,畢竟還沒測量,哪裡談得上 layout 和 draw 呢?
所以, setContentView() 和 View 的測量沒啥關係,那麼在其之後也就自然獲取不到 View 寬高的值了。
測量流程到底是從哪裡開始的
有了上面的經驗,我們已經知道,setContentView() 並不會觸發 View 的測量,而只是為 DecorView 指定了佈局;那麼接下來的問題就是,測量流程到底是從哪裡開始的呢?
我們簡單回顧一下 Activity 的啟動流程,然後來找到這個答案。
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
// 1. ActivityThread 內部類 H,處理 LAUNCH_ACTIVITY 的訊息
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
} break;
}
// 2. 直接從 ActivityThread 的 handleLaunchActivity() 開始了
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
// 3. 執行 performLaunchActivity() 方法
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
reportSizeConfigurations(r);
Bundle oldState = r.state;
// 4. 執行 handleResumeActivity() 方法
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed,
r.lastProcessedSeq, reason);
}
}
// 3. performLaunchActivity()
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 基於反射,利用 Instrumentation 物件建立當前 Activity 的例項
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
}
try {
if (activity != null) {
// attach() 方法做了一系列最基本的初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
activity.mCalled = false;
// 3.1 依然使用 Instrumentation 物件呼叫 Activity 的 onCreate() 方法
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
// 強制校驗 super 呼叫
if (!activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onCreate()");
}
}
}
return activity;
}
public void callActivityOnCreate(Activity activity, Bundle icicle,PersistableBundle persistentState) {
prePerformCreate(activity);
// 3.2 呼叫 Activity 的 performCreate() 方法
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
}
// 3.3 最終得以呼叫到實際實現的 onCreate()
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
restoreHasCurrentPermissionRequest(icicle);
onCreate(icicle, persistentState);
mActivityTransitionState.readState(icicle);
performCreateCommon();
}
// 4 performLaunchActivity() 執行完畢後,根據程式碼來看,會繼續執行 handleResumeActivity()
// 同樣的,這個方法會呼叫到一個 performResumeActivity(),在該方法內部也會最終執行到 onResume()
final void handleResumeActivity( ... ... ) {
// 最終會執行到 onResume(),不是重點
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
// 5. 執行到 WindowManagerImpl 的 addView()
// 然後會跳轉到 WindowManagerGlobal 的 addView()
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
}
}
}
public void addView( ... ... ) {
ViewRootImpl root;
synchronized (mLock) {
// 初始化一個 ViewRootImpl 的例項
root = new ViewRootImpl(view.getContext(), display);
try {
// 呼叫 setView,為 root 佈局 setView
// 其中 view 為傳下來的 DecorView 物件
// 也就是說,實際上根佈局並不是我們認為的 DecorView,而是 ViewRootImpl
root.setView(view, wparams, panelParentView);
}
}
}
// 6. 將 DecorView 載入到 WindowManager, View 的繪製流程從此刻才開始
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// 請求對 View 進行測量和繪製
// 與 setContentView() 不同,此處的方法是 ViewRootImpl 的方法
requestLayout();
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 7. 此方法內部有一個 post 了一個 Runnable 物件
// 在其中又呼叫一個 doTraversal() 方法;
// 再之後又會呼叫到 performTraversals() 方法,然後 View 的測繪流程就從此處開始了
scheduleTraversals();
}
}
private void performTraversals() {
... ...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
... ...
performLayout(lp, mWidth, mHeight);
... ...
performDraw();
... ...
}
問題到這裡就差不多得到了解答,View 的測繪流程是在 performTraversals() 才開始的;而這個方法的呼叫是在 onResume() 方法之後,所以在 onCreate() 和 onResume() 方法中拿不到 View 的寬高資訊也就很容易理解了。