㈠ android view的onDraw從第二次觸發開始,界面變樣了。。。
這個view的大小被限制了,導致draw出來會缺了邊,將view設大一些,或者邊變小一點
㈡ 「Android渲染」圖像是怎樣顯示到屏幕上的
我們每天花很多時間盯著手機屏幕,不知道你有沒有好奇過:
這時候來了一位Android程序員(當然也可以是iOS或者是前端程序員)說: 這里顯示的其實是一個View樹,我們看到的都是大大小小的View。
。。。聽起來很有道理,我們也經常指著屏幕說這個View怎麼怎麼樣,可問題又來了:
程序員老兄又來了: 屏幕當然不能識別View,它作為一個硬體,只能根據收到的數據改變每個像素單元的數據,這樣整體來看,用戶就發現屏幕上的內容變化了。至於View的內容是如何一步一步轉化成屏幕可是識別的數據的,簡單講可以分成三步:
。。。聽起來很有道理,可問題又來了:
那可就說來話長了。。。
對於 measure layout 和 draw ,Android工程師(大都)非常熟悉,我們常常在執行了 onDraw() 方法後,一個讓人自豪的自定義View就顯示出來了。在實際的Android繪制流程中,第一步就是通過 measure layout 和 draw 這些步驟准備了下面的材料:
在Android的繪制中,我們使用Canvas API進行來告訴表示畫的內容,如 drawCircle() drawColor() drawText() drawBitmap() 等,也是這些內容最終呈現在屏幕上。
在當前應用中,View樹中所有元素的材料最終會封裝到 DisplayList 對象中(後期版本有用 RenderNode 對 DisplayList 又做了一層封裝,實現了更好的性能),然後發送出去,這樣第一階段就完成了。
當然就有一個重要的問題:
會將Bitmap復制到下一個階段(准確地講就是復制到GPU的內存中)。
現在大多數設備使用了GPU硬體加速,而GPU在渲染來自Bitmap的數據時只能讀取GPU內存中的數據, 所以需要賦值Bitmap到GPU內存,這個階段對應的名稱叫 Sync&upload 。另外,硬體加速並不支持所有Canvas API,如果自定義View使用了不支持硬體加速的Canvas API(參考 Android硬體加速文檔 ),為了避免出錯就需要對View進行軟體繪制,其處理方式就是生成一個Bitmap,然後復制到GPU進行處理。
這時可能會有問題:如果Bitmap很多或者單個Bitmap尺寸很大,這個過程可能會時間比較久,那有什麼辦法嗎?
當然有(做作。。。)
關於Bitmap這里再多說一句:
Bitmap的內存管理一直是Android程序員很關心的問題,畢竟它是個很占內存的大胖子,在Android3.0~Android7.0,Bitmap內存放在java堆中,而android系統中每個進程的Java堆是有嚴格限制的,處理不好這些Bitmap內存,容易導致頻繁GC,甚至觸發Java堆的 OutOfMemoryError 。從Android8.0開始,bitmap的像素數據放入了native內存,於是Java Heap的內存問題暫時緩解了。
Tip:
現在材料已經備好,我們要真正地畫東西了。
接下來就要把東西畫出來了,畫出來的過程就是把前面的材料轉化成一個堆像素數據的過程,也叫 柵格化 ,那這個活兒誰來干呢?
候選人只有兩個:
大部分情況下,都是GPU來干這個活兒,因為GPU真的特別快!!!
所謂的「畫」,對於計算機來講就是處理圖像,其實就是根據需要(就是DisplayList中的命令)對數據做一些特定類型的數學運算,最後輸出結果的過程。我們看到的每一幀精美界面,(幾乎)都是GPU吭哧吭哧"算"出來的,這個就有疑問了:
我們簡單地聊聊CPU與GPU的區別:
CPU的核心數通常是幾個,單個核心的主頻高,功能強大,擅長串列處理復雜的流程;
GPU ( Graphics Processing Unit ) 有成百上千個核心,單個核心主頻低,功能有限,擅長(利用超多核心)大量並行簡單運算;正如它的名字一樣,GPU就是為圖像繪制這個場景量身定做的硬體(所以使用GPU也叫硬體加速),後來也被用到挖礦和神經網路中。
圖片肯定沒有視頻直觀,我們從感性的角度感受一下GPU到底有多快,我想下面的視頻看過就不會忘掉,你會被GPU折服:
Mythbusters Demo GPU versus CPU
看這個視頻,我們對於「加速」應該有了更深刻的印象,這里不再進一步分析CPU和GPU更微觀的差別(因為不懂),我想已經講明白為什們GPU更快了。
另外,在GPU開始繪制之前,系統也做了一些優化(對DisplayList中的命令進行預處理),讓整個繪制流程更加高效:
第二步的具體過程還是很復雜的,比如涉及到Alpha繪制,相關的優化會失效,詳情查看文章 為什麼alpha渲染性能低 .
至於畫在哪裡,我們現在理解為一個緩沖(Buffer)中就可以了,具體的機制放在第三步講。
到此,我們已經畫(繪制)完了圖像內容,把這個內容發送出去,第二步的任務就完成了。
Tip:
我們知道,除了我們的應用界面,手機屏幕上同時顯示著其他內容,比如SystemUI(狀態欄、導航欄)或者另外的懸浮窗等,這些內容都需要顯示到屏幕上。所以要先 把這些界面的內容合成,然後再顯示到屏幕 。
在講合成圖像之前,我們有必要知道這些界面圖像(Buffer)是怎麼傳遞的:
Android圖形架構中,使用生產者消費者模型來處理圖像數據,其中的圖像緩沖隊列叫 BufferQueue , 隊列中的元素叫 Graphic Buffer ,隊列有生產者也有消費者;每個應用通常會對應一個 Surface ,一個 Surface 對應著一個緩沖隊列,每個隊列中 Graphic Buffer 的數量不超過3個, 上面兩步後繪制的圖像數據最終會放入一個 Graphic Buffer ,應用自身就是隊列的生產者( BufferQueue 在Android圖形處理中有廣泛的應用,當前只討論界面繪制的場景)。
每個 Graphic Buffer 本身體積很大,在從生產者到消費者的傳遞過程中不會進行復制的操作,都是用匿名共享內存的方式,通過句柄來跨進程傳遞。
我們可以通過以下命令來查看手機當前用到的 Graphic Buffer 情況:
關於上面的命令,你可能會好奇這個 SurfaceFlinger 是什麼東西啊?
上文提到過每個應用(一般)對應一個 Surface ,從字面意思看, SurfaceFlinger 就是把應用的 Surface 投射到目的地。
實際上, SurfaceFlinger 就是界面(Buffer)合成的負責人,在應用界面繪制的場景, SurfaceFlinger 充當了 BufferQueue 的消費者。繪制好的 Graphic Buffer 會進入(queue)隊列, SurfaceFlinger 會在合適的時機(這個時機下文討論),從隊列中取出(acquire)Buffer數據進行處理。
我們知道,除了我們的應用界面,手機屏幕上同時顯示著其他內容,比如SystemUI(狀態欄、導航欄)或者另外的懸浮窗等,這些部分的都有各自的Surface,當然也會往對應的 BufferQueue 中生產 Graphic Buffer 。
如下圖所示, SurfaceFlinger 獲取到所有Surface的最新Buffer之後,會配合HWComposer進行處理合成,最終把這些Buffer的數據合成到一個 FrameBuffer 中,而FrameBuffer的數據會在另一個合適的時機(同樣下文討論)迅速地顯示到屏幕上,這時用戶才觀察到屏幕上的變化。
關於上圖中的 HWComposer ,它是Android HAL介面中的一部分,它定義了上層需要的能力,讓由硬體提供商來實現,因為不同的屏幕硬體差別很大,讓硬體提供商驅動自己的屏幕,上層軟體無需關心屏幕硬體的兼容問題。
事實上,如果你觀察足夠仔細的話,可能對上圖還有疑問:
同學你觀察很仔細(...),事實上,這是 SurfaceFlinger 合成過程中重要的細節,對於不同 Surface 的Buffer, 合成的方法有兩種:
顯然第一種方法是最高效的,但為了保證正確性,Android系統結合了兩種方法。具體實現上, SurfaceFlinger 會詢問( prepare ) HWComposer 是否支持直接合成,之後按照結果做對應處理。
有的朋友憋不住了:
Good question! (太做作了。。。)
為了保證最好的渲染性能,上面各個步驟之間並不是串列阻塞運行的關系,所以有一個機制來調度每一步的觸發時機,不過在此之前,我們先講介紹一個更基礎的概念:
屏幕刷新率
刷新率是屏幕的硬體指標,單位是Hz(赫茲),意思是屏幕每秒可以刷新的次數。
回到問題,既然屏幕這個硬體每隔一段時間(如60Hz屏幕是16ms)就刷新一次,最佳的方案就是屏幕刷新時開始新一輪的繪制流程,讓一次繪制的流程盡可能占滿整個刷新周期,這樣掉幀的可能性最小。基於這樣的思考,在Android4.1(JellyBean)引入 VSYNC(Vertical Synchronization - 垂直同步信號)
收到系統發出的VSYNC信號後, 有三件事會同時執行(並行) :
下圖描述了沒有掉幀時的VSYNC執行流程,現在我們可以直接回答問題了: 合適的時機就是VSYNC信號 。
從上圖可以看出,在一次VSYNC信號發出後,屏幕立即顯示2個VSYNC周期(60Hz屏幕上就是32ms)之前開始繪制的圖像,這當然是延遲,不過這個延遲非常穩定, 只要前面的繪制不掉鏈子 ,界面也是如絲般順滑。當然,Android還是推出一種機制讓延遲可以縮小到1個VSYNC周期,詳情可參考 VSYNC-offset 。
實際上,系統只會在需要的時候才發出VSYNC信號,這個開關由SurfaceFlinger來管理。應用也只是在需要的時候才接收VSYNC信號,什麼時候需要呢?也就是應用界面有變化,需要更新了,具體的流程可以參考 View.requestLayout() 或 View.invalidate() 到 Choreographer (編舞者)的調用過程。這個過程會注冊一次VSYNC信號,下一次VSYNC信號發出後應用就能收到了,然後開始新的繪制工作;想要再次接收VSYNC信號就需要重新注冊,可見,應用界面沒有改變的時候是不會進行刷新的。
我們可以看到,無論是VSYNC開關,還是應用對VSYNC信號的單次注冊邏輯,都是秉承著按需分配的原則,這樣的設計能夠帶來Android操作系統更好的性能和更低的功耗。
Tip:
終於。。。說完了
我們簡單回顧一下,
更形象一點就是:
之所以有這一節,是因為隨著Android版本的更替,渲染方案也發生了很多變化。為了簡化表達,我們前文都以當前最新的方案來講解,事實上,部分流程的實現方式在不同版本可能會有較大的變化,甚至在之前版本沒有實現方案,這里我盡可能詳細地列出Android版本更迭過程中與渲染相關的更新(包括監控工具)。
如果你居然能讀到這里,那我猜你對下面的參考文章也會感興趣:
https://source.android.com/devices/graphics
https://hencoder.com/tag/hui-/
https://www.youtube.com/watch?v=wIy8g8yNhNk&feature=emb_logo
https://www.youtube.com/watch?v=v9S5EO7CLjo
https://www.youtube.com/watch?v=zdQRIYOST64&t=177s
https://www.youtube.com/watch?v=we6poP0kw6E&index=64&list=
https://developer.android.com/topic/performance/rendering
https://developer.android.com/guide/topics/graphics/hardware-accel
https://developer.android.com/topic/performance/rendering/profile-gpu#su
https://mp.weixin.qq.com/s/0OOSmrzSkjG3cSOFxWYWuQ
Android Developer Backstage - Android Rendering
Android Developer Backstage - Graphics Performance
https://elinux.org/images/2/2b/Android_graphics_path--chis_simmonds.pdf
㈢ Android 自定義View之Draw過程(上)
Draw 過程系列文章
Android 展示之三部曲:
前邊我們已經分析了:
這倆最主要的任務是: 確定View/ViewGroup可繪制的矩形區域。
接下來將會分析,如何在這給定的區域內繪制想要的圖形。
通過本篇文章,你將了解到:
Android 提供了關於View最基礎的兩個類:
然而ViewGroup 並沒有約定其內部的子View是如何布局的,是疊加在一起呢?還是橫向擺放、縱向擺放等。同樣的View 也沒有約定其展示的內容是啥樣,是矩形、圓形、三角形、一張圖片、一段文字抑或是不規則的形狀?這些都要我們自己去實現嗎?
不盡然,值得高興的是Android已經考慮到上述需求了,為了開發方便已經預制了一些常用的ViewGroup、View。
如:
繼承自ViewGroup的子類
繼承自View的子類
雖然以上衍生的View/ViewGroup子類已經大大為我們提供了便利,但也僅僅是通用場景下的通用控制項,我們想實現一些較為復雜的效果,比如波浪形狀進度條、會發光的球體等,這些系統控制項就無能為力了,也沒必要去預制千奇百怪的控制項。想要達到此效果,我們需要自定義View/ViewGroup。
通常來說自定義View/ViewGroup有以下幾種:
3 一般不怎麼用,除非布局比較特殊。1、2、4 是我們常用的手段,對於我們常說的"自定義View" 一般指的是 4。
接下來我們來看看 4是怎麼實現的。
在xml里引用MyView
效果如下:
黑色部分為其父布局背景。
紅色矩形+黃色圓形即是MyView繪制的內容。
以上是最簡單的自定義View的實現,我們提取重點歸納如下:
由上述Demo可知,我們只需要在重寫的onDraw(xx)方法里繪制想要的圖形即可。
來看看View 默認的onDraw(xx)方法:
發現是個空實現,因此繼承自View的類必須重寫onDraw(xx)方法才能實現繪制。該方法傳入參數為:Canvas類型。
Canvas翻譯過來一般叫做畫布,在重寫的onDraw(xx)里拿到Canvas對象後,有了畫布我們還需要一支筆,這只筆即為Paint,翻譯過來一般稱作畫筆。兩者結合,就可以愉快的作畫(繪制)了。
你可能發現了,在Demo里調用
並沒有傳入Paint啊,是不是Paint不是必須的?實際上調用該方法後,底層會自動生成Paint對象。
可以看到,底層初始化了Paint,並且給其設置的顏色為在Java層設置的顏色。
onDraw(xx)比較簡單,開局一個Canvas,效果全靠畫。
試想,這個Canvas怎麼來的呢,換句話說是誰調用了onDraw(xx)。發揮一下聯想功能,在Measure、Layout 過程有提到過兩者套路很像:
那麼Draw過程是否也是如此套路呢?看見了onDraw(xx),那麼draw(xx)還遠嗎?
沒錯,還真有draw(xx)方法:
可以看出,draw(xx)主要分為兩個部分:
不管是A分支還是B分支,都進行了好幾步的繪制。
通常來說,單一一個View的層次分為:
後面繪制的可能會遮擋前邊繪制的。
對於一個ViewGroup來說,層次分為:
來看看A分支標注的4個點:
(1)
onDraw(canvas)
前面分析過,對於單一的View,onDraw(xx)是空實現,需要由我們自定義繪制。
而對於ViewGroup,也並沒有具體實現,如果在自定義ViewGroup里重寫onDraw(xx),它會執行嗎?默認是不會執行的,相關分析請移步:
Android ViewGroup onDraw為什麼沒調用
(2)
dispatchDraw(canvas),來看看在View.java里的實現:
發現是個空實現,再看看ViewGroup.java里的實現:
也即是說,對於單一View,因為沒有子布局,因此沒必要再分發Draw,而對於ViewGroup來說,需要觸發其子布局發起Draw過程(此過程後續分析),可以類比事件分發過程View、ViewGroup的處理。感興趣的請移步:
Android 輸入事件一擼到底之View接盤俠(3)
(3)
OverLay,顧名思義就是"蓋在某個東西上面",此處是在繪制內容之後,繪制前景之前。怎麼用呢?
以上是給一個ViewGroup設置overLay,效果如下:
你可能發現了,這和設置overLay差不多的嘛,實際還是有差別的。在onDrawForeground(xx)里會重新調整Drawable的尺寸,該尺寸與View大小一致,之前給Drawable設置的尺寸會失效。運行效果如下:
可以看出,ViewGroup都被前景蓋住了。
再來看看B分支的重點:邊緣漸變效果
先來看看TextView 邊緣漸變效果:
加上這倆參數。
實際上系統自帶的一些控制項也使用了該效果,如NumberPicker、YearPickerView
以上是NumberPicker 的效果,可以看出是垂直方向漸變的。
對於View.java 里的onDraw(xx)、draw(xx),ViewGroup.java里並沒有重寫。
而對於dispatchDraw(xx),在View.java里是空實現。在ViewGroup.java里發起對子布局的繪制。
來看看標記的2點:
(1)
設置padding的目的是為了讓子布局留出一定的空隙出來,因此當設置了padding後,子布局的canvas需要根據padding進行裁減。判斷標記為:
FLAG_CLIP_TO_PADDING 默認設置為true
FLAG_PADDING_NOT_NULL 只要有padding不為0,該標記就會打上。
也就是說:只要設置了padding 不為0,子布局顯示區域需要裁減。
能不能不讓子布局裁減顯示區域呢?
答案是可以的。
考慮到一種場景:使用RecyclerView的時候,我們需要設置paddingTop = 20px,效果是:RecyclerView Item展示時離頂部有20px,但是滾動的時候永遠滾不到頂部,看起來不是那麼友好。這就是上述的裁減起作用了,需要將此動作禁止。通過設置:
當然也可以在xml里設置:
(2)
drawChild(xx)
從方法名上看是調用子布局進行繪制。
child.draw(x1,x2,x3)里分兩種情況:
這兩者具體作用與區別會在下篇文章分析,不管是硬體加速繪制還是軟體加速繪制,最終都會調用View.draw(xx)方法,該方法上面已經分析過。
注意,draw(x1,x2,x3)與draw(xx)並不一樣,不要搞混了。
用圖表示:
View/ViewGroup Draw過程的聯系:
一般來說,我們通常會自定義View,並且重寫其onDraw(xx)方法,有沒有繪制內容的ViewGroup需求呢?
是有的,舉個例子,大家可以去看看RecyclerView ItemDecoration 的繪制,其中運用到了ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx)繪制的先後順序來實現分割線,分組頭部懸停等功能的。
本篇文章基於 Android 10.0