Android窗口機制(四)ViewRootImpl與View和WindowManager

Android窗口機制系列

Android窗口機制(一)初識Android的窗口結(jié)構(gòu)
Android窗口機制(二)Window场躯,PhoneWindow,DecorView郎汪,setContentView源碼理解
Android窗口機制(三)Window和WindowManager的創(chuàng)建與Activity
Android窗口機制(四)ViewRootImpl與View和WindowManager
Android窗口機制(五)最終章:WindowManager.LayoutParams和Token以及其他窗口Dialog森瘪,Toast

在前篇第(三)文章中,我們講到了在DecorView在handleResumeActivity方法中被綁定到了WindowManager吵血,也就是調(diào)用了windowManager.addView(decorView)谎替。而WindowManager的實現(xiàn)類是WindowManagerImpl,而它則是通過WindowManagerGlobal代理實現(xiàn)addView的蹋辅,我們看下addView的方法

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        View panelParentView = null;
        
        ...
            
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        //ViewRootImpl開始繪制view
        root.setView(view, wparams, panelParentView);
        ...
    }

可以看到在WindowManagerGlobal的addView中钱贯,最后是調(diào)用了ViewRootImpl的setView方法,那么這個ViewRootImpl到底是什么侦另。

ViewRootImpl

看到ViewRootImpl想到可能會有ViewRoot類秩命,但是看了源碼才知道,ViewRoot類在Android2.2之后就被ViewRootImpl替換了褒傅。我們看下說明

/* The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 */

ViewRootImpl是一個視圖層次結(jié)構(gòu)的頂部弃锐,它實現(xiàn)了View與WindowManager之間所需要的協(xié)議,作為WindowManagerGlobal中大部分的內(nèi)部實現(xiàn)殿托。這個好理解霹菊,在WindowManagerGlobal中實現(xiàn)方法中,都可以見到ViewRootImpl碌尔,也就說WindowManagerGlobal方法最后還是調(diào)用到了ViewRootImpl浇辜。addView,removeView,update調(diào)用順序
WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl

我們看下前面調(diào)用到了viewRootImpl的setView方法

  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                ...
                // Schedule the first layout -before- adding to the window  
                // manager, to make sure we do the relayout before receiving  
                // any other events from the system.
                requestLayout();
                ...
                try {
                ...
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } 
    }

在setView方法中券敌,
首先會調(diào)用到requestLayout(),表示添加Window之前先完成第一次layout布局過程柳洋,以確保在收到任何系統(tǒng)事件后面重新布局待诅。requestLayout最終會調(diào)用performTraversals方法來完成View的繪制。

接著會通過WindowSession最終來完成Window的添加過程熊镣。在下面的代碼中mWindowSession類型是IWindowSession卑雁,它是一個Binder對象,真正的實現(xiàn)類是Session绪囱,也就是說這其實是一次IPC過程测蹲,遠(yuǎn)程調(diào)用了Session中的addToDisPlay方法。

 @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

這里的mService就是WindowManagerService鬼吵,也就是說Window的添加請求扣甲,最終是通過WindowManagerService來添加的。

View通過ViewRootImpl來繪制

前面說到齿椅,ViewRootImpl調(diào)用到requestLayout()來完成View的繪制操作琉挖,我們看下源碼

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

View繪制,先判斷當(dāng)前線程

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

如果不是當(dāng)前線程則拋出異常涣脚,這個異常是不是感覺很熟悉啊示辈。沒錯,當(dāng)你在子線程更新UI沒使用handler的話就會拋出這個異常

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

拋出地方就是這里遣蚀,一般在子線程操作UI都會調(diào)用到view.invalidate矾麻,而View的重繪會觸發(fā)ViewRootImpl的requestLayout,就會去判斷當(dāng)前線程芭梯。

接著看险耀,判斷完線程后,接著調(diào)用scheduleTraversals()

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

scheduleTraversals中會通過handler去異步調(diào)用mTraversalRunnable接口

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

接著

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

可以看到粥帚,最后真正調(diào)用繪制的是performTraversals()方法胰耗,這個方法很長核心便是

private void performTraversals() {  
        ......  
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ......  
        performDraw();
        }
        ......  
    }  

而這個方法各自最終調(diào)用到的便是

        ......  
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  
        ....
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
        ......  
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());  
        ......  
mView.draw(canvas);  

會開始觸發(fā)測量繪制。
performTraversals方法會經(jīng)過measure芒涡、layout和draw三個過程才能將一個View繪制出來,所以View的繪制是ViewRootImpl完成的卖漫,另外當(dāng)手動調(diào)用invalidate费尽,postInvalidate,requestInvalidate也會最終調(diào)用performTraversals羊始,來重新繪制View旱幼。

View與WindowManager聯(lián)系

那么View和WindowManager之間是怎么通過ViewRootImpl聯(lián)系的呢。

從第三篇文章中我們知道突委,WindowManager是繼承于ViewManager接口的柏卤,而ViewManager提供了添加View冬三,刪除View,更新View的方法缘缚。就拿setContentView來說勾笆,當(dāng)Activity的onCreate調(diào)用到了setContentView后,view就會被繪制了嗎桥滨?肯定不是窝爪,setContentView只是把需要添加的View的結(jié)構(gòu)添加保存在DecorView中。此時的DecorView還并沒有被繪制(沒有觸發(fā)view.measure,layout,draw)齐媒。

DecorView真正的繪制顯示是在activity.handleResumeActivity方法中DecorView被添加到WindowManager時候蒲每,也就是調(diào)用到windowManager.addView(decorView)。而在windowManager.addView方法中調(diào)用到windowManagerGlobal.addView喻括,開始創(chuàng)建初始化ViewRootImpl邀杏,再調(diào)用到viewRootImpl.setView,最后是調(diào)用到viewRootImpl的performTraversals來進(jìn)行view的繪制(measure,layout,draw)唬血,這個時候View才真正被繪制出來淮阐。

這也就是為什么我們在onCreate方法中調(diào)用view.getMeasureHeight() = 0的原因,我們知道activity.handleResumeActivity最后調(diào)用到的是activity的onResume方法刁品,但是按上面所說在onResume方法中調(diào)用就可以得到了嗎泣特,答案肯定是否定的,因為ViewRootImpl繪制View并非是同步的挑随,而是異步(Handler)状您。

難道就沒有得監(jiān)聽了嗎?相信大家以前獲取使用的大多是

view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
    // TODO Auto-generated method stub
             
    }
});

沒錯兜挨,的確是這個膏孟,為什么呢,因為在viewRootImpl的performTraversals的繪制最后拌汇,調(diào)用了

 {
        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
        ...
        performDraw();
}

dispatchOnGlobalLayout會觸發(fā)OnGlobalLayoutListener的onGlobalLayout()函數(shù)回調(diào)
但此時View并還沒有繪制顯示出來柒桑,只是先調(diào)用了measure和layout,但也可以得到它的寬高了噪舀。

Paste_Image.png

另外魁淳,前面說到,ViewRootImpl在調(diào)用requestLayout準(zhǔn)備繪制View的時候會先判斷線程与倡,這里我們前面分析了界逛,但也只是分析了一點。

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

為什么這么說呢纺座?
先看Activity下這段代碼

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);
        new Thread(new Runnable() {
            @Override
            public void run() {
                tv.setText("Hohohong Test");
            }
        }).start();
    }

我是在onCreate里面的子線程去更新UI的息拜,那么會報錯嗎?測試后你就會知道不會報錯,如果你放置個Button點擊再去調(diào)用的話則會彈出報錯少欺。為什么會這樣喳瓣?
答案就是跟ViewRootImpl的初始化有關(guān),因為在onCreate的時候此時View還沒被繪制出來赞别,ViewRootImpl還未創(chuàng)建出來畏陕,它的創(chuàng)建是在activity.handleResumeActivity的調(diào)用到windowManager.addView(decorView)時候,如前面說的ViewRootImpl才被創(chuàng)建起來

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        ...
           
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        //ViewRootImpl保存在一個集合List中
        mRoots.add(root);
        mParams.add(wparams);
        //ViewRootImpl開始繪制view
        root.setView(view, wparams, panelParentView);
        ...
    }

此時創(chuàng)建完才會去判斷線程氯庆。是不是有種讓你豁然開朗的感覺蹭秋!

View與ViewRootImpl的綁定

另外View和ViewRootImpl是怎么綁定在一起的呢?通過view.getViewRootImpl可以獲取到ViewRootImpl堤撵。

    public ViewRootImpl getViewRootImpl() {
        if (mAttachInfo != null) {
            return mAttachInfo.mViewRootImpl;
        }
        return null;
    }

而這個AttachInfo則是View里面一個靜態(tài)內(nèi)部類仁讨,它的構(gòu)造方法

   AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
            mSession = session;
            mWindow = window;
            mWindowToken = window.asBinder();
            mDisplay = display;
            mViewRootImpl = viewRootImpl;
            mHandler = handler;
            mRootCallbacks = effectPlayer;
        }

可以看到viewRootImpl在它的構(gòu)造方法里賦值了,那么這個方法肯定是在ViewRootImpl創(chuàng)建時創(chuàng)建的实昨,而ViewRootImpl的創(chuàng)建是在調(diào)用WindowManagerGlobal.addView的時候

     root = new ViewRootImpl(view.getContext(), display);

而構(gòu)造方法中

 public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        ...
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
        ...
    }

可以看到View與ViewRootImpl綁定一起了洞豁。
之后就可以通過view.getViewRootImpl獲取到,而在Window里面也可以獲取到ViewRootImpl荒给,因為Window里面有DecorView(這里說的Window都是講它的實現(xiàn)類PhoneWindo)丈挟,前三篇已經(jīng)介紹過了,通過DecorView來獲取到ViewRootImpl

 private ViewRootImpl getViewRootImpl() {
        if (mDecor != null) {
            ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
            if (viewRootImpl != null) {
                return viewRootImpl;
            }
        }
        throw new IllegalStateException("view not added");
    }

另外志电,一個View會對應(yīng)一個ViewRootImpl嗎曙咽?我們做個測試,在一個布局中打印兩個不同控件的ViewRootImpl的內(nèi)存地址

  Log.e(TAG, "getViewRootImpl: textView: " + tv.getViewRootImpl() );
  Log.e(TAG, "getViewRootImpl: button: " + btn.getViewRootImpl() );

結(jié)果

Paste_Image.png

可以看到挑辆,都是同一個對象例朱,共用一個ViewRootImpl。

小結(jié)

  • 之所以說ViewRoot是View和WindowManager的橋梁鱼蝉,是因為在真正操控繪制View的是ViewRootImpl洒嗤,View通過WindowManager來轉(zhuǎn)接調(diào)用ViewRootImpl
  • 在ViewRootImpl未初始化創(chuàng)建的時候是可以進(jìn)行子線程更新UI的,而它創(chuàng)建是在activity.handleResumeActivity方法調(diào)用魁亦,即DecorView被添加到WindowManager的時候
  • ViewRootImpl繪制View的時候會先檢查當(dāng)前線程是否是主線程渔隶,是才能繼續(xù)繪制下去

ViewRootImpl的功能可不只是繪制,它還有事件分發(fā)的功能洁奈,想要了解的深入的話可以看下
ViewRootImpl源碼分析事件分發(fā)

下篇文章將介紹Dialog间唉,PopWindow,Toast這些窗口機制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睬魂,一起剝皮案震驚了整個濱河市终吼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌氯哮,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喉钢,居然都是意外死亡姆打,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門肠虽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幔戏,“玉大人,你說我怎么就攤上這事税课∠醒樱” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵韩玩,是天一觀的道長垒玲。 經(jīng)常有香客問我,道長找颓,這世上最難降的妖魔是什么合愈? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮击狮,結(jié)果婚禮上佛析,老公的妹妹穿的比我還像新娘。我一直安慰自己彪蓬,他們只是感情好寸莫,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著档冬,像睡著了一般膘茎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捣郊,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天辽狈,我揣著相機與錄音,去河邊找鬼呛牲。 笑死刮萌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的娘扩。 我是一名探鬼主播着茸,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼琐旁!你這毒婦竟也來了涮阔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤灰殴,失蹤者是張志新(化名)和其女友劉穎敬特,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡伟阔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年辣之,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皱炉。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡怀估,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出合搅,到底是詐尸還是另有隱情多搀,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布灾部,位于F島的核電站康铭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏梳猪。R本人自食惡果不足惜麻削,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望春弥。 院中可真熱鬧呛哟,春花似錦、人聲如沸匿沛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逃呼。三九已至鳖孤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抡笼,已是汗流浹背苏揣。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留推姻,地道東北人平匈。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像藏古,于是被迫代替她去往敵國和親增炭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

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