Android那兩個你碰不到但是很重要的類之ViewRootImpl

前言

這兩個類就是ActivityThread和ViewRootImpl,之所以說碰不到是因為我們無法通過正常的方式引用這兩個類或者其類的對象,調用方法或者直接拿他的屬性。但他們其實又無處不在,應用開發中很多時候都和他們息息相關,閱讀他們掌握其內部實現對我們理解Android運行機理有醍醐灌頂之療效,碼讀百變其義自見,常讀常新。本文就嘗試從幾個我們經常接觸的方面先談談ViewRootImpl。

1.ViewRootImpl哪來的?

首先是ViewRootImpl,位於android.view包下,從它所處的位置大概能猜到,跟View相關。其作用一句話總結,就是連接Window和View的紐帶。

這個要從我們最熟悉的Activity開始,我們知道Activity的設置佈局View是通過setContentView() 方法這個方法裡面也大有文章,我們簡單的梳理下。

  • Activity setcontentView()內部調用瞭getWindow().setContentView(layoutResID);也就是調用瞭Window的setContentView方法,Android裡Window的唯一實現類就是PhoneWindow,PhoneWindow setContentView,初始化DecorView和把我們設置的View作為其子類。
  • 目光轉移到ActivityThread 沒錯是我們提及的另外一個主角,先關註他的handleResumeActivity()方法,裡面關鍵的部門代碼,
public void handleResumeActivity(){
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView();
    ViewManager wm = a.getWindowManager();
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    wm.addView(decor, l);
}
  • WindowManager的實現類WindowManageImpl的addView方法裡調用瞭mGlobal.updateViewLayout(view, params);
  • 最後我們在WindowManagerGlobal的addView方法裡找到瞭
public void addView(){
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
}

小結

  • 通過梳理這個過程我們知道,setContenview()其實隻是在Window的下面掛瞭一個View鏈,View鏈的根就是ViewRootImpl。
  • Window通過把View和Activity聯系在一起。
  • View鏈的真正添加操作最終交給瞭WindowManagerGlobal執行。
  • 補充一點:PopupWindow本質就是在當前Window下掛瞭一個View鏈,PopupWindow本身沒有Window,就如雷鋒塔沒有雷鋒一樣;Dialog是有自己的window關於這點可自行查閱源碼考證。

2 ViewRootImpl 一個View鏈渲染的中轉站

View的渲染是自定而上層層向下發起的,大致經歷測量佈局和繪制,View鏈的管理者就是ViewRootImpl。通過

scheduleTraversals()方法發起渲染動作。交給Choreographer安排真正執行的時間關於Choreographer不熟悉的可以參考我的其他文章。最終執行performTraversals() 方法。

private void performTraversals(){
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
}

3 不能在子線程操作View?

ViewRoot的RequestLayout中有這樣一段代碼:

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

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}
  • 我們對View的操作,比如給TextView設置text,最終都會觸發ViewRootImpl的requestLayout() 方法,該方法有如上的一個check邏輯。這就是我們常說的不能在子線程中更新View。
  • 其實子線程中可以執行View的操作,但是有個前提是:View還未掛載時。 View未掛載時時不會觸發requestLayout的,還隻是一個普普通通的java對象。那掛載邏輯在哪?

4 View 掛載

  • 在ViewRootImpl的performTraversals() 裡有這個代碼
private void performTraversals(){
    host.dispatchAttachedToWindow(mAttachInfo, 0);//此處的host為ViewGroup
}
  • ViewGroup的dispatchAttachedToWindo()方法會把AttachInfo對象分配每一個View,最終實現我們所謂的掛載。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
 
  • 實現掛載的View有任何風吹草動就會把事件傳遞到大bossViewRootImpl這裡瞭。

通過addView添加進的View也是會收到父View的mAttachInfo這裡不展開瞭。

5 View.post()的Runnable最終在哪執行瞭?

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action);
    return true;
}
  • 以上是View post()的代碼,可見如果已經實現掛載的View,會直接把post進來的消息交給Hanlder處理瞭給執行,不然就post瞭HandlerActionQueue裡。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
  ..
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);//內部也是調用handler.post()
        mRunQueue = null;
    }
    ..
}
  • 最終這些Runnable會在View掛載的時候執行,也就是dispatchAttachedToWindow()方法裡執行。

6 為什麼View.post 可以獲取寬高

  • 這個是是一個問題延伸,在Activity中直接獲取寬高是獲取不到的,我們通常會使用view.post一個Runnable來獲取。原因就是Activity onCreate時通過setContentView隻是創建瞭View而未實現掛載,掛載是在onResume時,未掛載的View其實沒有經歷測量過程。。

  • 而通過post的方式,通過上一小節知道,未掛載的View上post之後,任務會在掛載之後,通過handler重新post,此時已經ViewRootImpl已經執行瞭performTraversals()完成瞭測量自然可以得到寬高。

7 還有一點值得註意

ViewRootImpl 不單單是渲染的中轉站,還是觸摸事件的中轉站。

硬件傳感器接收到觸摸事件經過層層傳遞分發到應用窗口的第一站就是ViewRootImpl。為什麼這麼說?因為我有證據~。這是ViewRoot裡的代碼

public void setView(){
    ..
    mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
        Looper.myLooper());
}
  • WindowInputEventReceiver是ViewRootImpl的一個內部類,其接收到input事件後,就會進行事件分發。
  • 這裡給我們的啟發是,並不是所有的主線程任務執行都是通過Handler機制, onTouch()事件是底層直接回調過來的,這就和我們之前卡頓監控說的方案裡有一項就是對onTouchEvent的監控。

總結

  • ViewRoot的代碼有一萬多行,本文分析的隻是冰山一角,裡面有大量細節直接研究。
  • 通過ViewRootImpl相關幾個點,簡單的做瞭介紹分析希望對你有幫助。

以上就是Android那兩個你碰不到但是很重要的類之ViewRootImpl的詳細內容,更多關於Android ViewRootImpl的資料請關註WalkonNet其它相關文章!

推薦閱讀: