1 前言
關於view的繪製流程,現在網上一查,就會直接告訴你,view的繪製流程是先onMeasure,然後onLayout,在最後onDraw,沒錯,繪製流程確實也是這樣。不過我們今天要討論的話題主要是知道這個流程是怎麼來的,然後順便淺嘗他們的內部的實現流程和邏輯。這樣大家就會對view的繪製流程有一個比較清晰的認識。
這裏我想從view的一些常用方法來進行研究,就從invalidate這個方法開始吧。相信這個方法很多人都用過,在自定義view的時候,或者在刷新view的時候,大家會經常用到這個函數。所以通俗了講,這個函數就是用來刷新view的。那麼就開始吧。
注:以下代碼來源API 14
爲什麼要用這麼老的代碼分析是有原因的,因爲越先考慮到的方案一定是越粗暴越簡單的,所以我們可以通過了解API 14的源碼,很快也很清晰的認識到整個view的繪製流程,當討論完API 14的源碼之後,我將會帶你走一遍API 28的源碼,不過這個時候,看API 28的源碼對你也就相對簡單了。
2 invalidate分析
開門見山,直接開看invalidate的源碼:
public void invalidate() {
invalidate(true);
}
顯然這種源碼是無法滿足我們的求知慾的,所以讓我們跟進去。
void invalidate(boolean invalidateCache) {
...
final ViewParent p = mParent;
...
if(xxx){
...
p.invalidateChild(this, null);
return;
}
if(xxx){
...
p.invalidateChild(this, r);
}
...
}
我們可以看到,實際上我們是調用了ViewParent的invalidateChild方法,而ViewParent實際上只是一個接口,那麼是由誰實現的這個接口呢,其實就是ViewRootImpl,那就來吧。
// ViewRootImpl.java
public void invalidateChild(View child, Rect dirty) {
...
scheduleTraversals();
...
}
繼續跟蹤:
// ViewRootImpl.java
public void scheduleTraversals() {
...
sendEmptyMessage(DO_TRAVERSAL);
...
}
哈,可以直接發送message,沒錯,ViewRootImpl其實繼承handler,也不難猜,畢竟只能在主線程更新UI,所以用到handler也不奇怪,那我們就接着看吧。
// ViewRootImpl.java
@Override
public void handleMessage(Message msg) {
switch(msg.what){
...
case DO_TRAVERSAL:
...
performTraversals();
...
break;
...
}
}
接着我們進入performTraversals(),不過這個方法奇長無比,我們要找的繪製流程,其實全都在這裏,所以讓我們來慢慢欣賞吧。
不如繼續簡化版:
// ViewRootImpl.java
private void performTraversals() {
final View host = mView;
...
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
mView.draw(canvas);
...
}
簡化的是不是太簡單了,其實這個方法裏面有很多大量且複雜的變量賦值設置轉換等等的代碼。
但是這樣有利於我們分析代碼主幹邏輯,不是嗎。
不過這裏首先講一下方法裏面的mView是啥,如果是調用的invalidate,那這裏的view自然是調用invalidate方法的view,如果是setContentView,那麼這裏的view就是DecorView,我在setContentView的時候,到底發生了什麼這篇文章有講到這個DecorView。
分析到這裏,我想大家就很熟悉了,view繪製的三大流程,measure、layout、draw,其實分析setContentView也能很容易分析到這裏來。
接下來我們重點看看這三個方法是一個怎樣的過程。
3 繪製流程
3.1 測量
首先我們來看看測量,代碼是
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
那麼我們先來看看這裏的這兩個參數具體是什麼東西。
聽名字是跟寬高有關的東西,確實是這樣,這兩個參數代表了這個view的寬和高。
我們來看看view的measure方法是怎麼處理這兩個參數的
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
onMeasure,這不就是我們平時自定義view要重寫的方法嗎,所以這也是爲什麼我們可以通過這個方法來確認view大小的原因。
我們是否有過自定義view,沒有重寫這個方法,回想一下,會出現什麼情況,我們的view會鋪滿整個父容器對嗎。
如果我們沒有重寫onMeasure方法,那麼調用的方法是什麼呢,當然是view類自己實現的onMeasure方法啦,不如我們來看看view自己實現的onMeasure做了什麼事。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
看似簡單,卻方法嵌套,並不好分析,應該先分析外層還是內層呢,當然是外層啦,讓我們來看看setMeasuredDimension方法。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET;
}
很簡單,就是單純的賦值,將測量出來的寬高賦值給view類的寬高兩個屬性。自此我們可以看出,view的真實寬高其實就保存在mMeasuredWidth和mMeasuredHeight這兩個變量之中。
接着我們再來看看getDefaultSize方法
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
這裏出現了MeasureSpec,大概講一下。MeasureSpec是類,所以我們可以將其類比成Integer類,不過這個類不僅僅保存了值,還保存了這個值的模式,無論是寬還是高,都有一種模式,所以MeasureSpec保存了這個數據的模式和值,值就是具體的值,比如寬20高20,這就是具體的值。
一個MeasureSpec類型的變量,我們可以通過MeasureSpec.getMode(measureSpec);
得到這個變量的模式,通過MeasureSpec.getSize(measureSpec);
得到這個變量的值。
那麼模式又分爲哪些呢,分爲三種
- EXACTLY
- AT_MOST
- UNSPECIFIED
這三種具體代表什麼意思,通俗來講EXACTLY類似MATCH_PARENT,這個很熟悉吧,即填充父容器,寬或者高填充父容器。
AT_MOST類似WRAP_CONTENT,自己有多大就佔多大地方。
我們可以看到前兩種模式都能夠計算出一個具體的值,有具體的寬度和高度,但是UNSPECIFIED比較奇特,沒有明顯的值,爲什麼會出現這種東西,大小都不能確定的view,這有可能嗎,當然有可能,ListView不就是一個典型的例子嗎,沒有特定的高度,UNSPECIFIED就是用於這種view的。
好了,MeasureSpec講完了,我們回過頭來繼續看getDefaultSize方法。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
還記得這個方法是幹嘛的嗎,獲得具體的寬和高。將計算出來的值賦予view的mMeasuredWidth和mMeasuredHeight這兩個變量。
首先我們來看參數,第一個參數是view自己計算出來的,第二個參數這是在ViewRootImpl#performTraversals()方法中計算出來的,所以記住,第二個參數是在其他地方計算出來的,這意味着精準。
所以我們在getDefaultSize方法中可以看到,AT_MOST和EXACTLY模式下,系統都選擇了使用在其他地方計算出來的值作爲寬高。
也是由於AT_MOST和EXACTLY模式下,View的寬高沒作任何處理,所以我們自定義view的時候需要在這裏作一下處理,比如處理一下AT_MOST模式下的情況,畢竟AT_MOST模式意味着WRAP_CONTENT。不過也可以理解,畢竟系統並不知道你自定義的view到底長什麼樣,對寬高要有怎樣的要求。
接下來我們來看看getDefaultSize方法的第一個參數是怎麼來的,這並不是在其他地方計算出來的,而是view內部就計算出來了,所以歸類爲UNSPECIFIED模式。第二個參數分別從getSuggestedMinimumWidth()和getSuggestedMinimumHeight()得到,其實這兩個方法差不多,我們那一個方法舉例子。
protected int getSuggestedMinimumWidth() {
int suggestedMinWidth = mMinWidth;
if (mBGDrawable != null) {
final int bgMinWidth = mBGDrawable.getMinimumWidth();
if (suggestedMinWidth < bgMinWidth) {
suggestedMinWidth = bgMinWidth;
}
}
return suggestedMinWidth;
}
首先告訴大家mMinWidth變量是什麼東西,這個東西默認爲0,其實我們可以直接在佈局文件裏給這個變量賦值,像這樣:
<TextView
android:minHeight="10dp"
android:minWidth="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
如果沒有定義,這個值就是0。
至於mBGDrawable這個變量,這是指view的背景,也可以在佈局文件裏面設置,如果沒有,那就沒有了。
自此,我們基本上就算分析完了整個測量的過程。
3.2 佈局
是以下代碼將我們拉入佈局這裏的
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
所以我們進入view的layout方法,一探究竟。
public void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
...
}
changed變量記錄着該view是否有過位置變化,其實其實單單看view的佈局方法未免有些單調,因爲onLayout方法如下
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
這裏是空的,原因很簡單。回想一下,我們什麼時候纔會重寫onLayout,當我們在自定義ViewGroup的時候,纔會重寫onLayout,而這個方法的目的是確定ViewGroup的子View的位置,所以View類裏面的這個方法必然是空的。
不過要知道,我們應用的界面,肯定不會直接使用ViewGroup,而是使用他們的子類,比如LinearLayout,這些子類對onLayout方法的重寫肯定是很到位的。
所以我們還是直接進入繪製這個流程吧。
3.3 繪製
這裏使用到的是View的draw方法,是draw,不是onDraw哦,這個方法十分親民,因爲方法內部註釋了繪製view的整個流程,如下:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 繪製遍歷執行幾個繪製步驟,這些步驟必須按適當的順序執行:
* 1. Draw the background
* 繪製背景
* 2. If necessary, save the canvas' layers to prepare for fading
* 如有必要,保存畫布的圖層以備漸變
* 3. Draw view's content
* 繪製視圖的內容
* 4. Draw children
* 繪製子view
* 5. If necessary, draw the fading edges and restore layers
* 如有必要,繪製漸變邊緣並恢復圖層
* 6. Draw decorations (scrollbars for instance)
* 繪製裝飾品(例如滾動條)
*/
而我們在自定義view的時候,通常不是會重寫onDraw方法嗎,這個方法在上述第三步被調用,也就是視圖的主要內容就是onDraw繪製的。
(感覺註釋把繪製的流程寫的好清楚= =)
API 28 源碼分析
接下來我們來看API 28的源碼,由於很多邏輯都和API 14相同,所以我們快速過一遍即可。
也從invalidate開始分析好了,其實前面都差不多,都可以很方便的跟蹤到ViewRootImpl#scheduleTraversals()方法來,不過API 14 和API 28在這裏的處理有區別,28主要源碼如下:
// ViewRootImpl.java
void scheduleTraversals() {
...
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
...
performTraversals();
...
}
然後就又來到包含了測量佈局和繪製的performTraversals方法來了。
// ViewRootImpl.java
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
其實這裏省略了的代碼量其實非常大,如果沒有目的的看這個方法,相信要看很久很久。經過8年(11~18)的優化和演變,這個方法在繪製view上的主要邏輯還是沒有變,可想當初在設計view的時候,下了多大的功夫,才能這麼穩定。閒話不說,先看看測量有關的源碼。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
代碼很少,這裏就不做省略了,看源碼,沒想到Android源碼也用上了Systrace工具。
我們可以看到依然調用了view的measure方法。而measure方法內部也調用了View的onMeasure,幾乎和API 14的源碼相同,這裏就不多做展示了。接下來我們來看佈局performLayout。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
省略之後好像也沒有什麼變化是吧,layout方法裏面自然也調用了view的onLayout方法,相比14的變化,其實很少,大多都是把14原有的代碼進行了封裝,變得更好被使用,然後新加了很多情景條件而已。
接下來看繪製。
API 28裏面都繪製倒是變得複雜不少,應該是這些年來,Android的UI變化很大的原因導致的吧。我們使用跟蹤法來看。
private void performDraw() {
...
boolean canUseAsync = draw(fullRedrawNeeded);
...
}
然後我們繼續看draw方法。
private boolean draw(boolean fullRedrawNeeded) {
...
drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)
...
}
這個方法絕對沒有上述那麼簡單,而且很複雜,裏面牽扯了大量和渲染器有關的代碼,而且還考慮到了view被滑動時的UI繪製,比API 14不知道複雜到哪裏去了= =
我們接着看。
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
這個方法也不是這麼簡單,看參數就知道,這個方法不僅涉及繪製,而且主要是對view的偏移量的計算和控制。
關於view的draw就不說了,跟API 14一樣,區別不大,而且在註釋裏面也很貼心的記錄了繪製的具體過程,雖然上面我已經貼過了,不過這裏再貼一次。
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 繪製遍歷執行幾個繪製步驟,這些步驟必須按適當的順序執行:
* 1. Draw the background
* 繪製背景
* 2. If necessary, save the canvas' layers to prepare for fading
* 如有必要,保存畫布的圖層以備漸變
* 3. Draw view's content
* 繪製視圖的內容
* 4. Draw children
* 繪製子view
* 5. If necessary, draw the fading edges and restore layers
* 如有必要,繪製漸變邊緣並恢復圖層
* 6. Draw decorations (scrollbars for instance)
* 繪製裝飾品(例如滾動條)
*/
同樣在第三步的時候,調用了view的onDraw方法。
好了,關於API 28的源碼探索就到這裏吧。
後話
本文討論的主要是view的三大流程的來源,以及這三大流程裏面大概做了什麼,並沒有涉及詳細的代碼分析,主要是爲了給大家一個大概的結構,而非鑽研其實現。並且還分析了API 14和API 28,我們發現其實這兩個版本的代碼其實在主幹上其實並沒有多少差別。只是後者更加完善,並且實現方式也更加巧妙了。所以view的流程分析大概就到這裏吧。