1. 程式人生 > >Android視窗機制(四)ViewRootImpl與View和WindowManager

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

Android視窗機制系列

Android視窗機制(一)初識Android的視窗結構
Android視窗機制(二)Window,PhoneWindow,DecorView,setContentView原始碼理解
Android視窗機制(三)Window和WindowManager的建立與Activity
Android視窗機制(四)ViewRootImpl與View和WindowManager
Android視窗機制(五)最終章:WindowManager.LayoutParams和Token以及其他視窗Dialog,Toast

在前篇第(三)文章中,我們講到了在DecorView在handleResumeActivity方法中被繫結到了WindowManager,也就是呼叫了windowManager.addView(decorView)。而WindowManager的實現類是WindowManagerImpl,而它則是通過WindowManagerGlobal代理實現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中,最後是呼叫了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是一個檢視層次結構的頂部,它實現了View與WindowManager之間所需要的協議,作為WindowManagerGlobal中大部分的內部實現。這個好理解,在WindowManagerGlobal中實現方法中,都可以見到ViewRootImpl,也就說WindowManagerGlobal方法最後還是呼叫到了ViewRootImpl。addView,removeView,update呼叫順序
WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl

我們看下前面呼叫到了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方法中,
首先會呼叫到requestLayout(),表示新增Window之前先完成第一次layout佈局過程,以確保在收到任何系統事件後面重新佈局。requestLayout最終會呼叫performTraversals方法來完成View的繪製。

接著會通過WindowSession最終來完成Window的新增過程。在下面的程式碼中mWindowSession型別是IWindowSession,它是一個Binder物件,真正的實現類是Session,也就是說這其實是一次IPC過程,遠端呼叫了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呼叫到requestLayout()來完成View的繪製操作,我們看下原始碼

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

View繪製,先判斷當前執行緒

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

如果不是當前執行緒則丟擲異常,這個異常是不是感覺很熟悉啊。沒錯,當你在子執行緒更新UI沒使用handler的話就會丟擲這個異常

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

丟擲地方就是這裡,一般在子執行緒操作UI都會呼叫到view.invalidate,而View的重繪會觸發ViewRootImpl的requestLayout,就會去判斷當前執行緒。

接著看,判斷完執行緒後,接著呼叫scheduleTraversals()

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

scheduleTraversals中會通過handler去非同步呼叫mTraversalRunnable介面

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

接著

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

可以看到,最後真正呼叫繪製的是performTraversals()方法,這個方法很長核心便是

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

而這個方法各自最終呼叫到的便是

        ......  
        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);  

會開始觸發測量繪製。
performTraversals方法會經過measure、layout和draw三個過程才能將一個View繪製出來,所以View的繪製是ViewRootImpl完成的,另外當手動呼叫invalidate,postInvalidate,requestInvalidate也會最終呼叫performTraversals,來重新繪製View。

View與WindowManager聯絡

那麼View和WindowManager之間是怎麼通過ViewRootImpl聯絡的呢。

從第三篇文章中我們知道,WindowManager是繼承於ViewManager介面的,而ViewManager提供了新增View,刪除View,更新View的方法。就拿setContentView來說,當Activity的onCreate呼叫到了setContentView後,view就會被繪製了嗎?肯定不是,setContentView只是把需要新增的View的結構新增儲存在DecorView中。此時的DecorView還並沒有被繪製(沒有觸發view.measure,layout,draw)。

DecorView真正的繪製顯示是在activity.handleResumeActivity方法中DecorView被新增到WindowManager時候,也就是呼叫到windowManager.addView(decorView)。而在windowManager.addView方法中呼叫到windowManagerGlobal.addView,開始建立初始化ViewRootImpl,再呼叫到viewRootImpl.setView,最後是呼叫到viewRootImpl的performTraversals來進行view的繪製(measure,layout,draw),這個時候View才真正被繪製出來。

這也就是為什麼我們在onCreate方法中呼叫view.getMeasureHeight() = 0的原因,我們知道activity.handleResumeActivity最後呼叫到的是activity的onResume方法,但是按上面所說在onResume方法中呼叫就可以得到了嗎,答案肯定是否定的,因為ViewRootImpl繪製View並非是同步的,而是非同步(Handler)。

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

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

沒錯,的確是這個,為什麼呢,因為在viewRootImpl的performTraversals的繪製最後,呼叫了

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

dispatchOnGlobalLayout會觸發OnGlobalLayoutListener的onGlobalLayout()函式回撥
但此時View並還沒有繪製顯示出來,只是先呼叫了measure和layout,但也可以得到它的寬高了。

Paste_Image.png

另外,前面說到,ViewRootImpl在呼叫requestLayout準備繪製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點選再去呼叫的話則會彈出報錯。為什麼會這樣?
答案就是跟ViewRootImpl的初始化有關,因為在onCreate的時候此時View還沒被繪製出來,ViewRootImpl還未創建出來,它的建立是在activity.handleResumeActivity的呼叫到windowManager.addView(decorView)時候,如前面說的ViewRootImpl才被建立起來

    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);
        ...
    }

此時建立完才會去判斷執行緒。是不是有種讓你豁然開朗的感覺!

View與ViewRootImpl的繫結

另外View和ViewRootImpl是怎麼繫結在一起的呢?通過view.getViewRootImpl可以獲取到ViewRootImpl。

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

而這個AttachInfo則是View裡面一個靜態內部類,它的構造方法

   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在它的構造方法裡賦值了,那麼這個方法肯定是在ViewRootImpl建立時建立的,而ViewRootImpl的建立是在呼叫WindowManagerGlobal.addView的時候

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

而構造方法中

 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都是講它的實現類PhoneWindo),前三篇已經介紹過了,通過DecorView來獲取到ViewRootImpl

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

另外,一個View會對應一個ViewRootImpl嗎?我們做個測試,在一個佈局中列印兩個不同控制元件的ViewRootImpl的記憶體地址

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

結果

Paste_Image.png

可以看到,都是同一個物件,共用一個ViewRootImpl。

小結

  • 之所以說ViewRoot是View和WindowManager的橋樑,是因為在真正操控繪製View的是ViewRootImpl,View通過WindowManager來轉接呼叫ViewRootImpl
  • 在ViewRootImpl未初始化建立的時候是可以進行子執行緒更新UI的,而它建立是在activity.handleResumeActivity方法呼叫,即DecorView被新增到WindowManager的時候
  • ViewRootImpl繪製View的時候會先檢查當前執行緒是否是主執行緒,是才能繼續繪製下去

ViewRootImpl的功能可不只是繪製,它還有事件分發的功能,想要了解的深入的話可以看下
ViewRootImpl原始碼分析事件分發

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



作者:Hohohong
連結:https://www.jianshu.com/p/9da7bfe18374
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授