淺談移動端 View 的顯示過程

隨著科技的發展,各種移動端早已成為人們日常生活中不可或缺的部分,人們使用移動端產品工作、社交、娛樂……移動端界面的流暢性已經成為影響用戶體驗的重要因素之一。那麼你是否思考過移動端所展現的流暢畫面是如何實現的呢?

本文通過對移動端View顯示過程的簡略分析,幫助開發者瞭解View渲染的邏輯,更好地優化自己的APP。

淺談移動端 View 的顯示過程

上圖展示的是一個完整的頁面渲染過程。通過上圖,我們可以初步瞭解每一幀頁面從代碼佈局的編寫到展示給使用者,其背後的邏輯是如何一步一步執行的。

屏幕如何呈像

像素點

在電子屏幕中顯示的圖片,其實都是由一個個“小點”所組成的,這些“小點”被稱為“像素點”。每一個像素點都有自己的顏色,每一張完整的圖片都是由它們相連拼接形成的。

每個像素點一般都有 3 個子像素:紅、綠、藍,根據這三種原色,我們能夠調製出各種各樣的顏色。

淺談移動端 View 的顯示過程

大電視機

與現在的平板電視不同的是,以前的黑白電視機或者大背投彩電,總是帶著大大的“後背”。“大後背”電視其實就是陰極射線管電視機,俗稱顯像管電視。其成像原理是電子槍發射出的電子束(陰極射線)通過聚焦系統和偏轉系統,射向屏幕上塗有熒光層的指定位置。被電子束轟擊的每個位置,熒光層都會產生一個小亮點,最終小亮點們將會組成一幅幅影像,顯示在電視屏幕上。

淺談移動端 View 的顯示過程

淺談移動端 View 的顯示過程

這也是以前大電視機的屏幕都呈圓弧形的原因。因為越接近圓形,邊長到中心的距離越相近,呈像越均勻。那為什麼當磁鐵貼近電視機時,會讓電視機的成像出現問題呢?那是因為磁鐵會干擾電子束的正常軌跡,並且在貼近屏幕的時候,也可能使得屏幕的熒光層磁化,出現一個個不正常的光斑。

下圖展示的是攝像機慢放後,電子束的繪製過程。

淺談移動端 View 的顯示過程

淺談移動端 View 的顯示過程

LCD 和 OLED

隨著科技的不斷進步,電視、手機、電腦的體積越來越薄,射線管顯像方式也逐漸被淘汰。目前在手機市場上佔據主流地位的是 LCD 和 OLED 兩種屏幕。

淺談移動端 View 的顯示過程

LCD 全稱為 Liquid Crystal Display ,即液晶顯示器。OLED 全稱為 Organic Light-Emitting Diode ,即有機發光二極管。這兩者之間存在顯著的差別:

1. 兩者成像原理不同

LCD 是靠白色的背光穿透彩色薄膜顯色的,而 OLED 則是靠每個像素點自行發光。

2. 在耗電量方面

LCD的耗電量較高,即使只顯示一個亮點,LCD 的背光源也需要一直髮光,而且容易出現漏光現象。而OLED的每個像素都能獨立工作,而且 可以自行發光,因此採用OLED的設備可以製作得更薄,甚至可以彎曲。

3.在製作方面

LCD使用的是無機材料, OLED 則需要使用有機材料,因此 OLED的製作費用更高,並且使用壽命不如 LCD 。

圖形顯示核心 GPU

與CPU相對比,GPU的計算單元更多,更擅長大規模併發計算,例如密碼破解、圖像處理等。CPU 則是遵循馮諾依曼架構存儲程序順序執行,在大規模並行計算能力上,受到的限制更大,因此更擅長邏輯控制。

淺談移動端 View 的顯示過程

應用程序編程接口 API (OpenGL)

在沒有統一的 API 之前,開發者需要在各式各樣的圖形硬件上編寫各種自定義接口和驅動程序,工作量極大。

1990 年 SGI(硅谷圖形公司)成為了工作站 3D 圖形領域的領導者,並將其 API 轉變為一項開放標準,即 OpenGL。後來,SGI還促成了 OpenGL 架構審查委員會(OpenGL ARB)的創建。

淺談移動端 View 的顯示過程

垂直同步 Vertical Synchronization

當我們在使用手機 APP 的過程中,發現頁面出現卡頓現象,那麼極有可能是頁面沒有在 16ms 內更新導致的。實際上,人眼與大腦之間的協作無法感知超過 60fps 的畫面更新。60fps 相當於是每秒 60 幀,那麼每個頁面需要在 1000/60 = 16ms 內更新為其他頁面,才不會讓我們感受到頁面的卡頓。

而在沒有 VSync 的情況下可能會出現以下情況:

淺談移動端 View 的顯示過程

如上圖所示,在沒有 VSync 的情況下,會出現需要顯示第二幀時,其尚未處理完成的情況,因此Display 中顯示的仍是第一幀。這會造成該幀顯示時長超過16ms,從而導致頁面卡頓的現象。

為了使 CPU、GPU 生成幀的速度與 Display 保持一致,Android 系統每 16ms 就會發出一次 VSYNC 信號,觸發 UI 渲染更新。

淺談移動端 View 的顯示過程

從上圖中我們可以看出,每隔 16ms ,安卓會發出一個 VSync 信號,收到信號後 CPU 開始處理下一幀的的內容,GPU 在 CPU 處理結束之後,將會進行光柵化,此時屏幕上顯示的是上一幀已經處理完成的頁面。如此反覆,就可以在頁面中展示一幅幅的指定畫面。而確保畫面流暢的前提是CPU 和 GPU 處理一幀所花費的時間不能超過 16 ms,否則就會出現以下情況:

淺談移動端 View 的顯示過程

當CPU 和 GPU 處理一幀的時間超過了16 ms時,在第一個 Display 中,由於 GPU 處理 B 畫面的時間過長,導致系統發出 VSync 信號時, Display不能及時地顯示出 B 畫面,而重複顯示A頁面,造成卡頓。

此外,在第二個 Display 中,由於 A Buffer 還在被 Display 所使用,不能在收到 VSync 信號後開始處理下一幀的頁面,導致該時間段內 CPU 的閒置。為了避免這種時間的浪費,三緩存機制由此出現:

淺談移動端 View 的顯示過程

如上圖所示,在三緩存機制中,當 A 緩存被 Display 使用、B 緩存被 GPU 處理時,系統會發出 Vsync 信號,並加入新的緩存 C ,用來緩存下一幀的內容。這種方式雖然不能完全避免 A頁面的重複顯示,但是能夠讓後面頁面的顯示更加平滑。

View 的繪製流程

View 的繪製是從 ViewRootImpl 的 performTraversals() 方法開始的,其整體流程大致分為三步,如下圖所示:

淺談移動端 View 的顯示過程

measure

控件測量過程從 performMeasure() 方法開始。在該方法中childWidthMeasureSpec 和 childHeightMeasureSpec,分別是用來確定寬度和高度的。

MeasureSpec 是一個 int 值,它存儲著兩個信息:低 30 位是 View 的 specSize,高 2 位是 View 的 specMode。

specMode 有三種類型:

1.UNSPECIFIED

父視圖對子視圖沒有任何限制,可以將視圖按照開發者的意願設置成任意的大小,在一般開發過程中不會用到。

2.EXACTLY

父視圖為子視圖指定一個確切的尺寸,該尺寸由 specSize 的值來決定。

3.AT_MOST

父視圖為子視圖指定一個最大的尺寸,該尺寸的最大值是 specSize。

觀察 View 的 measure() 方法,可以發現該方法是被 final 修飾的,因此 View 的子類只能夠通過重載 onMeasure() 方法來完成自己的測量邏輯。

淺談移動端 View 的顯示過程

在 onMeasure() 方法中:

淺談移動端 View 的顯示過程

調用 getDefaultSize() 方法來獲取視圖的大小:

淺談移動端 View 的顯示過程

該方法中的第二個參數 measureSpec 是從 measure() 方法中傳遞過來的:通過 getMode() 和 getSize() 解析獲取其中對應的值,再根據 specMode 給最終的 size 賦值。

不過以上只是一個簡單控件的一次 measure 過程,在真正測量的過程中,由於一個頁面往往包含多個子 View ,所以需要循環遍歷測量,在 ViewGroup 中有一個 measureChildren() 方法,就是用來測量子視圖的:

淺談移動端 View 的顯示過程

measure 整體流程的方法調用鏈如下:

淺談移動端 View 的顯示過程

layout

在performTraversals() 方法的測量過程結束後,進入 layout 佈局過程:

performLayout(lp,desiredWindowWidth,desiredWindowHeight);

該過程的主要作用即根據子視圖的大小以及佈局參數,將相應的 View 放到合適的位置上。

host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());

如上,layout() 方法接收了四個參數,按照順時針,分別是左上右下。該座標針對的是父視圖,以左上為起始點,傳入了之前測量出的寬度和高度。之後,讓我們進入到 layout() 方法中觀察:

淺談移動端 View 的顯示過程

我們通過 setFrame() 方法給四個變量賦值,判斷 View 的位置是否變化以及是否需要重新進行 layout,而且其中還調用了 onLayout() 方法。

在進入該方法後,我們可以發現裡面是空的,這是因為子視圖的具體位置是相對於父視圖而言的,所以 View 的 onLayout 為空實現。

淺談移動端 View 的顯示過程

再進入 ViewGroup 類中查看,我們可以發現,這其實是一個抽象的方法,在這樣的情況下, ViewGroup 的子類便需要重寫該方法:

淺談移動端 View 的顯示過程

draw

繪製的流程主要如下圖所示,該流程也是存在遍歷子 View 繪製的過程:

淺談移動端 View 的顯示過程

需要注意的是,View 的 onDraw() 方法是空的,這是因為每個視圖的內容都不相同,這個部分交由子類根據自身的需要來處理,才更加合理:

淺談移動端 View 的顯示過程

安卓渲染機制的整體流程

淺談移動端 View 的顯示過程

1.APP 在 UI 線程構建 OpenGL 渲染需要的命令及數據;

2.CPU 將數據上傳(共享或者拷貝)給 GPU 。(PC 上一般有顯存,但是 ARM 這種嵌入式設備內存一般是 GPU 、 CPU 共享內存);

3.通知 GPU 渲染。一般而言,真機不會阻塞等待 GPU 渲染結束,通知結束後就返回執行其他任務;

4.通知 SurfaceFlinger 圖層合成;

5.SurfaceFlinger 開始合成圖層。

總結

移動端技術發展很快,而畫面顯示優化是一個持續發展的實踐課題,貫穿於每個開發者的日常工作中。未來,個推技術團隊將繼續關注移動端的性能優化,為大家分享相關的技術乾貨。


分享到:


相關文章: