當前位置:首頁 » 安卓系統 » android繪制過程

android繪制過程

發布時間: 2024-01-06 20:42:27

A. Android:一篇文章帶你完全梳理自定義View工作流程!

了解自定義View流程前,需了解一定的自定義View基礎,具體請看文章: (1)自定義View基礎 - 最易懂的自定義View原理系列

下面,我將詳細講解 View 繪制的三大流程: measure 過程、 layout 過程、 draw 過程

請看文章: 自定義View Layout過程 - 最易懂的自定義View原理系列(3)

至此,關於自定義 View 的工作流程講解完畢。

結合原理 & 實現步驟,若需實現1個自定義View,請看文章: 手把手教你寫一個完整的自定義View

B. 安卓開發中矢量圖的繪制及動畫

矢量圖也稱為面向對象的圖像或繪圖圖像,是根據幾何特性來繪制的圖形,在安卓開發中可以使用失量圖代替原來的圖片資源,矢量圖具有佔用空間小和可以隨意縮放但不失真的優勢,在我的多個項目中都有運用。

通過學習和實踐,我總結了一些與矢量圖相關的知識,方便今後更好的使用矢量圖,同時也可以供大家查閱參考。

繪制矢量圖之前需要先定義畫布的寬高,後續的繪制效果都展示在這個畫布上。在繪制過程中需要輸入的坐標就是這個畫布上的點。

安卓的矢量圖常見於 drawable 文件夾下,是一個xml文件,由 vector 標簽包裹,在 vector 標簽中可包含多個 path 標簽,依次疊加顯示。

在矢量圖中最重要的就是 path 屬性,圖像的樣式就是由 path 屬性中的數據繪制而成,這些數據由不同的命令組合而成,下面就介紹一些矢量圖的繪制命令。

將前面的命令示例連接起來就可以生成一個完整的圖像,它大概長這個樣子:

畫布的尺寸為500x500,圖上的頂點是200,10的位置,也是我們開始作圖的起點。通過這個圖片可以更好的理解每一個繪圖命令。

安卓中可以為矢量圖添加動畫效果,這樣用戶就可以看到一個動的圖片,可以一定程度的提高app的交互效果。矢量圖動畫是圖形內部的變化,可以做到View動畫無法實現的效果。

這種動畫針對的是矢量圖中 path 欄位的值,通過連續改變 path 欄位的值而達到產生動畫的效果。

註:pathData動畫所需的AnimatedVectorDrawable最低要求API等級為25

實現一個矢量圖動畫需要以下幾步:
1. 准備起始狀態和結束狀態的矢量圖兩張。
2. 創建動畫配置文件。
3. 創建動畫矢量圖文件。
4. 啟動動畫。

基於這種要求,我准備了兩個矢量圖:

控制動畫運行的是一個 objectAnimator ,此處把 objectAnimator 包裹在一個 set 中也是可以的,說白了就是執行這個動畫文件。
ration 用來指定動畫的持續時間。
propertyName 中的pathData指的就是矢量圖中的pathData。
valueFrom 和 valueTo 一個是起始路徑,一個是結束路徑,可以想到,這個動畫就是在持續修改pathData,從而達到展示動畫的效果。而 valueFrom 和 valueTo 的值是直接從先前准備的矢量圖中復制過來的,所以那個結束狀態的矢量圖中唯一有用的東西就是pathData屬性,沒有那個文件也無所謂。
valueType 這里必須填判型寫pathType,這是專門用來計算path的類型。

此時,文件的最外層由 animated-vector 包裹,同時需要添加一個 drawable 參數,這個 drawable 用於指定動畫應用於那個矢量圖上,我們是要從未啟用狀態變成啟用狀態,所以是在未啟用狀態開始執行動畫,在動畫未開始的時候展示的也是未啟用狀態。此處我們指定為 @drawable/icon_filter_off 。
內部有一個 target 標簽,這個標簽可以有多個,分別對應不同的動畫,但同一個 path 只能應用一個動畫。
name 用於指定要執行動畫的 path 。status正是我們為右下角小圖標path設置的名稱。
animation 用於指定需要執行的動畫。此處引用我們剛剛創建的猛沖山動畫資源 @animator/filter_turn_on 。
當我們創建枝中好動畫矢量圖之後,頁面中引用的資源就不再是之前的靜態矢量圖了,需要把 ImageView 的圖片替換成 @drawable/animated_filter_on

經過這么多的步驟,我們終於做出了一個矢量圖動畫,而且是一個。說實話,有點累,然而我這個狀態切換的動畫一套就要兩個,所以我又加了一個回來的動畫和對應的動畫矢量圖,一共六個文件,完成了篩選狀態的兩個切換動畫。這還是比較簡單的實現方式,對於兩種狀態切換的動畫,網上還有一種使用selector的方式,這種方式更麻煩,而且使用方法並沒有簡單一些,所以我的選擇是在需要切換狀態的時候更改 ImageView 的圖片資源,然後再執行動畫。

trimPath動畫相當於是改變了矢量圖繪制的位置,是從頭開始畫還是從80%的位置開始畫,然後再動態的修改這個百分比,從而達到動畫的效果。理解起來倒不是很難。

先放一個我使用trimPath動畫做的loading效果,這個動畫效果被我用在LoadingDialog中,在界面載入的時候會重復播放這個動畫。

android:name="load" 不用多說,這個是我們做動畫時路徑名稱。這里為了讓心電圖路徑更清晰,我設置了描邊寬度為20( android:strokeWidth="20" ),同時還要設置描邊的顏色才能展示出來。後面的 android:trimPathStart="0" 和 android:trimPathEnd="0" 是本次trimPath動畫的重點。

這兩個屬性都設置為0是因為動畫的起始幀都為0,然後通過 objectAnimator 慢慢把這兩個屬性變為1,這樣一個慢慢增長的動畫就形成了。
網路上一個橫線變成搜索按鈕的示例是將這兩個屬性分別應用到了兩個 path 上,而我是將兩個屬性同時應用到一個 path 上,原理都是一樣的。

在配置文件中,我將兩個動畫都設置為3秒且循環播放,起始點的動畫慢於終點的動畫1秒,達到只畫中間1秒間隔線段的效果。和路徑變形動畫的區別是 android:valueType="floatType" ,我們只需要計算從0到1的數字,然後應用到 trimPathStart 和 trimPathEnd 欄位上。至此,loading的動畫就配置完了。

這一步已經沒什麼可說的了,就是將指定的矢量圖中指定的路徑設置一個指定的動畫。

通過幾天的學習,已經大致掌握了矢量圖的展示及動畫的製作,但這一套流程下來成本比較高,是程序員方式的動畫製作流程。除了製作成本,創意成本也是相當高的,一個好的創意能極大的提升用戶體驗,而好多時候我們的創意能夠被實現也是很困難的。希望以後能實現一些更好的效果,讓用戶使用起來更舒服。

SVG—最簡單的SVG動畫
SVG路徑(path)中的圓弧(A)指令的語法說明及計算邏輯
Android中的矢量圖
Android高級動畫(2)

C. Android 自定義控制項 layout

Android 繪制流程

View :View主要執行layout方法,使用 serFrame 方法來設置本身 View 的四個頂點的位置,確定View本身的位置。
ViewGroup :ViewGroup主要執行onLayout方法,遞歸遍歷所有子View,確定子View的位置。

我們來看ViewRootImpl中的 performLayout() 方法

看到這里,那host.getMeasuredWidth() / host.getMeasuredHeight()是什麼?它是直接調用View中的方法,其實就是經過measure後的DecorView的測量寬度和高度。在 Android 自定義控制項 measure 中有說明。

2.3.2.1 我們先來看ViewGroup中的 layout() 方法

ViewGroup裡面的layout最終會調入到父類View中的layout,View的layout後面講解。這里可以先告訴大家,最終會調用View的onLayout方法,而ViewGroup的onLayout是抽象方法,所以它的子類LinearLayout必須要實現。

2.3.2.2 我們再來看LinearLayout中的 onLayout() 方法。

2.3.2.3 挑一個縱向的吧,我們再來看LinearLayout中的 layoutVertical() 方法。

2.3.2.4 我們再來看LinearLayout中的 setChildFrame() 方法。

又一次回到了View的layout方法,接下來就看View分發的layout。

我們先來看View中的 layout() 方法。

我們先來看View中的 onLayout() 方法。

空空如也,其實View的布局由父容器決定,所以空實現是正常的,當然也可以在自定義View中進行更改。

《Android 視圖模塊 全家桶》

Android開發之自定義控制項(二)---onLayout詳解
自定義View Layout過程 - 最易懂的自定義View原理系列(3)

D. Android UI繪制之View繪制的工作原理

這是AndroidUI繪制流程分析的第二篇文章,主要分析界面中View是如何繪制到界面上的具體過程。

ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 建立關聯。

measure 過程決定了 View 的寬/高, Measure 完成以後,可以通過 getMeasuredWidth 和 getMeasuredHeight 方法來獲取 View 測量後的寬/高,在幾乎所有的情況下,它等同於View的最終的寬/高,但是特殊情況除外。 Layout 過程決定了 View 的四個頂點的坐標和實際的寬/高,完成以後,可以通過 getTop、getBottom、getLeft 和 getRight 來拿到View的四個頂點的位置,可以通過 getWidth 和 getHeight 方法拿到View的最終寬/高。 Draw 過程決定了 View 的顯示,只有 draw 方法完成後 View 的內容才能呈現在屏幕上。

DecorView 作為頂級 View ,一般情況下,它內部會包含一個豎直方向的 LinearLayout ,在這個 LinearLayout 裡面有上下兩個部分,上面是標題欄,下面是內容欄。在Activity中,我們通過 setContentView 所設置的布局文件其實就是被加到內容欄中的,而內容欄id為 content 。可以通過下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通過 content.getChildAt(0) 可以得到設置的 view 。 DecorView 其實是一個 FrameLayout , View 層的事件都先經過 DecorView ,然後才傳遞給我們的 View 。

MeasureSpec 代表一個32位的int值,高2位代表 SpecMode ,低30位代表 SpecSize , SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。
SpecMode 有三類,如下所示:

UNSPECIFIED

EXACTLY

AT_MOST

LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬/高。

對於頂級View,即DecorView和普通View來說,MeasureSpec的轉換過程略有不同。對於DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定;

對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;

MeasureSpec一旦確定,onMeasure就可以確定View的測量寬/高。

小結一下

當子 View 的寬高採用 wrap_content 時,不管父容器的模式是精確模式還是最大模式,子 View 的模式總是最大模式+父容器的剩餘空間。

View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即測量、布局、繪制。其中 measure 確定 View 的測量寬/高, layout 確定 view 的最終寬/高和四個頂點的位置,而 draw 則將 View 繪制在屏幕上。

measure 過程要分情況,如果只是一個原始的 view ,則通過 measure 方法就完成了其測量過程,如果是一個 ViewGroup ,除了完成自己的測量過程外,還會遍歷調用所有子元素的 measure 方法,各個子元素再遞歸去執行這個流程。

如果是一個原始的 View,那麼通過 measure 方法就完成了測量過程,在 measure 方法中會去調用 View 的 onMeasure 方法,View 類裡面定義了 onMeasure 方法的默認實現:

先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源碼

可以看到, getMinimumWidth 方法獲取的是 Drawable 的原始寬度。如果存在原始寬度(即滿足 intrinsicWidth > 0),那麼直接返回原始寬度即可;如果不存在原始寬度(即不滿足 intrinsicWidth > 0),那麼就返回 0。

接著看最重要的 getDefaultSize 方法:

如果 specMode 為 MeasureSpec.UNSPECIFIED 即未指定模式,那麼返回由方法參數傳遞過來的尺寸作為 View 的測量寬度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精確模式,那麼返回從 measureSpec 中取出的 specSize 作為 View 測量後的寬度和高度。

看一下剛才的表格:

當 specMode 為 EXACTLY 或者 AT_MOST 時,View 的布局參數為 wrap_content 或者 match_parent 時,給 View 的 specSize 都是 parentSize 。這會比建議的最小寬高要大。這是不符合我們的預期的。因為我們給 View 設置 wrap_content 是希望View的大小剛好可以包裹它的內容。

因此:

如果是一個 ViewGroup,除了完成自己的 measure 過程以外,還會遍歷去調用所有子元素的 measure 方法,各個子元素再遞歸去執行 measure 過程。

ViewGroup 並沒有重寫 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 這幾個方法專門用於測量子元素。

如果是 View 的話,那麼在它的 layout 方法中就確定了自身的位置(具體來說是通過 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft , mRight , mTop , mBottom 這四個值), layout 過程就結束了。

如果是 ViewGroup 的話,那麼在它的 layout 方法中只是確定了 ViewGroup 自身的位置,要確定子元素的位置,就需要重寫 onLayout 方法;在 onLayout 方法中,會調用子元素的 layout 方法,子元素在它的 layout 方法中確定自己的位置,這樣一層一層地傳遞下去完成整個 View 樹的 layout 過程。

layout 方法的作用是確定 View 本身的位置,即設定 View 的四個頂點的位置,這樣就確定了 View 在父容器中的位置;
onLayout 方法的作用是父容器確定子元素的位置,這個方法在 View 中是空實現,因為 View 沒有子元素了,在 ViewGroup 中則進行抽象化,它的子類必須實現這個方法。

1.繪制背景( background.draw(canvas); );
2.繪制自己( onDraw );
3.繪制 children( dispatchDraw(canvas) );
4.繪制裝飾( onDrawScrollBars )。

dispatchDraw 方法的調用是在 onDraw 方法之後,也就是說,總是先繪制自己再繪制子 View 。

對於 View 類來說, dispatchDraw 方法是空實現的,對於 ViewGroup 類來說, dispatchDraw 方法是有具體實現的。

通過 dispatchDraw 來傳遞的。 dispatchDraw 會遍歷調用子元素的 draw 方法,如此 draw 事件就一層一層傳遞了下去。dispatchDraw 在 View 類中是空實現的,在 ViewGroup 類中是真正實現的。

如果一個 View 不需要繪制任何內容,那麼就設置這個標記為 true,系統會進行進一步的優化。

當創建的自定義控制項繼承於 ViewGroup 並且不具備繪制功能時,就可以開啟這個標記,便於系統進行後續的優化;當明確知道一個 ViewGroup 需要通過 onDraw 繪制內容時,需要關閉這個標記。

參考:《Android開發藝術探索》

E. Android 自定義View之Layout過程

系列文章:

在上篇文章: Android 自定義View之Measure過程 ,我們分析了Measure過程,本次將會掀開承上啟下的Layout過程神秘面紗,
通過本篇文章,你將了解到:

在上篇文章的比喻里,我們說過:

該ViewGroup 重寫了onMeasure(xx)和onLayout(xx)方法:

同時,當layout 執行結束,清除PFLAG_FORCE_LAYOUT標記,該標記會影響Measure過程是否需要執行onMeasure。

該View 重寫了onMeasure(xx)和onLayout(xx)方法:

MyViewGroup里添加了MyView、Button兩個控制項,最終運行的效果如下:

可以看出,MyViewGroup 里子布局的是橫向擺放的。我們重點關注Layout過程。實際上,MyViewGroup里我們只重寫了onLayout(xx)方法,MyView也是重寫了onLayout(xx)方法。
接下來,分析View Layout過程。

與Measure過程類似,連接ViewGroup onLayout(xx)和View onLayout(xx)之間的橋梁是View layout(xx)。

可以看出,最終都調用了setFrame(xx)方法。

對於Measure過程在onMeasure(xx)里記錄了尺寸的值,而對於Layout過程則在layout(xx)里記錄了坐標值,具體來說是在setFrame(xx)里,該方法兩個重點地方:

View.onLayout(xx)是空實現
從layout(xx)和onLayout(xx)聲明可知,這兩個方法都是可以被重寫的,接下來看看ViewGroup是否重寫了它們。

ViewGroup.layout(xx)雖然重寫了layout(xx),但是僅僅做了簡單判斷,最後還是調用了View.layout(xx)。

這重寫後將onLayout變為抽象方法,也就是說繼承自ViewGroup的類必須重寫onLayout(xx)方法。
我們以FrameLayout為例,分析其onLayout(xx)做了什麼。

FrameLayout.onLayout(xx)為子布局Layout的時候,起始坐標都是以FrameLayout為基準,並沒有記錄上一個子布局佔了哪塊位置,因此子布局的擺放位置可能會重疊,這也是FrameLayout布局特性的由來。而我們之前的Demo在水平方向上記錄了上一個子布局的擺放位置,下一個擺放時只能在它之後,因此就形成了水平擺放的功能。
由此類推,我們常說的某個子布局在父布局裡的哪個位置,決定這個位置的即是ViewGroup.onLayout(xx)。

上邊我們分析了View.layout(xx)、View.onLayout(xx)、ViewGroup.layout(xx)、ViewGroup.onLayout(xx),這四者什麼關系呢?
View.layout(xx)

View.onLayout(xx)

ViewGroup.layout(xx)

ViewGroup.onLayout(xx)

View/ViewGroup 子類需要重寫哪些方法:

用圖表示:

通過上述的描述,我們發現Measure過程和Layout過程里定義的方法比較類似:

它倆的套路比較類似:measure(xx)、layout(xx)一般不需要我們重寫,measure(xx)里調用onMeasure(xx),layout(xx)為調用者設置坐標值。
若是ViewGroup:onMeasure(xx)里遍歷子布局,並測量每個子布局,最後將結果匯總,設置自己測量的尺寸;onLayout(xx)里遍歷子布局,並設置每個子布局的坐標。
若是View:onMeasure(xx)則測量自身,並存儲測量尺寸;onLayout(xx)不需要做什麼。

Measure過程雖然比Layout過程復雜,但仔細分析後就會發現其本質就是為了設置兩個成員變數:

而Layout過程雖然比較簡單,其本質是為了設置坐標值

將Measure設置的變數和Layout設置的變數聯系起來:

此外,Measure過程通過設置PFLAG_LAYOUT_REQUIRED 標記來告訴需要進行onLayout,而Layout過程通過清除 PFLAG_FORCE_LAYOUT來告訴Measure過程不需要執行onMeasure了。
這就是Layout的承上作用

我們知道View的繪制需要依靠Canvas繪制,而Canvas是有作用區域限制的。例如我們使用:

Cavas繪制的起點是哪呢?
對於硬體繪制加速來說:正是通過Layout過程中設置的RenderNode坐標。
而對於軟體繪制來說:

關於硬體繪制加速/軟體繪制 後續文章會分析。

這就是Layout的啟下作用
以上即是Measure、Layout、Draw三者的內在聯系。
當然Layout的"承上"還需要考慮margin、gravity等參數的影響。具體用法參見最開始的Demo。

getMeasuredWidth()/getMeasuredHeight 與 getWidth/getHeight區別
我們以獲取width為例,分別來看看其方法:

getMeasuredWidth():獲取測量的寬,屬於"臨時值"
getWidth():獲取View真實的寬
在Layout過程之前,getWidth() 默認為0
何時可以獲取真實的寬、高

下篇將分析Draw()過程,我們將分析"一切都是draw出來的"道理

本篇基於 Android 10.0

F. android幀的繪制過程以及fps的獲取

幀的渲染過程中一些關鍵組件的流程圖

任何可以產生圖形信息的組件都統稱為圖像的生產者,比如OpenGL ES, Canvas 2D, 和 媒體解碼器等。

SurfaceFlinger是最常見的圖像消費者,Window Manager將圖形信息收集起來提供給SurfaceFlinger,SurfaceFlinger接受後經過合成再把圖形信息傳遞給顯示器。同時,SurfaceFlinger也是唯一一個能夠改變顯示器內容的服務。SurfaceFlinger使用OpenGL和Hardware Composer來生成surface.

某些OpenGL ES 應用同樣也能夠充當圖像消費者,比如相機可以直接使用相機的預覽界面圖像流,一些非GL應用也可以是消費者,比如ImageReader 類。

Window Manager是一個用於控制window的系統服務,包含一系列的View。每個Window都會有一個surface,Window Manager會監視window的許多信息,比如生命周期、輸入和焦點事件、屏幕方向、轉換、動畫、位置、轉換、z-order等,然後將這些信息(統稱window metadata)發送給SurfaceFlinger,這樣,SurfaceFlinger就能將window metadata合成為顯示器上的surface。

為硬體抽象層(HAL)的子系統。SurfaceFlinger可以將某些合成工作委託給Hardware Composer,從而減輕OpenGL和GPU的工作。此時,SurfaceFlinger扮演的是另一個OpenGL ES客戶端,當SurfaceFlinger將一個緩沖區或兩個緩沖區合成到第三個緩沖區時,它使用的是OpenGL ES。這種方式會比GPU更為高效。

一般應用開發都要將UI數據使用Activity這個載體去展示,典型的Activity顯示流程為:

一般app而言,在任何屏幕上起碼有三個layer:

那麼android是如何使用這兩種合成機制的呢?這里就是Hardware Composer的功勞。處理流程為:

借用google一張圖說明,可以將上面講的很多概念展現,很清晰。地址位於 https://source.android.com/devices/graphics/

即 Frame Rate,單位 fps,是指 gpu 生成幀的速率,如 33 fps,60fps,越高越好。
但是對於快速變化的游戲而言,你的FPS很難一直保持同樣的數值,他會隨著你所看到的顯示卡所要描畫的畫面的復雜程度而變化。

安卓系統中有 2 種 VSync 信號:

如上圖,CPU/GPU 向 Buffer 中生成圖像,屏幕從 Buffer 中取圖像、刷新後顯示。這是一個典型的生產者——消費者模型。理想的情況是幀率和刷新頻率相等,每繪制一幀,屏幕顯示一幀。而實際情況是,二者之間沒有必然的大小關系,如果沒有鎖來控制同步,很容易出現問題。

所謂」撕裂」就是一種畫面分離的現象,這樣得到的畫像雖然相似但是上半部和下半部確實明顯的不同。這種情況是由於幀繪制的頻率和屏幕顯示頻率不同步導致的,比如顯示器的刷新率是75Hz,而某個游戲的FPS是100. 這就意味著顯示器每秒更新75次畫面,而顯示卡每秒更新100次,比你的顯示器快33%。

兩個緩存區分別為 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中寫數據,屏幕從 Frame Buffer 中讀數據。VSync 信號負責調度從 Back Buffer 到 Frame Buffer 的復制操作,可認為該復制操作在瞬間完成。

雙緩沖的模型下,工作流程這樣的:

應用和SurfaceFlinger的渲染迴路必須同步到硬體的VSYNC,在一個VSYNC事件中,顯示器將顯示第N幀,SurfaceFlinger合成第N+1幀,app合成第N+2幀。

使用VSYNC同步可以保證延遲的一致性,減少了app和SurfaceFlinger的錯誤,以及顯示在各個階段之間的偏移。然而,前提是app和SurfaceFlinger每幀時間的變化並不大。因此,從輸入到顯示的延遲至少有兩幀。
為了解決這個問題,您可以使用VSYNC偏移量來減少輸入到顯示的延遲,其方法為將app和SurfaceFlinger的合成信號與硬體的VSYNC關聯起來。因為通常app的合成耗時是小於兩幀的(33ms左右)。
VSYNC偏移信號細分為以下3種,它們都保持相同的周期和偏移向量:

注意,當 VSync 信號發出時,如果 GPU/CPU 正在生產幀數據,此時不會發生復制操作。屏幕進入下一個刷新周期時,從 Frame Buffer 中取出的是「老」數據,而非正在產生的幀數據,即兩個刷新周期顯示的是同一幀數據。這是我們稱發生了「掉幀」(Dropped Frame,Skipped Frame,Jank)現象。

第一列t1: when the app started to draw (開始繪制圖像的瞬時時間)
第二列t2: the vsync immediately preceding SF submitting the frame to the h/w (VSYNC信令將軟體SF幀傳遞給硬體HW之前的垂直同步時間),也就是對應上面所說的軟體Vsync
第三列t3: timestamp immediately after SF submitted that frame to the h/w (SF將幀傳遞給HW的瞬時時間,及完成繪制的瞬時時間)

每mpsys SurfaceFlinger一次計算匯總出一個fps,計算規則為:
frame的總數N:127行中的非0行數
繪制的時間T:設t=當前行t2 - 上一行的t2,求出所有行的和∑t
fps=N/T (要注意時間轉化為秒)

一次mpsys SurfaceFlinger會輸出127幀的信息,但是這127幀可能是這個樣子:

如果t3-t1>16.7ms,則認為發生一次卡頓

設目標fps為target_fps,目標每幀耗時為target_ftime=1000/target_fps
從以下幾個維度衡量流暢度:

參考文章:

http://windrunnerlihuan.com/2017/05/21/VSync%E4%BF%A1%E5%8F%B7/

G. 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

H. Android 重學系列 View的繪制流程(六) 硬體渲染(上)

本文開始聊聊Android中的硬體渲染。如果跟著我的文章順序,從SF進程到App進程的繪制流程一直閱讀,我們到這里已經有了一定的基礎,可以試著進行橫向比對如Chrome瀏覽器渲染流程,看看軟體渲染,硬體渲染,SF合成都做了什麼程度的優化。

先讓我們回顧一下負責硬體渲染的主體對象ThreadedRenderer在整個繪制流程中做了哪幾個步驟。

在硬體渲染的過程中,有一個很核心的對象RenderNode,作為每一個View繪制的節點對象。

當每一次進行准備進行繪制的時候,都會雷打不動執行如下三個步驟:

如果遇到什麼問題歡迎來到 https://www.jianshu.com/p/c84bfa909810 下進行討論

實際上整個硬體渲染的設計還是比較龐大。因此本文先聊聊ThreadedRender整個體系中主要對象的構造以及相關的原理。

首先來認識下面幾個重要的對象有一個大體的印象。

在Java層中面向Framework中,只有這么多,下面是一一映射的簡圖。

能看到實際上RenderNode也會跟著View 樹的構建同時一起構建整個顯示層級。也是因此ThreadedRender也能以RenderNode為線索構建出一套和軟體渲染一樣的渲染流程。

僅僅這樣?如果只是這么簡單,知道我習慣的都知道,我喜歡把相關總結寫在最後。如果把總攬寫在正文開頭是因為設計比較繁多。因為我們如果以流水線的形式進行剖析容易造成迷失細節的困境。

讓我繼續介紹一下,在硬體渲染中native層的核心對象。

如下是一個思維導圖:

有這么一個大體印象後,就不容易迷失在源碼中。我們先來把這些對象的實例化以及上面列舉的ThreadedRenderer在ViewRootImpl中執行行為的順序和大家來聊聊其原理,先來看看ThreadedRenderer的實例化。

當發現mSurfaceHolder為空的時候會調用如下函數:

而這個方法則調用如下的方法對ThreadedRenderer進行創建:

文件:/ frameworks / base / core / java / android / view / ThreadedRenderer.java

能不能創建的了ThreadedRenderer則決定於全局配置。如果ro.kernel.qemu的配置為0,說明支持OpenGL 則可以直接返回true。如果qemu.gles為-1說明不支持OpenGL es返回false,只能使用軟體渲染。如果設置了qemu.gles並大於0,才能打開硬體渲染。

我們能看到ThreadedRenderer在初始化,做了三件事情:

關鍵是看1-3點中ThreadRenderer都做了什麼。

文件:/ frameworks / base / core / jni / android_view_ThreadedRenderer.cpp

能看到這里是直接實例化一個RootRenderNode對象,並把指針的地址直接返回。

能看到RootRenderNode繼承了RenderNode對象,並且保存一個JavaVM也就是我們所說的Java虛擬機對象,一個java進程全局只有一個。同時通過getForThread方法,獲取ThreadLocal中的Looper對象。這里實際上拿的就是UI線程的Looper。

在這個構造函數有一個mDisplayList十分重要,記住之後會頻繁出現。接著來看看RenderNode的頭文件:
文件:/ frameworks / base / libs / hwui / RenderNode.h

實際上我把幾個重要的對象留下來:

文件:/ frameworks / base / core / java / android / view / RenderNode.java

能看到很簡單,就是包裹一個native層的RenderNode返回一個Java層對應的對象開放Java層的操作API。

能看到這個過程生成了兩個對象:

這個對象實際上讓RenderProxy持有一個創建動畫上下文的工廠。RenderProxy可以通過ContextFactoryImpl為每一個RenderNode創建一個動畫執行對象的上下文AnimationContextBridge。

文件:/ frameworks / base / libs / hwui / renderthread / RenderProxy.cpp

在這里有幾個十分重要的對象被實例化,當然這幾個對象在聊TextureView有聊過( SurfaceView和TextureView 源碼淺析 ):

我們依次看看他們初始化都做了什麼。

文件:/ frameworks / base / libs / hwui / renderthread / RenderThread.cpp

能看到其實就是簡單的調用RenderThread的構造函數進行實例化,並且返回對象的指針。

RenderThread是一個線程對象。先來看看其頭文件繼承的對象:
文件:/ frameworks / base / libs / hwui / renderthread / RenderThread.h

其中RenderThread的中進行排隊處理的任務隊列實際上是來自ThreadBase的WorkQueue對象。

文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h

ThreadBase則是繼承於Thread對象。當調用start方法時候其實就是調用Thread的run方法啟動線程。

另一個更加關鍵的對象,就是實例化一個Looper對象到WorkQueue中。而直接實例化Looper實際上就是新建一個Looper。但是這個Looper並沒有獲取當先線程的Looper,這個Looper做什麼的呢?下文就會揭曉。

WorkQueue把一個Looper的方法指針設置到其中,其作用可能是完成了某一件任務後喚醒Looper繼續工作。

而start方法會啟動Thread的run方法。而run方法最終會走到threadLoop方法中,至於是怎麼走進來的,之後有機會會解剖虛擬機的源碼線程篇章進行講解。

在threadloop中關鍵的步驟有如下四個:

在這個過程中創建了幾個核心對象:

另一個核心的方法就是,這個方法為WorkQueue的Looper注冊了監聽:

能看到在這個Looper中注冊了對DisplayEventReceiver的監聽,也就是Vsync信號的監聽,回調方法為displayEventReceiverCallback。

我們暫時先對RenderThread的方法探索到這里,我們稍後繼續看看回調後的邏輯。

文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h

能看到這里的邏輯很簡單實際上就是調用Looper的pollOnce方法,阻塞Looper中的循環,直到Vsync的信號到來才會繼續往下執行。詳細的可以閱讀我寫的 Handler與相關系統調用的剖析 系列文章。

文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h

實際上調用的是WorkQueue的process方法。

文件:/ frameworks / base / libs / hwui / thread / WorkQueue.h

能看到這個過程中很簡單,幾乎和Message的loop的邏輯一致。如果Looper的阻塞打開了,則首先找到預計執行時間比當前時刻都大的WorkItem。並且從mWorkQueue移除,最後添加到toProcess中,並且執行每一個WorkItem的work方法。而每一個WorkItem其實就是通過從某一個壓入方法添加到mWorkQueue中。

到這里,我們就明白了RenderThread中是如何消費渲染任務的。那麼這些渲染任務又是哪裡誕生呢?

上文聊到了在RenderThread中的Looper會監聽Vsync信號,當信號回調後將會執行下面的回調。

能看到這個方法的核心實際上就是調用drainDisplayEventQueue方法,對ui渲染任務隊列進行處理。

能到在這里mVsyncRequested設置為false,且mFrameCallbackTaskPending將會設置為true,並且調用queue的postAt的方法執行ui渲染方法。

還記得queue實際是是指WorkQueue,而WorkQueue的postAt方法實際實現如下:
/ frameworks / base / libs / hwui / thread / WorkQueue.h

情景帶入,當一個Vsync信號達到Looper的監聽者,此時就會通過WorkQueue的drainDisplayEventQueue 壓入一個任務到隊列中。

每一個默認的任務都是執行dispatchFrameCallback方法。這里的判斷mWorkQueue中是否存在比當前時間更遲的時刻,並返回這個WorkItem。如果這個對象在頭部needsWakeup為true,說明可以進行喚醒了。而mWakeFunc這個方法指針就是上面傳下來:

把阻塞的Looper喚醒。當喚醒後就繼續執行WorkQueue的process方法。也就是執行dispatchFrameCallbacks方法。

在這里執行了兩個事情:

先添加到集合中,在上面提到過的threadLoop中,會執行如下邏輯:

如果大小不為0,則的把中的IFrameCallback全部遷移到mFrameCallbacks中。

而這個方法什麼時候調用呢?稍後就會介紹。其實這部分的邏輯在TextureView的解析中提到過。

接下來將會初始化一個重要對象:

這個對象名字叫做畫布的上下文,具體是什麼上下文呢?我們現在就來看看其實例化方法。
文件:/ frameworks / base / libs / hwui / renderthread / CanvasContext.cpp

文件:/ device / generic / goldfish / init.ranchu.rc

在init.rc中默認是opengl,那麼我們就來看看下面的邏輯:

首先實例化一個OpenGLPipeline管道,接著OpenGLPipeline作為參數實例化CanvasContext。

文件:/ frameworks / base / libs / hwui / renderthread / OpenGLPipeline.cpp

能看到在OpenGLPipeline中,實際上就是存儲了RenderThread對象,以及RenderThread中的mEglManager。透過OpenGLPipeline來控制mEglManager進而進一步操作OpenGL。

做了如下操作:

文件:/ frameworks / base / libs / hwui / renderstate / RenderState.cpp

文件:/ frameworks / base / libs / hwui / renderthread / DrawFrameTask.cpp

實際上就是保存這三對象RenderThread;CanvasContext;RenderNode。

文件:/ frameworks / base / core / jni / android_view_ThreadedRenderer.cpp

能看到實際上就是調用RenderProxy的setName方法給當前硬體渲染對象設置名字。

文件:/ frameworks / base / libs / hwui / renderthread / RenderProxy.cpp

能看到在setName方法中,實際上就是調用RenderThread的WorkQueue,把一個任務隊列設置進去,並且調用runSync執行。

能看到這個方法實際上也是調用post執行排隊執行任務,不同的是,這里使用了線程的Future方式,阻塞了執行,等待CanvasContext的setName工作完畢。

熱點內容
編程主機電腦 發布:2024-11-29 00:43:51 瀏覽:983
oc訪問成員變數嗎 發布:2024-11-29 00:14:59 瀏覽:517
七牛雲伺服器生成縮略圖 發布:2024-11-29 00:12:36 瀏覽:272
如何重設華為賬號密碼 發布:2024-11-29 00:03:33 瀏覽:813
安卓聽小說下載到哪個文件夾 發布:2024-11-29 00:03:01 瀏覽:932
閑魚掛腳本 發布:2024-11-29 00:01:27 瀏覽:630
ae加快緩存 發布:2024-11-28 23:50:34 瀏覽:342
java的版本號 發布:2024-11-28 23:48:18 瀏覽:100
sql存儲過程區別 發布:2024-11-28 23:35:37 瀏覽:919
ms計算機需要什麼配置 發布:2024-11-28 23:34:21 瀏覽:975