Activity 啟動回調(diào)及View.post分析

Android 源碼 (http://androidxref.com/
先看下面這段代碼,目的是獲取TextView的尺寸。

public class MainActivity extends AppCompatActivity {
    TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.text) ;

        Log.d("MainActivity", "onCreate textView:" + textView.getWidth());
    }

    @Override
    public void onResume(){
        super.onResume();
        Log.d("MainActivity", "onResume textView:" + textView.getWidth());

        new Handler().post(new Runnable() {
            @Override
            public void run() {
                Log.d("MainActivity", "onResume Handler.post textView:" + textView.getWidth());
            }
        });

        textView.post(new Runnable() {
            @Override
            public void run() {
                Log.d("MainActivity", "onResume textView.post textView:" + textView.getWidth());
            }
        });

    }
}

打印結(jié)果如下:

D/MainActivity: onCreate textView:0
D/MainActivity: onResume textView:0
D/MainActivity: onResume Handler.post textView:0
D/MainActivity: onResume textView.post textView:210

可見只有最后的View.post可以獲取尺寸,其他方式不能。大家都知道在onCreate里是獲取不到控件的尺寸的,但是為什么onResume里也不能獲取,Handler().post方式也不能獲取呢。
先看下onResume里為什么不能,這里涉及到Activity啟動流程,當(dāng)調(diào)用startActivity后,經(jīng)過binder機(jī)制,會通過ActivityThread的Handler對象mH發(fā)送message,最終走到handleLaunchActivity方法, Activity就在此方法里被創(chuàng)建:

 @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
    ...
    final Activity a = performLaunchActivity(r, customIntent);
    handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
    return a;
    ...
}

performLaunchActivity創(chuàng)建完了Activity后,通過handleResumeActivity走onResume生命周期回調(diào),這里我們沒有看到onCreate回調(diào),猜測onCreate回調(diào)在performLaunchActivity里。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
     ...
     Activity activity = null;     

     java.lang.ClassLoader cl = appContext.getClassLoader();
     activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

    Application app = r.packageInfo.makeApplication(false, mInstrumentation);
    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);
    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
    ...
    return activity;
}

可見上面方法完成了生成了Activity對象,生成了Application,同時回調(diào)了Activity的OnCreate方法,再看attach方法:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
         ...
         mWindow = new PhoneWindow(this, window);
         mWindow.setWindowControllerCallback(this);
         mWindow.setCallback(this);
         ...
}

上面創(chuàng)建了個Window,并且Window的回調(diào)設(shè)為Actvity,這就是為什么Activity能接收按鍵反饋的原因。從上面分析看出,performLaunchActivity完成了Application, Activity的創(chuàng)建,還有Activity的OnCreate回調(diào)。performLaunchActivity走完了,就開始走h(yuǎn)andleResumeActivity,

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
r = performResumeActivity(token, clearHide, reason);//回調(diào)onResume
 if (r.window == null && !a.mFinished && willBeVisible) {//下面就是將UI顯示出來,并經(jīng)過measure,layout,draw流程
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient && !a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);//UI繪制流程開始的地方
                }
              
}
}

上面關(guān)于Window的那段很長,主要就是通過ViewRootImpl將UI顯示出來,注意到流程沒有,是使用performResumeActivity來回調(diào)onResume,然后再顯示UI,所以在onResume里去取得控件尺寸,當(dāng)然是失敗的,這時候控件都沒有走measure,layout ,draw流程,這個流程的發(fā)起者就是這里的ViewRootImpl。
通過以上分析我們明白了為什么onCreate,onResume里不能取得控件尺寸了。

這里分析下UI繪制流程,為分析那兩個post結(jié)果不同做準(zhǔn)備,從上面的wm.addView(decor, l);開始,這個方法的功能是把最頂層的decorView加入Window。進(jìn)去addView看看就知道,Window把這個任務(wù)交給了ViewRootImpl,所以上面說發(fā)起者是ViewRootImpl,ViewRootImpl調(diào)用setView方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ....
     requestLayout();
    ...
}
 public void requestLayout() {
    scheduleTraversals();
}
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().
             getQueue().postSyncBarrier();//發(fā)送同步障礙消息
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//發(fā)起繪制消息
            ...
        }
}
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
 }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

   void doTraversal() {
          ...
          performTraversals();
          ...
        }
}
    

總結(jié)上面的繪制流程,就是ViewRootImpl通過Handler先發(fā)個同步障礙消息,再發(fā)個真正繪制消息(TraversalRunnable封裝的),最后由performTraversals執(zhí)行真正的繪制任務(wù)。

這里明白為什么通過Handler.post不能取得控件尺寸了吧?因?yàn)镠andler.post在onResume里先發(fā),繪制消息后發(fā),所以Handler.post的消息先回調(diào),這時控件還沒繪制,所以得不到尺寸。

那么為什么View.post方式可以取得控件尺寸呢?看看View.post發(fā)生了什么:

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        // Postpone the runnable until we know on which thread it needs to run.
        // Assume that the runnable will be successfully placed after attach.
        getRunQueue().post(action);
        return true;
 }

 private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
  }

上面代碼意思是attachInfo如果不空,就用attachInfo的mHandler發(fā)消息,否則就把消息先保存著。這時候在onResume回調(diào)里,attachInfo還是空的,在dispatchAttachedToWindow時,attachInfo才被賦值。
可見Runnable并沒有像Handler那樣被post到消息隊(duì)列里,getRunQueue().post(action);注釋上也有說推遲(Postpone the runnable until we know on which thread it needs to run.),簡單說就是View自帶個HandlerActionQueue來保存post的Runnable,初始時可以放4個Runnable,這個查源碼就知道了。
現(xiàn)在的問題就是放在HandlerActionQueue的Runnable什么時候執(zhí)行了。
答案在上面說的dispatchAttachedToWindow方法里,查看View的dispatchAttachedToWindow方法:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    ...
     // Transfer all pending runnables.
       if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);//發(fā)送之前保存的Runnable消息
            mRunQueue = null;
        }
     onAttachedToWindow();
    ...
}

通過info.mHandler把之前保存的runnable發(fā)送出去。之前分析的繪制流程最后一步是performTraversals,dispatchAttachedToWindow就是在這個方法里被調(diào)用:

 private void performTraversals() {
    host.dispatchAttachedToWindow(mAttachInfo, 0);//通過ViewGroup一層層傳給child,看源碼就知道了
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
}

可見View.post先把之前保存的Runnable消息發(fā)送給消息隊(duì)列,然后執(zhí)行performMeasure,performLayout,performDraw三個真正的繪制方法,當(dāng)Runnable消息回調(diào)時,當(dāng)然就可以取得控件尺寸了。

額外的東西:
同步障礙
Handler 消息分為同步消息和異步消息,在之前的分析中,Handler在發(fā)繪制消息前,先發(fā)了個同步障礙消息,同步障礙消息和普通Message不同是它的target為空,而普通Message的target就是Handler自身。之所以先發(fā)同步障礙,是為了在消息隊(duì)列里跳過同步消息,先處理異步消息,繪制消息是異步消息,所以繪制流程能更快的執(zhí)行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 【Android Handler 消息機(jī)制】 前言 在Android開發(fā)中,我們都知道不能在主線程中執(zhí)行耗時的任務(wù)...
    Rtia閱讀 4,897評論 1 28
  • 轉(zhuǎn)載于:請叫我大蘇的 Android屏幕刷新機(jī)制 我主要的目的是跟著文章的思路從新走一遍,讓自己更好的理解相關(guān)的知...
    ghroost閱讀 2,104評論 2 11
  • 本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布 這次就來梳理一下 Android 的屏幕刷新機(jī)...
    請叫我大蘇閱讀 25,758評論 48 205
  • 家住19層。蚊子依然肆虐。想了很久,也不知道這丫們是從哪里擠進(jìn)來的。 門是開了就關(guān)的。 窗戶也有很細(xì)的紗窗。 而且...
    踏歌徐行閱讀 250評論 1 0
  • 泰安第十六中學(xué) 李樹偉 校園里有許多樹,迎春、櫻花、紫薇、側(cè)柏、雪松,而我獨(dú)愛校園一角的那棵老槐。 老槐有...
    冬日雨花閱讀 612評論 0 1