《深入理解Android:卷三》深入理解控件系統讀書筆記(中)

本篇文章承接上一篇《深入理解Android:卷三》深入理解控件系統讀書筆記(上),從繪制原理,以及動畫原理方面,繼續深入了解Android的控件系統。

6.4 深入理解控件樹的繪制

6.4.1 理解Canvas

Canvas的繪圖指令可以分為兩部分:

  • 繪制指令。這些最常用的指令由一系列名為drawXXX()的方法提供。用來實現實際的繪制行為,例如繪制點、線、圓以及方塊等
  • 輔助指令。用于提供輔助功能,會影響后續繪制指令的效果,如設置變換、裁剪區等。同時還提供了save()restore()用于撤銷一部分輔助指令

1. Canvas的繪制目標
  對軟件Canvas來說,其繪制目標是一個建立在Surface之上的位圖Bitmap

軟件Canvas的繪制目標

  當通過Surface.lockCanvas()方法獲取一個Canvas時會以Surface的內存創建一個Bitmap,通過Canvas所繪制的內容都會直接反映到Surface
  硬件Canvas的繪制目標有兩種。一種是HardwareLayer,可以將其理解為一個紋理(GL Texture),或者更簡單地認為它是一個硬件加速下的位圖(Bitmap)。另一種被稱為DisplayList,它并不是一塊Buffer,而是一個指令序列。DisplayList會將Canvas的繪制指令編譯并優化為硬件繪制指令,并且可以在需要時將這些指令回放到一個HardwareLayer上,而不需要重新使用Canvas進行繪制。
  Bitmap、HardwareLayer以及DisplayList都可以稱為Canvas的畫布


從使用角度來說,BitmapHardwareLayer十分相似。開發者可以將一個Bitmap通過Canvas繪制到另一個Bitmap上,也可以將一個HardwareLayer繪制到另一個HardwareLayer上。二者的區別僅在于使用時采用了硬件加速還是軟件加速。
另外,將DisplayList回放到HardwareLayer上,與繪制一個BitmapHardwareLayer的結果并沒有什么不同。只不過DisplayList并不像Bitmap那樣存儲了繪制的結果,而是存儲了繪制的過程。


2. 坐標變換
  Canvas提供了配套使用的save()/restore()方法用以撤銷不需要的變換。它們可以嵌套調用,在這種情況下restore()將會把坐標系狀態返回到與其配對的save所創建的保存點。另外,也可以通過保存某個save()的返回值,并將這個返回值傳遞給restoreToCount()方法的方式來顯示地指定一個保存點
  坐標系的變換,使得控件在onDraw()方法中使用Canvas時,使用的是控件自身的坐標系。而這個控件自身的坐標系就是通過Canvas的變換指令從窗口坐標系沿著控件樹一步一步變化出來的

6.4.2 View.invalidate()與臟區域

當一個控件的內容發生變化而需要重繪的時候,它會通過invalidate()方法將其需要重繪的區域沿著控件樹提交給ViewRootImpl,并保存到ViewRootImplmDirty成員中,最后通過scheduleTraversals()引發一次遍歷,進而進行重繪。在回溯過程中,會將沿途的控件標記為臟,即設置PFLAG_DIRTYPFLAGE_DIRTY_OPAQUE(不透明)兩者之一添加到View.mPrivateFlags成員中。如果控件時“實心”的,則將標記設為PFLAGE_DIRTY_OPAQUE,否則為PFLAGE_DIRTY。控件系統在重繪過程中區分這兩種標記以決定是否為此控件繪制背景,如果是實心的就會跳過背景的繪制工作從而提高效率。
  在一個方法可以連續調用多個控件的invalidate()方法,而不用擔心會由于多次重繪而產生的效率問題。另外,多次調用invalidate()方法會使得ViewRootImpl多次接收到設置臟區域的請求,ViewRootImpl會將這些臟區域累加到mDirty中,進而在隨后的“遍歷”中一次性地完成所有臟區域的重繪。

6.4.3 開始繪制

繪制控件樹的入口就在performDraw(),其工作也很簡單,一是調用draw()執行實際的繪制工作,二是在必要時,向WMS通知繪制已經完成。draw()方法中產生了硬件加速繪制和軟件繪制兩個分支,分支的條件為mAttachInfo.mHardwareRender()是否存在并且有效。在ViewRootImpl.setView()中會調用enableHardwareAcceleration()方法,倘若窗口的LayoutParams.flags中包含FLAG_HARDWARE_ACCELERATED標記,這個方法會通過HardwareRenderer.createGlRenderer()創建一個HardwareRender并保存在mAttachInfo中。因此mAttachInfo所保存的HardwareRenderer是否存在便成為區分使用硬件加速繪制還是軟件繪制的依據。

6.4.4 軟件繪制原理

軟件繪制由ViewRootImpl.drawSoftware()完成,主要分為以下4步工作:

  • 通過Surface.lockCanvas()獲取一個用于繪制的Canvas
  • Canvas進行變換以實現滾動效果
  • 通過mView.draw()將根控件繪制在Canvas
  • 通過Surface.unlockCanvasAndPost()顯示繪制后的內容
      其中,第二步和第三步是控件繪制過程中的兩個基本階段,即首先通過Canvas的變換指令將Canvas的坐標系變換到控件自身的坐標系之下,然后再通過控件的View.draw(Canvas)方法將控件的內容繪制在這個變換后的坐標系中。
      注意,在View中還有draw(Canvas)的另一個重載,即View.draw(ViewGroup,Canvas,long)。后者是在父控件的繪制過程中所調用的(參數ViewGroup是其父控件),并且參數Canvas所在的坐標系為其父控件的坐標系。View.draw(ViewGroup,Canvas,long)會根據控件的位置、旋轉、縮放以及動畫對Canvas進行坐標系的變換,是的Canvas的坐標系從父控件的坐標系變化到本控件的坐標系,并且會在變化完成后調用draw(Canvas)來在變換后的坐標系中進行繪制。當然,該重載方法除了坐標系變換,還包括了硬件加速、繪圖緩存以及動畫計算等工作。

1. 純粹的繪制:View.draw(Canvas)
  純粹的繪制主要涉及以下4步:

  • 繪制背景,注意背景不會受到滾動的影響
  • 調用onDraw()方法繪制控件自身的內容
  • 通過調用dispatchDraw()繪制子控件
  • 繪制特殊的裝飾,即滾動條

2. 確定子控件的繪制順序:dispatchDraw()
  繪制順序依次執行以下4步:

  • 設置裁剪區域。默認情況下,ViewGroup通過Canvas.clipRect()方法將子控件的繪制限制在自身的區域內。超出區域將會被裁剪。是否需要進行越界內容的裁剪取決于ViewGroup.mGroupFlags中是否包含CLIP_TO_PADDING_MASK標記。開發者可通過ViewGroup.setClipToPadding()方法修改這一行為,使得子控件超出的內容仍得以顯示
  • 遍歷繪制所有的子控件,根據mGroupFlags中是否存在FLAG_USE_CHILD_DRAWING_ORDER標記存在兩種不同的情況:
    1. 默認情況下,dispatchDraw()會按照mChildren列表的索引順序進行繪制。
    2. 倘若存在FLAG_USE_CHILD_DRAWING_ORDER標記,則表示此ViewGroup希望按照其自定義的繪制順序進行繪制。自定義的繪制順序由getChildDrawingOrder()方法實現
  • 在每次遍歷中,調用drawChild()方法繪制一個子控件。該方法僅僅是調用子控件的View.draw(ViewGroup,Canvas,long)
  • 通過Canvas.restoreToCount()撤銷之前所做的裁剪設置
      有關裁剪的使用,可參照TabWidget的實現來加深理解。

3. 變化坐標系:View.draw(ViewGroup,Canvas,long)
  該方法的工作流程可參見以下幾步:

  • 進行動畫的計算,并將結果存儲在transformToApply中,這是進行坐標系變換的第一個因素
  • 計算控件內容的滾動量。向Scroller設置一個目標的滾動量,以及滾動動畫的持續時間,scroller會自動計算在動畫國成中本次繪制所需的滾動量。這是進行坐標系變換的第二個因素
  • 使用Canvas.save()保存Canvas的當前狀態。此時Canvas的坐標系為父控件的坐標系。在隨后將Canvas變換到此空間的坐標系并完成繪制后,會通過Canvas.restoreTo()Canvas重置到此時的狀態,以便Canvas可以繼續用來繪制父控件的下一個子控件
  • 第一次變換,對應控件位置與滾動量。最先處理的是子控件位置mLeft/mTop,以及滾動量。子控件的位置mLeft/mTop是進行坐標變換的第三個因素
  • 將動畫產生的變換矩陣應用到Canvas中。canvas.concat(transformToApply.getMatrix()),主要是各種Animation,如SacleAnimation
  • 控件自身的變換矩陣應用到Canvas中,canvas.concat(getMatrix())。如setScaleX/Y(),setTranslationXY()等產生的變換??丶陨淼淖儞Q矩陣是進行坐標系變換的第四個因素
  • 設置剪裁。當父控件的mGroupFlags包含FLAG_CLIP_CHILDREN時,子控件在繪制之前必須通過canvas.clipRect()設置裁剪區域。注意要和dispatchDraw()中的裁剪工作區分:dispatchDraw()中的裁剪是為了保證所有的子控件繪制的內容不得越過ViewGroup的邊界。其設置由setClipToPadding()方法完成。而FLAG_CLIP_CHILDREN則表示所有子控件的繪制內容不得超出子控件自身的邊界,由setClipChildren()方法啟用或禁用
  • 使用變換過的Canvas進行最終繪制,調用dispatchDraw()draw(Canvas)兩個方法
  • 恢復Canvas的狀態到一切開始之前,canvas.restoreToCount(restoreTo)。這樣Canvas又回到了父控件的坐標系,使得父控件的dispatchDraw()便可以將這個Canvas交給下一個子控件的draw(ViewGroup, Canvas, long)方法

4. 以軟件方式繪制控件樹的完成流程

控件樹繪制的完整流程

軟件繪制的流程特點

6.4.5 硬件加速繪制的原理

1. 硬件加速繪制簡介
  倘若窗口使用硬件加速,則ViewRootImpl會創建一個HardwareRenderer并保存在mAttachInfo中。HardwareRenderer是用于硬件加速的渲染器,它封裝了硬件加速的圖形庫,并以Android與硬件加速圖形庫的中間層的身份存在。它負責從AndroidSurface生成一個HardwareLayer,供硬件加速圖形庫作為繪制的輸出目標,并提供一系列工廠方法用于創建硬件加速繪制過程中所需的DisplayListHardwareLayerHardwareCanvas等工具。

2. 硬件加速繪制的入口HardwareRenderer.draw()
drawSoftware()的4個主要工作作為對比來分析該實現:

  • 獲取Canvas。不同于軟件繪制時用Surface.lockCanvas()新建一個Canvas,HardwareRendererHardwareRenderer創建之初便已被創建并綁定在由Surface創建的EGLSurface
  • Canvas進行變換以實現滾動效果。由于硬件繪制的過程位于HardwareRenderer內部,因此ViewRootImpl需要在onHardwarePreDraw()回調中完成這個操作
  • 繪制控件內容。這是硬件繪制和軟件繪制的根本區別。軟件繪制時通過View.draw()以遞歸的方式將整個控件樹用給定的Canvas直接繪制Surface上。而硬件加速繪制則先通過View.getDisplayList()獲取根控件的DisplayList中包含了已編譯過的用于繪制整個控件樹的繪圖指令。如果說軟件繪制是直接繪制,那么硬件繪制則是通過DisplayList間接繪制
  • 將繪制結果顯示出來。硬件加速繪制通過sEgl.swapBuffers()將繪制內容顯示出來。本質和Surface.unlockCanvasAndPost()方法一致,都是通過 ANativeWindow::queueBuffer將繪制內容發布給SurfaceFlinger

3. DisplayList的創建與渲染
  總體來看,硬件加速繪制過程中的View.getDisplayList()HardwareCanvas.drawDisplayList()的組合相當于軟件繪制過程中的View.draw().
  ·View.getDisplayList()·的實現體現了DisplayList的使用方法。DisplayList渲染與Surface的繪制十分相似,分為如下三個步驟:

  • 通過DisplayList.start()創建一個HardwareCanvas并準備好開始錄制繪圖指令
  • 使用HardwareCanvas進行與Canvas一樣的變換與繪制操作
  • 通過DisplayList.end()完成錄制并回收HardwareCanvas
      可見DisplayList的渲染也是使用我們熟悉的View.draw()方法完成的,而且View.draw()方法的實現在硬件加速和軟件繪制下完全一樣。但仍體現了另一個重要區別:軟件繪制的整個過程都是用了來自Surface.lockCanvas()的同一個Canvas;而硬件加速時,控件使用由自己的DisplayList所產生的Canvas進行繪制,此時每個控件onDraw()Canvas參數各不相同。另外,getDisplayList()中進行了滾動量的變換,因此在硬件加速繪制的情況下,View.draw(ViewGroup, Canvas,long)方法不需要進行滾動量變換

4. 硬件加速繪制下的子控件繪制
  軟件繪制時的流程:View.draw(Canvas)(自身)->dispatchDraw()->View.draw(ViewGroup, Canvas, long)-> View.draw(Canvas)(子控件)
  硬件加速繪制與軟件繪制在前兩步完全相同,區別在于第三步,即在View.draw(ViewGroup, Canvas, long),兩者幾乎完全不同。其根本原因在于硬件加速繪制希望在Canvas上繪制子控件的DisplayList,而不是使用View.onDraw()直接繪制,總結兩者不同之處如下:

  • 變化因素的應用方法不同。軟件繪制時通過Canvas的變換操作將坐標系變換到子控件自身坐標系,而硬件加速繪制時Canvas的坐標系仍保持在父控件的坐標系下,然后通過DisplayList的相關方法將變換因素設置給DisplayListHardwareCanvas.drawDisplayList()會按照這些變換因素再以這些變換繪制(準確地說是回放)DisplayList
  • 繪制方法不同。軟件繪制時可以說是直接繪制。硬件加速繪制時使用的是View.getDisplayList()HardwareCanvas.drawDisplayList()的組合進行間接繪制

5. 硬件加速繪制總結

軟件繪制與硬件加速繪制的遞歸方式的差異

硬件加速繪制的流程特點

6.4.6 使用繪圖緩存

繪圖緩存是指一個Bitmap或一個HardwareLayer,它保存了控件及其子控件的一個快照。繪圖緩存有兩種類型,即軟件緩存(Bitmap)和硬件緩存(HardwareLayer),開發者可以通過View.setLayerType()LAYER_TYPE_SOFTWARELAYER_TYPE_HARDWARE決定此控件使用哪種類型的緩存。默認情況下,控件的緩存類型為LAYER_TYPE_NONE。但值得注意的是,由于硬件緩存依賴于HardwareCanvas,所以在軟件繪制的情況下,緩存類型被設置為LAYER_TYPE_HARDWARE的控件仍然會選擇使用軟件緩存。而在硬件加速繪制的情況下,可以在硬件緩存和軟件緩存中任選其一。另外,View.setLayerType()可以通過傳入Paint類型的參數用于實現一些顯示效果,如透明度、Xfermode以及ColorFilter

1.軟件繪制下的軟件緩存
  使用軟件緩存進行繪制時使用View.buildDrawingCache()/getDrawingCache()canvas.drawBitmap()的組合替代無緩存模式下的View.draw(Canvas)。這種模式和硬件加速繪制時的處理如出一轍,View.buildDrawingCache()的實現方式與View.getDisplayList()方法幾乎完全一致,只不過它的目標是一個Bitmap而不是DisplayList。而View.getDrawingCache()則返回mDrawingCachemUnscaleDrawingCache,前者會根據兼容模式進行放大或縮小,用于做繪制時的軟件緩存,因為繪制到窗口時需要根據兼容模式進行縮放。而后者反映了控件的真實尺寸,往往被用作控件截圖等用途

2. 硬件加速繪制下的繪圖緩存
  繪圖緩存的實現位于View.getDisplayList(),如果將DisplayList理解為一種緩存,那么硬件加速繪制下的繪圖緩存則是在DisplayList上建立的另一級緩存,即二級繪圖緩存。
  硬件加速時,使用軟件緩存的方式與軟件繪制的流程一樣,只是繪制到DisplayList上。而使用硬件緩存時,HardwareLayer就是我們所說的硬件緩存,其處理在View.getHardwareLayer()

硬件加速繪制啟用繪圖緩存后的特點

3. 繪圖緩存的利弊
  使用繪圖緩存的原則:

  • 不要為十分輕量級的控件啟用繪圖緩存。因為緩存繪制的開銷可能大于控件重繪開銷
  • 為很少發生內容改變的控件啟用繪圖緩存。因為啟用繪圖緩存的控件在invalidate()時會產生額外的緩存繪制繪制操作
  • 當父控件要頻繁改變子控件的位置或變換時對其子控件啟用繪圖緩存。這會避免頻繁地重繪子控件

6.4.7 控件動畫

控件系統存在三種方式實現控件的動畫。
1.ValueAnimator,ObjectAnimator,ViewPropertyAimator。當動畫運行時,ValueAnimator會將AnimationHandler不斷地拋給Choreographer,并在VSYNC事件到來時修改指定的控件屬性,控件屬性的變化引發invalidate()操作進而進行重繪。
2.LayoutTransition,用于ViewGroup中。
3.View.startAnimation(),與控件繪制內部過程聯系緊密,因此針對此方法展開分析動畫的實現原理

1. 啟動動畫
  從startAnimation()方法啟動的動畫依托于Animation類的子類。啟動動畫時首先將給定的Animaiton通過setAnimaiton()保存到mCurrentAnimaiton成員中,再通過invalidate()方法觸發一次重繪

2. 計算動畫變換
  既然動畫是以坐標系變換的方式產生效果的,因此動畫計算的代碼位于View.draw(ViewGroup,Canvas,long)中:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
      
       .........

        Transformation transformToApply = null;
        //獲取startAnimation()所給予的Animation對象
        final Animation a = getAnimation();
        if (a != null) {
            //通過drawAnimation()方法計算當前時間點的變換(Transformation)。結果保存在parent.mChildTransformation中
            more = drawAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
        //在介紹繪制原理時提到,把transformToApply應用到坐標系變換中
        .............
        }
}

drawAnimation()中,通過Animation.getTransformation()計算當前時間點的變換,并將其保存到父控件的mChildTransformation成員中,然后在View.draw(ViewGroup, Canvas, long)方法中將這個變換以坐標系變換的方式應用到Canvas或者DisplayList中,從而對最終的繪制結果產生影響。倘若動畫還將繼續,則調用invalidate()以便在下次VSYNC時間到來時進行下一幀的計算與繪制

3. 動畫的結束
  動畫的結束借由parent.finishAnimatingView()實現,也就是由父控件完成。交給父控件來完成,是因為在執行動畫將這個控件從父控件中移除時,ViewGroup會將其從mChildren中移除,但會同時將其放置到mDisappearingChildren數組中,并等待動畫結束。由于mDisappearingChildren中的控件依然會得到繪制,因此在執行了ViewGroup.removeView()之后,用戶仍然可以看到動畫中的控件,直到動畫結束后才會消失。另外,LayoutTransition也依賴于這一機制,使得其移出動畫被用戶看到

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

推薦閱讀更多精彩內容