<<Android 開發(fā)藝術(shù)探索>> Chapter 4

View的工作原理

初識ViewRoot和DectorView

首先我們給出這一節(jié)總結(jié)的結(jié)論, 然后我們再從源碼中來分析這些結(jié)論

  1. ViewRoot對應(yīng)于ViewRootImpl類,它是連接WIndowManagerDecorview的紐帶,View的三大流程均是通過ViewRoot來完成的。
  2. ActivityThread中,當(dāng)Activity對象被創(chuàng)建完畢完,會將DecorView添加到Window中,同時創(chuàng)建ViewRootImpl對象,并對二者建立關(guān)聯(lián)。
  3. View的繪制流程是從ViewRootperformTraversals方法開始的,它經(jīng)過measure、layoutdraw三個過程才能最總將一個View繪制出來。
    • measure用來測量View的寬和高
    • layout用來確定View在父容器中的放置位置
    • draw則負(fù)責(zé)將View繪制在屏幕上
  4. performTraversals()會依次調(diào)用performMeasure()、performLayout()performDraw()三個方法。
    performMeasure()中會調(diào)用measure()方法,在measure()方法中會調(diào)用onMeasure()方法,在onMeasure()方法則會對所有的子元素進(jìn)行measure()過程,這個時候measure()流程就從父容器傳遞到子元素中,這樣就完成了依次measure()過程。performLayout()performDraw()的傳遞流程和performMeasure()是類似的。
  5. measure()完成以后,可以通過getMeasureWidth()getMeasureHeight()方法來獲取到View測量的寬/高,在幾乎所有的情況下它都等于VIew的最終的寬和高。
  6. DecorView作為頂級View,有上下兩部分。 其實(shí)DecorView是一個FrameLayout,View層的事件都先經(jīng)過DecorView,然后才傳遞給我們的View。
    performTraversals工作流程圖.png

Activity的啟動是在ActivityThread里完成的,handleLaunchActivity()會依次間接的執(zhí)行到ActivityonCreate(),onStart(),onResume()。在執(zhí)行完這些后ActivityThread會調(diào)用 WindowManager#addView(),而這個addView()最終其實(shí)是調(diào)用了WindowManagerGlobaladdView()方法,我們就從這里開始看:

addView.png

WindowManager維護(hù)著所有ActivityDecorViewViewRootImpl。這里初始化了一個ViewRootImpl,然后調(diào)用了它的setView()方法,將DevorView作為參數(shù)傳遞了進(jìn)去。所以看看 ViewRootImpl中的setView()做了什么:
ViewRootImp.setView().png

在 setView() 方法里調(diào)用了 DecorView 的 assignParent() 方法,所以去看看 View 的這個方法:
View.assignParent().png

所以從上面的源碼中我們可以發(fā)現(xiàn)

  • ViewRootImpl其實(shí)是DecorViewparent, 它其實(shí)是位于window層DecorView中間的位置(證明了上面的第一條和第二條結(jié)論)

我們重新看回 ViewRootImplsetView()這個方法,這個方法里還調(diào)用了一個requestLayout()方法:

ViewRootImpl.requestLayout().png

那我們繼續(xù)跟進(jìn)看一下requestLayout()里發(fā)生了什么
ViewRootImpl.scheduleTraversals().png

mChoreographer.postCallback()這個方法,傳入了三個參數(shù),第二個參數(shù)是一個Runnable對象,先來看看這個Runnable
TraversalRunnable.png

這個Runnable做的事很簡單,就調(diào)用了一個方法,doTraversal():
ViewRootImpl.doTraversal().png

劃重點(diǎn)!!!這里執(zhí)行了performTraversals(), 還記得我們第三條結(jié)論嗎, 那我們?nèi)タ匆幌?code>performTraversals()中執(zhí)行了什么
ViewRootImpl.performTraversals().png

performTraversals()中執(zhí)行了performMeasure() performLayout()performDraw()方法.
證明了上面的第三條結(jié)論

  • 也就是說,其實(shí)打開一個Activity當(dāng)它的onCreate---onResume生命周期都走完后,才將它的 DecorView 與新建的一個 ViewRootImpl 對象綁定起來,同時開始安排一次遍歷 View 任務(wù)也就是繪制 View 樹的操作等待執(zhí)行,然后將 DecorViewparent 設(shè)置成 ViewRootImpl 對象。
    這也就是為什么在 onCreate---onResume 里獲取不到 View 寬高的原因,因?yàn)樵谶@個時刻 ViewRootImpl 甚至都還沒創(chuàng)建,更不用說是否已經(jīng)執(zhí)行過測量操作了。
  • 還可以得到一點(diǎn)信息是,一個 Activity 界面的繪制,其實(shí)是在 onResume() 之后才開始的。

理解MeasureSpec

在測量過程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后再根據(jù)這個measure來測量出View的寬/高

  1. MeasureSpec
    MesureSpec代表一個32位的int值,高2位代表SpecMode測量模式,低30位代表在該測量模式下的規(guī)格大小。設(shè)計的目的是避免過多的對象內(nèi)存分配

    SpecMode有三類:

    • UNSPECIFIED
      父容器不對View有任何限制,要多大就給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)。
    • EXACTLY
      父容器已經(jīng)檢測出View所需要的精確大小,這和時候View的最終大小就是SpecSizes所指定的值。它對應(yīng)于LayoutParams 中的match_parent和具體的數(shù)值這兩種模式
    • AT_MOST
      父容器指定了一個可用大小即SpecSize,View的大小不能大于這個值,具體是什么值要看不同View的具體體現(xiàn)。它對應(yīng)于LayoutParams中的wrap_content.
  2. MeasureSpec和LayoutParams的對應(yīng)關(guān)系

    • DecorViewMeasureSpec由窗口的尺寸和其自身的LayoutParams共同決定
      • LayoutParamsMATCH_PARENT時:DecorView的大小為窗口的大小,SpecModeEXACTLY
      • LayoutParamsWRAP_PARENT時:DecorView的大小不定, 最大為窗口的大小,SpecModeAT_MOST
      • LayoutParams為固定大小時:DecorView的大小為LayoutParams指定的大小,SpecModeEXACTLY
    • 普通ViewMeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定.
      • View采用MATCH_PARENT時,View的大小為父容器的大小,不管父容器的MeasureSpec是什么,SpecMode都與父容器的SpecMode一致,
      • View采用WRAP_PARENT時,View的大小為父容器的大小,不管父容器的MesureSpec是什么,SpecMode總是AT_MOST
      • View采用固定寬高的時候,View的大小為LayoutParams指定的大小,不管父容器的MeasureSpec是什么,SpecMode都是EXACTLY

    所以我們在使用自定義View時要注意處理自定義View的WRAP_PARENT

View的工作流程

Measure過程
  1. View的Measure過程

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),     
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    Measure過程.jpg
    • getSuggestedMinimumWidth()getSuggestedMinimumHeight()返回的是ViewminWidth屬性和Background寬度的最大值
    • getDefaultSize()返回的大小就是參數(shù)widthMeasureSpec或者heightMeasureSpec中的specSize,也就是View測量后的大小,絕大部分情況和View的最終大小(Layout階段確定)相同
    • setMeasuredDimension()方法會設(shè)置View的寬/高的測量值
    • 直接繼承View的自定義控件,需要重寫onMeasure()方法并且設(shè)置wrap_content時的自身大小,否則在布局中使用了wrap_content相當(dāng)于使用了match_parent
      解決方法:在onMeasure()時,給View指定一個內(nèi)部寬/高,并在wrap_content時設(shè)置即可,其他情況沿用系統(tǒng)的測量值即可
  2. ViewGroup的measure過程

    • View中是通過performTraversals() -> performMeasure() -> measure() -> onMeasure()來進(jìn)行測量的。
    • 對于ViewGroup來說,除了完成自己的measure過程之外,還會遍歷去調(diào)用所有子元素的measure方法,個個子元素再遞歸去執(zhí)行這個過程,和View不同的是,ViewGroup是一個抽象類,沒有重寫ViewonMeasure()方法,提供了measureChildren()方法。
      ViewGroup的measure過程
    • measure完成之后,通過getMeasureWidth/Height()方法就可以獲取View的測量寬/高,需要注意的是,在某些極端情況下,系統(tǒng)可能要多次measure才能確定最終的測量寬/高,比較好的習(xí)慣是在onLayout方法中去獲取測量寬/高或者最終寬/高。
    • 如何在Activity中獲取View的寬/高信息
      View的測量過程是在onResume()后才完成的,所以在ViewonResume()前調(diào)用getMeasureWidth/Height()方法不會得到View的寬高。下面給出4種解決方法。
      • Activity/View.onWindowFocusChanged()
        onWindowFocusChanged這個方法的含義是:View已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,需要注意:它會被調(diào)用多次,當(dāng)Activity的窗口得到焦點(diǎn)和失去焦點(diǎn)均會被調(diào)用。
      • view.post(runnable)
        通過post將一個runnable投遞到消息隊(duì)列的尾部,當(dāng)Looper調(diào)用此runnable的時候,View也初始化好了
      • ViewTreeObserver
        使用ViewTreeObserver的眾多回調(diào)可以完成這個功能,比如OnGlobalLayoutListener這個接口,當(dāng)View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時,onGlobalLayout方法會被回調(diào)。需要注意的是,伴隨著View樹狀態(tài)的改變,onGlobalLayout會被回調(diào)多次。
      • view.measure(int widthMeasureSpec,int heightMeasureSpec)
        • match_parent
          無法measure出具體的寬高,因?yàn)椴恢栏溉萜鞯氖S嗫臻g,無法測量出View的大小
        • 具體的數(shù)值(dp/px)
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
          view.measure(widthMeasureSpec,heightMeasureSpec);
          
        • wrap_content
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
          view.measure(widthMeasureSpec,heightMeasureSpec);
          
Layout過程

View的默認(rèn)實(shí)現(xiàn)中,View的測量寬/高和最終寬/高是相等的,測量寬/高形成于Viewmeasure過程,而最終寬/高形成于Viewlayout過程

Draw過程
  • View繪制到屏幕上,大概的幾個步驟:
    1. 繪制背景background.draw(canvas)
    2. 繪制自己onDraw
    3. 繪制children(dispatchDraw)
    4. 繪制裝飾onDrawScrollBars
  • View的繪制過程是通過dispatchDraw()來實(shí)現(xiàn)的,它會遍歷所有子元素的draw()方法
  • 如果一個View不需要繪制任何內(nèi)容,那么設(shè)置setWillNotDraw()true后,系統(tǒng)會進(jìn)行相應(yīng)的優(yōu)化;ViewGroup默認(rèn)為true,如果我們的自定義ViewGroup需要通過onDraw()來繪制內(nèi)容的時候,需要顯示的關(guān)閉它。

自定義View

  • 直接繼承ViewViewGroup的控件, 需要在onMeasure()中對wrap_content做特殊處理。
  • 直接繼承View的控件,如果不在draw()方法中處理padding,那么padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure()onLayout()中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。
  • View內(nèi)部提供了post系列的方法,完全可以替代Handler的作用。
  • View中有線程和動畫,需要在ViewonDetachedFromWindow()中停止。

參考:http://www.itdecent.cn/p/75dc9e4b67ae

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容