Android窗口機制(一)初識Android的窗口結構
Android窗口機制(二)Window,PhoneWindow,DecorView,setContentView源碼理解
Android窗口機制(三)Window和WindowManager的建立與Activity
Android窗口機制(四)ViewRootImpl與View和WindowManager
Android窗口機制(五)最終章:WindowManager.LayoutParams和Token以及其餘窗口Dialog,Toastandroid
在前篇第(三)文章中,咱們講到了在DecorView在handleResumeActivity方法中被綁定到了WindowManager,也就是調用了windowManager.addView(decorView)。而WindowManager的實現類是WindowManagerImpl,而它則是經過WindowManagerGlobal代理實現addView的,咱們看下addView的方法canvas
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究竟是什麼。session
看到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 -> ViewRootImplide
咱們看下前面調用到了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來添加的。post
前面說到,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之間是怎麼經過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.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。
ViewRootImpl的功能可不僅是繪製,它還有事件分發的功能,想要了解的深刻的話能夠看下
ViewRootImpl源碼分析事件分發
下篇文章將介紹Dialog,PopWindow,Toast這些窗口機制
做者:Hohohong 連接:https://www.jianshu.com/p/9da7bfe18374 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。