Ⅰ 「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圖形渲染原理上
對於Android開發者來說,我們或多或少有了解過Android圖像顯示的知識點,剛剛學習Android開發的人會知道,在Actvity的onCreate方法中設置我們的View後,再經過onMeasure,onLayout,onDraw的流程,界面就顯示出來了;對Android比較熟悉的開發者會知道,onDraw流程分為軟體繪制和硬體繪制兩種模式,軟繪是通過調用Skia來操作,硬繪是通過調用Opengl ES來操作;對Android非常熟悉的開發者會知道繪制出來的圖形數據最終都通過GraphiBuffer內共享內存傳遞給SurfaceFlinger去做圖層混合,圖層混合完成後將圖形數據送到幀緩沖區,於是,圖形就在我們的屏幕顯示出來了。
但我們所知道的Activity或者是應用App界面的顯示,只屬於Android圖形顯示的一部分。同樣可以在Android系統上展示圖像的WebView,Flutter,或者是通過Unity開發的3D游戲,他們的界面又是如何被繪制和顯現出來的呢?他們和我們所熟悉的Acitvity的界面顯示又有什麼異同點呢?我們可以不藉助Activity的setView或者InflateView機制來實現在屏幕上顯示出我們想要的界面嗎?Android系統顯示界面的方式又和IOS,或者Windows等系統有什麼區別呢?……
去探究這些問題,比僅僅知道Acitvity的界面是如何顯示出來更加的有價值,因為想要回答這些問題,就需要我們真正的掌握Android圖像顯示的底層原理,當我們掌握了底層的顯示原理後,我們會發現WebView,Flutter或者未來會出現的各種新的圖形顯示技術,原來都是大同小異。
我會花三篇文章的篇幅,去深入的講解Android圖形顯示的原理,OpenGL ES和Skia的繪制圖像的方式,他們如何使用,以及他們在Android中的使用場景,如開機動畫,Activity界面的軟體繪制和硬體繪制,以及Flutter的界面繪制。那麼,我們開始對Android圖像顯示原理的探索吧。
在講解Android圖像的顯示之前,我會先講一下屏幕圖像的顯示原理,畢竟我們圖像,最終都是在手機屏幕上顯示出來的,了解這一塊的知識會讓我們更容易的理解Android在圖像顯示上的機制。
圖像顯示的完整過程,分為下面幾個階段:
圖像數據→CPU→顯卡驅動→顯卡(GPU)→顯存(幀緩沖)→顯示器
我詳細介紹一下這幾個階段:
實際上顯卡驅動,顯卡和顯存,包括數模轉換模塊都是屬於顯卡的模塊。但為了能能詳細的講解經歷的步驟,這里做了拆分。
當顯存中有數據後,顯示器又是怎麼根據顯存裡面的數據來進行界面的顯示的呢?這里以LCD液晶屏為例,顯卡會將顯存里的數據,按照從左至右,從上到下的順序同步到屏幕上的每一個像素晶體管,一個像素晶體管就代表了一個像素。
如果我們的屏幕解析度是1080x1920像素,就表示有1080x1920個像素像素晶體管,每個橡素點的顏色越豐富,描述這個像素的數據就越大,比如單色,每個像素只需要1bit,16色時,只需要4bit,256色時,就需要一個位元組。那麼1080x1920的解析度的屏幕下,如果要以256色顯示,顯卡至少需要1080x1920個位元組,也就是2M的大小。
剛剛說了,屏幕上的像素數據是從左到右,從上到下進行同步的,當這個過程完成了,就表示一幀繪制完成了,於是會開始下一幀的繪制,大部分的顯示屏都是以60HZ的頻率在屏幕上繪制完一幀,也就是16ms,並且每次繪制新的一幀時,都會發出一個垂直同步信號(VSync)。我們已經知道,圖像數據都是放在幀緩沖中的,如果幀緩沖的緩沖區只有一個,那麼屏幕在繪制這一幀的時候,圖像數據便沒法放入幀緩沖中了,只能等待這一幀繪制完成,在這種情況下,會有很大了效率問題。所以為了解決這一問題,幀緩沖引入兩個緩沖區,即 雙緩沖機制 。雙緩沖雖然能解決效率問題,但會引入一個新的問題。當屏幕這一幀還沒繪制完成時,即屏幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩沖區並把兩個緩沖區進行交換後,顯卡的像素同步模塊就會把新的一幀數據的下半段顯示到屏幕上,造成畫面撕裂現象。
為了解決撕裂問題,就需要在收到垂直同步的時候才將幀緩沖中的兩個緩沖區進行交換。Android4.1黃油計劃中有一個優化點,就是CPU和GPU都只有收到垂直同步的信號時,才會開始進行圖像的繪制操作,以及緩沖區的交換工作。
我們已經了解了屏幕圖像顯示的原理了,那麼接著開始對Android圖像顯示的學習。
從上一章已經知道,計算機渲染界面必須要有GPU和幀緩沖。對於Linux系統來說,用戶進程是沒法直接操作幀緩沖的,但我們想要顯示圖像就必須要操作幀緩沖,所以Linux系統設計了一個虛擬設備文件,來作為對幀緩沖的映射,通過對該文件的I/O讀寫,我們就可以實現讀寫屏操作。幀緩沖對應的設備文件於/dev/fb* ,*表示對多個顯示設備的支持, 設備號從0到31,如/dev/fb0就表示第一塊顯示屏,/dev/fb1就表示第二塊顯示屏。對於Android系統來說,默認使用/dev/fb0這一個設幀緩沖作為主屏幕,也就是我們的手機屏幕。我們Android手機屏幕上顯示的圖像數據,都是存儲在/dev/fb0里,早期AndroidStuio中的DDMS工具實現截屏的原理就是直接讀取/dev/fb0設備文件。
我們知道了手機屏幕上的圖形數據都存儲在幀緩沖中,所以Android手機圖像界面的原理就是將我們的圖像數據寫入到幀緩沖內。那麼,寫入到幀緩沖的圖像數據是怎麼生成的,又是怎樣加工的呢?圖形數據是怎樣送到幀緩沖去的,中間經歷了哪些步驟和過程呢?了解了這幾個問題,我們就了解了Android圖形渲染的原理,那麼帶著這幾個疑問,接著往下看。
想要知道圖像數據是怎麼產生的,我們需要知道 圖像生產者 有哪些,他們分別是如何生成圖像的,想要知道圖像數據是怎麼被消費的,我們需要知道 圖像消費者 有哪些,他們又分別是如何消費圖像的,想要知道中間經歷的步驟和過程,我們需要知道 圖像緩沖區 有哪些,他們是如何被創建,如何分配存儲空間,又是如何將數據從生產者傳遞到消費者的,圖像顯示是一個很經典的消費者生產者的模型,只有對這個模型各個模塊的擊破,了解他們之間的流動關系,我們才能找到一條更容易的路徑去掌握Android圖形顯示原理。我們看看谷歌提供的官方的架構圖是怎樣描述這一模型的模塊及關系的。
如圖, 圖像的生產者 主要有MediaPlayer,CameraPrevier,NDK,OpenGl ES。MediaPlayer和Camera Previer是通過直接讀取圖像源來生成圖像數據,NDK(Skia),OpenGL ES是通過自身的繪制能力生產的圖像數據; 圖像的消費者 有SurfaceFlinger,OpenGL ES Apps,以及HAL中的Hardware Composer。OpenGl ES既可以是圖像的生產者,也可以是圖像的消費者,所以它也放在了圖像消費模塊中; 圖像緩沖區 主要有Surface以及前面提到幀緩沖。
Android圖像顯示的原理,會僅僅圍繞 圖像的生產者 , 圖像的消費者 , 圖像緩沖區 來展開,在這一篇文章中,我們先看看Android系統中的圖像消費者。
SurfaceFlinger是Android系統中最重要的一個圖像消費者,Activity繪制的界面圖像,都會傳遞到SurfaceFlinger來,SurfaceFlinger的作用主要是接收圖像緩沖區數據,然後交給HWComposer或者OpenGL做合成,合成完成後,SurfaceFlinger會把最終的數據提交給幀緩沖。
那麼SurfaceFlinger是如何接收圖像緩沖區的數據的呢?我們需要先了解一下Layer(層)的概念,一個Layer包含了一個Surface,一個Surface對應了一塊圖形緩沖區,而一個界面是由多個Surface組成的,所以他們會一一對應到SurfaceFlinger的Layer中。SurfaceFlinger通過讀取Layer中的緩沖數據,就相當於讀取界面上Surface的圖像數據。Layer本質上是 Surface和SurfaceControl的組合 ,Surface是圖形生產者和圖像消費之間傳遞數據的緩沖區,SurfaceControl是Surface的控制類。
前面在屏幕圖像顯示原理中講到,為了防止圖像的撕裂,Android系統會在收到VSync垂直同步時才會開始處理圖像的繪制和合成工作,而Surfaceflinger作為一個圖像的消費者,同樣也是遵守這一規則,所以我們通過源碼來看看SurfaceFlinger是如何在這一規則下,消費圖像數據的。
SurfaceFlinger專門創建了一個EventThread線程用來接收VSync。EventThread通過Socket將VSync信號同步到EventQueue中,而EventQueue又通過回調的方式,將VSync信號同步到SurfaceFlinger內。我們看一下源碼實現。
上面主要是SurfaceFlinger初始化接收VSYNC垂直同步信號的操作,主要有這幾個過程:
經過上面幾個步驟,我們接收VSync的初始化工作都准備好了,EventThread也開始運轉了,接著看一下EventThread的運轉函數threadLoop做的事情。
threadLoop主要是兩件事情
mConditon又是怎麼接收VSync的呢?我們來看一下
可以看到,mCondition的VSync信號實際是DispSyncSource通過onVSyncEvent回調傳入的,但是DispSyncSource的VSync又是怎麼接收的呢?在上面講到的SurfaceFlinger的init函數,在創建EventThread的實現中,我們可以發現答案—— mPrimaryDispSync 。
DispSyncSource的構造方法傳入了mPrimaryDispSync,mPrimaryDispSync實際是一個DispSyncThread線程,我們看看這個線程的threadLoop方法
DispSyncThread的threadLoop會通過mPeriod來判斷是否進行阻塞或者進行VSync回調,那麼mPeriod又是哪兒被設置的呢?這里又回到SurfaceFlinger了,我們可以發現在SurfaceFlinger的 resyncToHardwareVsync 函數中有對mPeriod的賦值。
可以看到,這里最終通過HWComposer,也就是硬體層拿到了period。終於追蹤到了VSync的最終來源了, 它從HWCompser產生,回調至DispSync線程,然後DispSync線程回調到DispSyncSource,DispSyncSource又回調到EventThread,EventThread再通過Socket分發到MessageQueue中 。
我們已經知道了VSync信號來自於HWCompser,但SurfaceFlinger並不會一直監聽VSync信號,監聽VSync的線程大部分時間都是休眠狀態,只有需要做合成工作時,才會監聽VSync,這樣即保證圖像合成的操作能和VSync保持一致,也節省了性能。SurfaceFlinger提供了一些主動注冊監聽VSync的操作函數。
可以看到,只有當SurfaceFlinger調用 signalTransaction 或者 signalLayerUpdate 函數時,才會注冊監聽VSync信號。那麼signalTransaction或者signalLayerUpdate什麼時候被調用呢?它可以由圖像的生產者通知調用,也可以由SurfaceFlinger根據自己的邏輯來判斷是否調用。
現在假設App層已經生成了我們界面的圖像數據,並調用了 signalTransaction 通知SurfaceFlinger注冊監聽VSync,於是VSync信號便會傳遞到了MessageQueue中了,我們接著看看MessageQueue又是怎麼處理VSync的吧。
MessageQueue收到VSync信號後,最終回調到了SurfaceFlinger的 onMessageReceived 中,當SurfaceFlinger接收到VSync後,便開始以一個圖像消費者的角色來處理圖像數據了。我們接著看SurfaceFlinger是以什麼樣的方式消費圖像數據的。
VSync信號最終被SurfaceFlinger的onMessageReceived函數中的INVALIDATE模塊處理。
INVALIDATE的流程如下:
handleMessageTransaction的處理比較長,處理的事情也比較多,它主要做的事情有這些
handleMessageRefresh函數,便是SurfaceFlinger真正處理圖層合成的地方,它主要下面五個步驟。
我會詳細介紹每一個步驟的具體操作
合成前預處理會判斷Layer是否發生變化,當Layer中有新的待處理的Buffer幀(mQueuedFrames>0),或者mSidebandStreamChanged發生了變化, 都表示Layer發生了變化,如果變化了,就調用signalLayerUpdate,注冊下一次的VSync信號。如果Layer沒有發生變化,便只會做這一次的合成工作,不會注冊下一次VSync了。
重建Layer棧會遍歷Layer,計算和存儲每個Layer的臟區, 然後和當前的顯示設備進行比較,看Layer的臟區域是否在顯示設備的顯示區域內,如果在顯示區域內的話說明該layer是需要繪制的,則更新到顯示設備的VisibleLayersSortedByZ列表中,等待被合成
rebuildLayerStacks中最重要的一步是 computeVisibleRegions ,也就是對Layer的變化區域和非透明區域的計算,為什麼要對變化區域做計算呢?我們先看看SurfaceFlinger對界面顯示區域的分類:
還是以這張圖做例子,可以看到我們的狀態欄是半透明的,所以它是一個opaqueRegion區域,微信界面和虛擬按鍵是完全不透明的,他是一個visibleRegion,除了這三個Layer外,還有一個我們看不到的Layer——壁紙,它被上方visibleRegion遮擋了,所以是coveredRegion
對這幾個區域的概念清楚了,我們就可以去了解computeVisibleRegions中做的事情了,它主要是這幾步操作: