Android View 的繪制流程 01 - 前置流程

Android View 的繪制流程 - 開篇 MeasureSpec
Android View 的繪制流程 01 - 前置流程
Android View 的繪制流程 02 - performMeasure
Android View 的繪制流程 03 - performLayout
Android View 的繪制流程 04 - performDraw
Android View 的繪制流程總結

之前文集中學習了幾個自定義的View, 那么一定還記得三個自定義View的重要流程.

  • measure, (測量, 測量每一個 View 及 ViewGroup 的尺寸 )
  • layout,???? (擺放, 根據測量的結果及參數, 在布局上擺放每一個控件)
  • draw, ??????(繪制, 擺放好位置后, 就開始繪制, 然后顯示在屏幕上)

這個文集也主要是圍繞著三個流程來學習. 正式開始.
?

1. 前置流程

View 的繪制流程最初就是在 ActivityThread.handleLaunchActivity() 中開始.


?
?

2.1 ActivityThread.handleLaunchActivity()

在 Activity 啟動的過程中, 會調用 ActivityThread.handleLaunchActivity() 方法.
handleLaunchActivity( ) 內部調用以下方法.

  1. ActivityThread.performLaunchActivity() - 這個方法會調用 Activity 的 onCreate 方法
  2. ActivityThread.handleResumeActivity() - 這個方法會調用 Activity 的 onResume 方法.(起始)

這個方法雖是入口, 但是我們重點學習不在這里, 所以就不再貼代碼上來, 只是簡單說一下流程.


?
?

1.2 ActivityThread.handleResumeActivity()

ActivityThread 3599 行

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
     ...
    r = performResumeActivity(token, clearHide, reason);
        ...
        if (...) {
            //-------------1----------------
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            ...
            //------------2----------------
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            ...
            ...
            if (...) {
                if (...) {
                    ...
                    //-------------3--------------
                    wm.addView(decor, l);
                }
            }
              ...      
}

handleResumeActivity 方法中調用了 performResumeActivity, 我猜測里面可能會調用了 Activity 的 onResume 方法, 在這里不是重點, 不再表述.
重點是下面幾行.

第一部分
看到 View decor = r.window.getDecorView(); , 又見到了熟悉的 DecorView, 結合 Android 之 setContentView 流程 這個文集, 立刻能聯想到 DecorView 的一系列關系.

  • DecorView 在 PhoneWindow 中
  • PhoneWindow 是 Window 的唯一派生類
  • DecorView 是一個 FrameLayout.
  • DecorView 內部有一個布局, 布局內部有一個ID 為 android.id.content 的 ViewGroup.(mContentParent)
  • 執行完 setContentView 后, DecorView 中 mContentParent 被改名為 NO_ID,
  • mContentParent 中包含了一個 SubDecorView ,
  • SubDecorView 中 有一個 ContentFrameLayout, 改名為 android.id.content.
  • 我們調用 setContentView ,傳入的資源文件, 就在 SubDecorView 的 ContentFrameLayout 控件中.
  • ...

怎么樣, 有沒有想起上面的那些?
所以說在 View 的 繪制流程中, 和這個 DecorView 有著非常密切的關系.
那么現在我們可以知道, PhoneWindow 賦值給了 r.window 屬性, DecorView 賦值給了 decor 變量.
?
?
第二部分
接著看 ViewManager wm = a.getWindowManager();
a 就是 Activity , Activity 中 getWindowManager() 返回的是 WindowManager 對象 mWindowManager,

public WindowManager getWindowManager() {
        return mWindowManager;
}
public interface WindowManager extends ViewManager {
}

可是 WindowManager 只是一個接口, ViewManager 也是一個接口, 那么肯定有實現類, 那么繼續在 Activity.java 中搜索, 看是實例化的哪個類.
在 Activity.java 6955 行, 看到 mWindowManager 是在這里被賦值的.
mWindowManager = mWindow.getWindowManager();.
接著跟進去, 看 mWindowManager 在 Window.java 769 行中被賦值.
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
那么現在就知道了, WindowManagerImpl 就是 ViewManager 子類的子類 . (WindowManagerImpl 繼承自 WindowManager, WindowManager 是一個接口, 又繼承自 ViewManager),
至此, 我們得知, vm 其實可以看做是 WindowManagerImpl 對象

接著看 WindowManager.LayoutParams l = r.window.getAttributes();
這個比較簡單, 代碼跟進去發現就是 獲取 PhoneWindow 的窗口屬性
?
?
第三部分
wm.addView(decor, l);
調用 WindowManagerImpl.addView 方法, 傳入 decorView 和 PhoneWindow 的窗口屬性.


?
?

1.3 WindowManagerImpl.addView(View, ViewGroup.LayoutParams)

WindowManagerImpl.java 90 行

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

又調用了 WindowManagerGlobal 的addView 方法, 并且傳入DecorView 與 params


?
?

1.4 WindowManagerGlobal.addView(View, ViewGroup.LayoutParams, Display, Window )

WindowManagerGlobal.java 278行

//管理所有Activity 的 ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//管理所有 Activity 的 DecorView
private final ArrayList<View> mViews = new ArrayList<View>();

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
    ...
   final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    ...
    synchronized (mLock) {
        ...
        ViewRootImpl root;
        ...
        //初始化 ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);
        //添加 DecorView 到 DecorView 集合
        mViews.add(view);
        //添加 ViewRootImpl 到集合
        mRoots.add(root);
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            ...
            throw e;
        }
    }
}

WindowManager 維護著所有 Activity 的 DecorView 和 ViewRootImpl .這里初始化了一個 ViewRootImpl 又使用 ViewRootImpl 調用了 setView, 也傳入了 DecorView 和經過轉換的 WindowManager.LayoutParams.


?
?

1.5 ViewRootImpl.setView(View , WindowManager.LayoutParams , View)

ViewRootImpl.java 632 行

View mView;
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    //異步刷新View
    synchronized (this) {
        if (mView == null) {
            mView = view;
            ....
            requestLayout();
            ...
            view.assignParent(this);
            ...
        }
    }
}

首先把 DecorView 賦值給 ViewRootImpl 類成員變量 mView. 這里需要記住 mView 就是 DecorView.
然后調用了 ViewRootImpl 類 方法 requestLayout(), 請求對頁面進行布局, 對View 完成異步刷新, 在其內部執行 View 的繪制方法., 再去看 requestLayout()方法之前, 先看一下 view.assignParent(this); 這個方法將 ViewRootImpl 對象 this 作為參數調用了 View的 assignParent().

View.java 16834 行

void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RuntimeException("view " + this + " being added, but"+ " it already has a parent");
    }
}
  • 參數是 ViewParent, 而 ViewRootImpl 實現了 ViewParent 接口, 所以在這里就將 DecorView 和 ViewRootImpl 綁定起來了.
  • 每個Activity 的根布局都是 DecorView, 而 DecorView 的 Parent 又是 ViewRootImpl, 所以在子 View 里執行 invalidate() 之類的操作,需要循環找 parent 的時候, 最后都會走到 ViewRootImpl 里.

現在接著看 ViewRootImpl.requestLayout() 方法.


?
?

1.6 ViewRootImpl.requestLayout()

ViewRootImpl.java 1153 行

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

請求對頁面進行布局, 對View 完成異步刷新, 在其內部執行 View 的繪制方法.
checkThread(); 校驗當前所在的線程
scheduleTraversals() , (當我們自定義 View 調用 invalidate 的時候, 其實最后也是調用了這個方法), 這個方法是屏幕刷新的關鍵. 一起去一探究竟.


?
?

1.7 ViewRootImpl.scheduleTraversals()

Traversals /tr??v?rs(?)l/ 遍歷的意思
ViewRootImpl.java 1354 行

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

這里看到調用了 mChoreographer.postCallback 里面傳入了三個參數, 第二個參數是一個 Runnable 對象, 繼續跟蹤到這個 Runnable 中.
記住這個 boolean類型的變量mTraversalScheduled
記住這個方法 postSyncBarrier()
這兩個還有下面的 removeSyncBarrier() 將在以后另起一章來講解 Choreographer 的調用時機


?
?

1.8 ViewRootImpl.mTraversalRunnable

ViewRootImpl 6730 行

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

這個 Runnable 在 run 中調用了 doTraversal() 方法, 繼續跟蹤


?
?

1.9 ViewRootImpl.doTraversal()

ViewRootImpl.java 1377 行

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        ...
        //最最關鍵的方法
        performTraversals();
        ...
    }
}

boolean 類型變量 mTraversalScheduled 在1.7 中設置為 true, 并且調用了 postSyncBarrier(),
這里設置為了 false, 調用了 removeSyncBarrier().
現在進入最最最關鍵的方法 performTraversals()


?
?

1.10 ViewRootImpl.performTraversals() 從這里開始執行三個流程.

ViewRootImpl.java 1576 行

private void performTraversals() {
    ...
    if (...) {
        ...
        if (...) {  
            if (...) {
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);        
                 // Ask host how big it wants to be
                //測量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                             ...
                layoutRequested = true;
            }
        }
    } else {
       ...
    }
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        //擺放
        performLayout(lp, mWidth, mHeight);
                ...
    }
        ...
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
        ...
        //繪制
        performDraw();
    } else {
        ...
    }
        ...
}

performMeasure() 執行測量
performLayout() 執行擺放
performDraw() 執行繪制
跟蹤到這里, 終于看到了這三個方法. 這個方法的邏輯很復雜, 每次都會根據一些狀態來判斷走哪個流程, 有時候可能只執行某一個, 有時候可能三個都執行或者兩個. 但是不管哪個流程, 都會遍歷一遍View 樹, 因此 View 的繪制是需要遍歷很多次 View 樹, 如果界面非常復雜, 耗時就會久一點. 當然這三個流程在遍歷時, 也不一定都會遍歷View 樹, ViewGroup 在傳遞的時候, 還會根據響應的狀態判斷是否繼續向下傳遞.

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
getRootMeasureSpec : 根據 window 的布局參數計算出 root view 的 measure.
(可以理解為: 基于 phonewindow 的 layout parms, 算出 DecorView 的 measureSpec )

mWidth 和 mHeight 是 window (也就是 PhoneWindow) 的高度和寬度.
lp 是 window 的 LayoutParams 值. lp.width 和 lp.height 默認都是 MATCH_PARENT,
getRootMeasureSpec() 中又調用了 makeMeasureSpec() 將 高度和寬度值與 MATCH_PARENT 傳入. 組合后返回一個采用32位存儲的整型值 measureSpec.

根據在 Android View 的繪制流程 - 開篇 MeasureSpec 中所學習的.
現在已經得到了 DecorView 的 MeasureSpec了, 包含的模式與值分別如下.

MeasureSpec 模式 大小
childWidthMeasureSpec MeasureSpec.EXACTLY window 的寬度值
childHeightMeasureSpec MeasureSpec.EXACTLY window 的高度值

下一章將會開始學習 performMeasure() 流程.


?
?

擴展知識

  • 知識點1

為什么我們在 Activity.onCreate 與 onResume 中獲取不到 View 寬高 ?
看過上面的流程, 是不是知道了, 打開一個Activity, 當它的 onCreate 和 onResume 執行完后, 才會將它的 DecorView 與新建的一個 ViewRootImpl 綁定起來, 同時開始執行測量, 擺放, 繪制等流程. 執行完測量后, 我們自己 View 中控件才能寬高等屬性. 所以, 都還沒有進行測量, 甚至連 ViewRootImpl 都沒創建,(在1.4中創建的) 怎么會有寬高呢.
還能得到一個信息是, Activity 界面的繪制, 是在 onResume 之后.

  • 知識點2

當我們調用 View 的 invalidate() 方法, 執行重繪的時候, 內部也是要層層走到 ViewRootImpl 的 scheduleTraversals 方法里去. 然后這個方法會將遍歷繪制 View 樹的操作 preformTraversals() 封裝到 Runnable 中. 傳給 Chorerographer, 以當前的時間戳放進一個 mCallbackQueue 隊列中, 然后調用了 native 層方法向底層注冊監聽下一個屏幕刷新信號事件.

流程圖.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容