Android View體系

之前零散寫了一些宪拥,這篇算是總集篇。大概涉及到

  • setContentView()
  • SubDecor铣减、Decorview她君、ContentView
  • PhoneWindow、WindowManager
  • ViewRootImpl葫哗、Choreographer
  • Toast.show()缔刹、Dialog.show()
  • View.invalidate()、View.requestLayout()
  • View.post()

新寫一個(gè)Activity會(huì)調(diào)用到setContentView()方法設(shè)置布局劣针,那就以此作為切入點(diǎn)校镐。
Activity.setContentView()->AppCompatDelegateImpl.setContentView()

    public void setContentView(int resId) {
        //創(chuàng)建SubDecor
        ensureSubDecor();
        //id為content的FrameLayout
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        //移除content上所有view
        contentParent.removeAllViews();
        //加載xml布局到content
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        //回調(diào)
        mOriginalWindowCallback.onContentChanged();
    }

創(chuàng)建SubDecor這個(gè)ViewGroup(SubDecor包含ActionBar、Toolbar酿秸、ContentView等)灭翔,并將SubDecor添加到window的Decorview,Decorview就是頂層View了辣苏;找到SubDecor上id為content的FrameLayout肝箱,加載xml布局反射創(chuàng)建View樹并添加到content。

ensureSubDecor()內(nèi)部調(diào)用createSubDecor()稀蟋,createSubDecor()中調(diào)用mWindow.setContentView(subDecor)將subDecor添加到Window的Decorview煌张。

Window大家都知道,其實(shí)現(xiàn)類是PhoneWindow退客,在Activity.attach()方法中初始化骏融。而Activity.attach()在ActivityThread的performLaunchActivity()方法中調(diào)用,performLaunchActivity()中創(chuàng)建了Activity實(shí)例萌狂,加載資源档玻,調(diào)用activity.attach()然后通過Instrumentation類回調(diào)onCreate()生命周期。當(dāng)然這篇也不是分析Activity啟動(dòng)流程之類的文章茫藏,只需關(guān)注PhoneWindow误趴。

Activity.attach()

    final void attach(...){
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
    }

PhoneWindow.setContentView()

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } 
     
        mContentParent.addView(view, params);
    }

    //mContentParent
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)

    //Window.findViewById(int id)
    public <T extends View> T findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

創(chuàng)建DecorView,將SubDecor添加到DecorView务傲。installDecor()方法中可以看到mContentParent是DecorView中id為content的ViewGroup凉当,那這玩意是平常我們所說的ContentView嗎?其實(shí)不是售葡,剛看到這里我也有點(diǎn)懵逼看杭。如果這玩意是ContentView,那SubDecor已經(jīng)添加進(jìn)了ContentView挟伙,View樹再add進(jìn)來豈不是會(huì)覆蓋SubDecor中的Toolbar楼雹、Actionbar?實(shí)際上ContentView是SubDecor的子View,繼續(xù)看下去烘豹。

AppCompatDelegateImpl.createSubDecor()隨便挑一個(gè)創(chuàng)建SubDecor的分支

subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

while (windowContentView.getChildCount() > 0) {
    final View child = windowContentView.getChildAt(0);
    windowContentView.removeViewAt(0);
    contentView.addView(child);
}

windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);

精簡(jiǎn)一下很明顯了瓜贾。DecorView中id為content的windowContentView重置為NO_ID,SubDecor中id為action_bar_activity_content的contentView設(shè)置為android.R.id.content携悯;并將原DecorView中windowContentView下的子View剪切到SubDecor的ContentView祭芦。

abc_screen_toolbar.xml

<androidx.appcompat.widget.ActionBarOverlayLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/decor_content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

    <include layout="@layout/abc_screen_content_include"/>
    ...
</androidx.appcompat.widget.ActionBarOverlayLayout>

abc_screen_content_include.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <androidx.appcompat.widget.ContentFrameLayout
            android:id="@id/action_bar_activity_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:foregroundGravity="fill_horizontal|top"
            android:foreground="?android:attr/windowContentOverlay" />

</merge>

所以SubDecor是ActionBarOverlayLayout,ContentView是SubDecor中的ContentFrameLayout

setContentView()看完了憔鬼,View樹何時(shí)開始繪制真正顯示出來呢龟劲?老生常談的是onResume()之后,那就從onResume()生命周期的調(diào)用處開始看轴或。

ActivityThread.handleResumeActivity()

    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {

        //onResume回調(diào)
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        wm.addView(decor, l);
        r.activity.makeVisible();
    }

Activity.makeVisible()

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

熟悉的WindowManager.addView()昌跌。WindowManager的實(shí)現(xiàn)類是WindowManagerImpl,其通過三個(gè)接口方法addView()照雁、updateViewLayout()蚕愤、removeView()來管理View,也就是說Window是View的管理者饺蚊。

WindowManager.addView()調(diào)用到WindowManagerGlobal.addView()

        ViewRootImpl root;
        root = new ViewRootImpl(view.getContext(), display);
        root.setView(view, wparams, panelParentView, userId);

ViewRootImpl.setView()

    public void setView(...){
        ......
        requestLayout();
    }

    public void requestLayout() {
        ......
        scheduleTraversals();
    }

    void scheduleTraversals() {
        ......
        mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }

mChoreographer.postCallback()也就是執(zhí)行mTraversalRunnable這個(gè)Runnable

ViewRootImpl.mTraversalRunnable

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

ViewRootImpl.doTraversal()

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

    private void performTraversals() {
        //繪制流程
        performMeasure();
        performLayout();
        performDraw();
    }

WindowManager.addView()最終走到ViewRootImpl.performTraversals()方法萍诱,內(nèi)部依次調(diào)用performMeasure()、performLayout()污呼、performDraw()裕坊,從DecorView開始遍歷View樹對(duì)應(yīng)調(diào)用View.measure()、View.layout()燕酷、View.draw()直到View樹完成繪制流程籍凝。可以下結(jié)論苗缩,ViewRootImpl接管了繪制流程饵蒂。

這里還有個(gè)問題,mTraversalRunnable何時(shí)執(zhí)行run()方法呢酱讶,下面回看Choreographer.postCallback()退盯,經(jīng)過一些列調(diào)用,調(diào)用到內(nèi)部類FrameDisplayEventReceiver.scheduleVsync()

    public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

nativeScheduleVsync(mReceiverPtr)調(diào)用native方法發(fā)出VSync信號(hào)浴麻,回調(diào)到FrameDisplayEventReceiver.onVsync()

        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            ......
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            ......
            doFrame(mTimestampNanos, mFrame);
        }

        void doFrame(long frameTimeNanos, int frame) {
            ......
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        }

doFrame()就是執(zhí)行mTraversalRunnable走繪制流程了得问,也就是說VSync垂直同步信號(hào)到來時(shí)會(huì)執(zhí)行繪制流程刷新UI囤攀,按60幀算每16ms系統(tǒng)都會(huì)發(fā)出VSYNC信號(hào)软免。

簡(jiǎn)單總結(jié)下View樹如何顯示:在Activity的onResume()生命周期之后通過WindowManager.addView(),初始化ViewRootImpl焚挠,等待VSync垂直同步信號(hào)到來膏萧,調(diào)用到ViewRootImpl.performTraversals()開啟View樹繪制流程。

Activity是這個(gè)流程,那其它的View像Toast榛泛、Dialog呢蝌蹂?當(dāng)然也是一樣,Toast.show()曹锨、Dialog.show()都會(huì)調(diào)用到WindowManager.addView()孤个,至于之后的流程就完全和上述一致了。

Toast稍微麻煩點(diǎn)沛简,Toast.show()通過binder機(jī)制獲取NMS代理對(duì)象齐鲤,調(diào)用到NMS.enqueueToast()將Toast內(nèi)部類TN做為回調(diào)對(duì)象傳入。NMS經(jīng)過一系列調(diào)用最終回調(diào)到TN.show()顯示Toast椒楣,并在顯示Duration時(shí)長(zhǎng)后回調(diào)TN.hide()取消Toast给郊。

TN.show()

        final Handler mHandler;

        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        ......
                    }
                }
            };
        }

        public void handleShow(IBinder windowToken) {
                ......
                mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
                        mHorizontalMargin, mVerticalMargin,
                        new CallbackBinder(getCallbacks(), mHandler));
            }
        }

ToastPresenter.show()

    public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
        ......
        try {
            //重點(diǎn)
            mWindowManager.addView(mView, mParams);
        } catch (WindowManager.BadTokenException e) {
            Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
            return;
        }
    }

Dialog.show()

    public void show() {
        ......
        //DecorView
        mDecor = mWindow.getDecorView();
        //重點(diǎn)
        mWindowManager.addView(mDecor, l);
    }

依然是WindowManager.addView()。下面看改變View的屬性時(shí)調(diào)用的View.invalidate()捧灰、改變View的大小時(shí)調(diào)用的View.requestLayout()淆九。

View.invalidate()

public void invalidate() {
        invalidate(true);
    }

public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        final ViewParent p = mParent;
        p.invalidateChild(this, damage);
    }

    /**
     * The parent this view is attached to.
     * {@hide}
     *
     * @see #getParent()
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    protected ViewParent mParent;

ViewParent也就是父View,往上調(diào)用直到ViewRootImpl.invalidateChildInParent()

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        ......
        invalidateRectOnScreen(dirty);
    }

private void invalidateRectOnScreen(Rect dirty) {
        ......
        scheduleTraversals();
    }

void scheduleTraversals() {
        ......
        mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }

流程又走到了熟悉的地方毛俏,Choreographer.postCallback()炭庙,傳入mTraversalRunnable,等待Vsync信號(hào)回調(diào)mTraversalRunnable.run()走繪制流程拧抖。

View.requestLayout()

    public void requestLayout() {
        ......
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
    }

ViewRootImpl.requestLayout()

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

仍然是熟悉的流程煤搜,這里看個(gè)大概,實(shí)際調(diào)用鏈內(nèi)部代碼還是有些復(fù)雜的唧席;需知刷新UI最終都要走ViewRootImpl.performTraversals()擦盾。

View.post()

由前面流程分析可知View樹在onResume()之后才開始走繪制流程,那我們經(jīng)常使用的View.post()在onCreate()中如何獲取到View的寬高屬性呢淌哟?其實(shí)思路很簡(jiǎn)單迹卢,把post傳入的Runnable保存下來,等待View樹繪制完畢再回調(diào)run()方法徒仓,具體調(diào)用鏈就不再跟了腐碱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掉弛,隨后出現(xiàn)的幾起案子症见,更是在濱河造成了極大的恐慌,老刑警劉巖殃饿,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谋作,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡乎芳,警方通過查閱死者的電腦和手機(jī)遵蚜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門帖池,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吭净,你說我怎么就攤上這事睡汹。” “怎么了寂殉?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵囚巴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我友扰,道長(zhǎng)文兢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任焕檬,我火速辦了婚禮姆坚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘实愚。我一直安慰自己兼呵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布腊敲。 她就那樣靜靜地躺著击喂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碰辅。 梳的紋絲不亂的頭發(fā)上懂昂,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音没宾,去河邊找鬼凌彬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛循衰,可吹牛的內(nèi)容都是我干的铲敛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼会钝,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼伐蒋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起迁酸,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤先鱼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奸鬓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焙畔,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年全蝶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闹蒜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抑淫,死狀恐怖绷落,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情始苇,我是刑警寧澤砌烁,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站催式,受9級(jí)特大地震影響函喉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荣月,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一管呵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哺窄,春花似錦捐下、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至生年,卻和暖如春婴程,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抱婉。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工档叔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蒸绩。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓蹲蒲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親侵贵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子届搁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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