UI 優化系列專題,來聊一聊 Android 渲染相關知識,主要涉及 UI 渲染背景知識、如何優化 UI 渲染兩部分內容。
UI 優化系列專題
- UI 渲染背景知識
《View 繪制流程之 setContentView() 到底做了什么?》
《View 繪制流程之 DecorView 添加至窗口的過程》
《深入 Activity 三部曲(3)View 繪制流程》
《Android 之 LayoutInflater 全面解析》
《關于渲染,你需要了解什么?》
《Android 之 Choreographer 詳細分析》
- 如何優化 UI 渲染
《Android 之如何優化 UI 渲染(上)》
《Android 之如何優化 UI 渲染(下)》
在 View 繪制流程系列,分別介紹了 View 的創建以及添加至窗口的過程,它們也是為今天要分析的 View 繪制任務做的鋪墊,View 的繪制流程主要包含三個階段:measure -> layout -> draw。
在具體分析之前,還是通過幾個問題來了解下今天要分析的內容:
- Handler 異步消息的作用?
- Android 是如何解決不確定的布局尺寸?即 MATCH_PARENT 或 WRAP_CONTENT。
- 為什么 View.GONE 不會占用布局空間?
- getWidth() 和 getMeasuredWidth() 有什么區別?在什么時候調用才會有值?
requestLayout()
View 繪制的起始點是在 ViewRootImpl 的 requestLayout 方法,前面有分析到在該方法首先會檢查是否在原線程。這里簡單說下,UI 的繪制并非一定要在主線程,但是它要求是在原線程,絕大多數操作系統 UI 框架都是單線程的,這主要是因為多線程的 UI 框架在設計上會非常復雜。
然后通過 scheduleTraversals 方法發送消息開始 View 繪制流程:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//編舞者,可以用它來監聽幀頻
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//... 省略
}
}
注意 postSyncBarrier() 發送同步屏障消息,可能很多人不知道 Handler 有兩種 Message 類型。
- 同步消息(普通消息)
- 異步消息(Android 4.1 新增,配合 VSYNC 信號)
Handler 的構造方法提供了用于區分兩種消息的構造方法,不過它們被 @hide 了,但是 Message 為我們敞開了:
//設置為異步消息
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
//獲取當前消息類型,是同步消息還是異步消息
public boolean isAsynchronous() {
return (flags & FLAG_ASYNCHRONOUS) != 0;
}
一般情況下同步消息和異步消息的處理方式并沒有什么區別,只有在設置了同步屏障時才會出現差異。同步屏障為 Handler 消息機制增加了一種簡單的優先級關系,異步消息的優先級要高于同步消息,用于配合系統的 VSYNC 信號。簡單點說,設置了同步屏障之后,Handler 只會處理異步消息。
但是發送同步屏障的接口并沒有對應用開發者公開,其實它的主要作用是為了更快的響應 UI 繪制事件,避免長時間等待于消息隊列。
繼續分析,發送 UI 繪制任務 mTraversalRunnable 到 Choreographer。
//編舞者,可以用它來監聽幀頻
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
- Choreographer 是負責獲取 VSYNC 同步信號并統一調度 UI 的繪制任務。Choreographer 是線程級別單例,并且具有處理當前線程消息隊列(MessageQueue)的能力。關于 Choreographer 更詳細的分析,可以參考《Android 之 Choreographer 詳細分析》。
// 線程級別單例,肯定不會感到默認,最簡單的方式使用ThreadLocal
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 當前線程Looper,當前的分析在主線程
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
// 為當前線程創建一個Choreographer
return new Choreographer(looper, VSYNC_SOURCE_APP);
}
};
看下 Choreographer 的構造方法,注意 FrameHandler 接收對應線程的 Looper 對象。
private Choreographer(Looper looper) {
//當前線程Looper
mLooper = looper;
//創建handle對象,用于處理消息
mHandler = new FrameHandler(looper);
//創建VSYNC的信號接受對象
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
//初始化上一次frame渲染的時間點
mLastFrameTimeNanos = Long.MIN_VALUE;
//計算幀率,也就是一幀所需的渲染時間,getRefreshRate是刷新率,一般是60
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//創建消息處理隊列
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
- mTraversalRunnable
mTraversalRunnable 本質是一個 Runnable,通過 mChoreographer.postCallback() 發送到主線程消息隊列(這里以主線程繪制流程做分析)。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//開始執行繪制遍歷
doTraversal();
}
}
doTraversal() 真正開始執行 UI 繪制的遍歷過程:
void doTraversal() {
if (mTraversalScheduled) {
//防止重復
mTraversalScheduled = false;
//移除屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//執行UI繪制的遍歷過程
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
mTraversalScheduled 變量主要防止重復的繪制任務,removeSyncBarrier 方法移除同步屏障,因為此時 View 繪制任務已經處于執行過程中。performTraversals() 將依次完成 View 的三大繪制流程:performMeasure()、performLayout() 和 performDraw()。
private void performTraversals() {
// 當前DecorView
final View host = mView;
// ... 省略
//想要展示窗口的寬高
int desiredWindowWidth;
int desiredWindowHeight;
if (mFirst) {
//將窗口信息依附給DecorView
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
//開始進行布局準備
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null) {
// ... 省略
if (!mStopped) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
// DecorView默認LayoutParams的屬性是MATCH_PARENT
// 此時的寬度測量模式為EXACTLY(表示確定大小), 測量大小為窗口寬度大小,因為DecorView的LayoutParams為MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
// 高度測量模式也是確定的EXACTLY,測量大小為窗口高度大小
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// 執行View測量工作,計算出每個View尺寸
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
/*******部分代碼省略**********/
if (measureAgain) {
//View的測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
} else {
/*******部分代碼省略**********/
}
final boolean didLayout = layoutRequested /*&& !mStopped*/ ;
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
//View的布局
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
/*******部分代碼省略**********/
}
/*******部分代碼省略**********/
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
/*******部分代碼省略**********/
//View的繪制
performDraw();
}
} else {
if (viewVisibility == View.VISIBLE) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
mIsInTraversal = false;
}
}
首先需要說明 DecorView 的 LayoutParams 寬高默認為 MATCH_PARENT,即窗口尺寸。
public LayoutParams() {
//默認是MATCH_PARENT
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
在具體分析測量過程之前,先要講下 Android 系統為自適應布局尺寸引入了 LayoutParams.MATCH_PARENT 和 LayoutParams.WRAP_CONTENT,這樣就會有不確定的情況,那 Android 又是如何解決不確定的布局尺寸呢?
答案就是 MeasureSpec( 測量規格),它本質是 4 個字節的 int 數值,主要包含兩部分:高 2 位表示測量模式,低 30 位表示測量大小。
- 測量模式
MeasureSpec.EXACTLY:精確大小,父容器已經測量出所需要的精確大小,這也是我們 childView 的最終大小 — MATCH_PARENT。
MeasureSpec.AT_MOST: 最終的大小不能超過我們的父容器 — WRAP_CONTENT。
UNSPECIFIED:不確定的,源碼內部使用,一般在 ScorllView、ListView 中能看到這些,需要動態測量。
在 MeasureSpec 中測量模式關鍵方法:
/**
* 獲取測量模式,取最高兩位
*/
public static int getMode(int measureSpec) {
//noinspection ResourceType
//MODE_MASK為110000000000000000000000000000
return (measureSpec & MODE_MASK);
}
- 測量大小
測量大小是根據測量模式來確定,在 Measure 流程中,系統將 View 的 LayoutParams 根據父容器施加的規則轉化成對應的 MeasureSpec,在 onMeasure() 中根據這個 MeasureSpec 來確定 View 的測量寬高。
在 MeasureSpec 中測量大小關鍵方法:
/**
* 獲取測量大小,取低30位
*/
public static int getSize(int measureSpec) {
//~MODE_MASK為00111111111111111111111111111111
return (measureSpec & ~MODE_MASK);
}
說道這里,需要先看下表示窗口視圖 DecorView 的測量模式和測量大小:
// 獲取DecorView的寬高測量規格
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
// DecorView默認走這里
case ViewGroup.LayoutParams.MATCH_PARENT:
// 此時測量大小就是窗口大小,測量模式就是EXACTLY,表示確定的
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// 此時測量模式可調整的,即AT_MOST(最大),最大為窗口大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
//返回測量規格
return measureSpec;
}
由于 DecorView 的寬高為 MATCH_PARENT,故它的寬高測量規格都為:EXACTLY + windowSize(窗口大小,視具體手機屏幕決定)。
1. performMeasure()
接下來開始 View 的測量工作,注意 mView 實際是 DecorView 如下:
/**
* 執行View的測量工作
*/
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
// mView實際是DecorView
// 也就是真正測量工作是從DecorView開始的
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
調用 DecorView 的 measure 方法,不過 measure 方法是 View 獨有的,并且被聲明為 final。
/**
* View的measure方法
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 是否有光學邊界
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
// 將寬度測量規格和高度測量規格整合成long,高32位為寬度,低32位為高度測量規格
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
// 緩存當前測量結果的容器
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
// 是否強制布局
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 這里主要是做優化,防止在未發生變化的情況下,無謂的測量工作
// 寬度和高度測量規格是否發生過變化
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
// 寬高的測量模式是否為EXACTLY
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
// 新的測量大小是否等于當前測量大小
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
// 如果與上次測量結果發生變化此時需要重寫測量
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
// < 0 表示當前測量已經失效(緩存不存在),需要重新測量
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
// 否則測量結果未發生變化,value高32位為寬度,低32位為高度
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
// 注意這個標志位,標記在layout之前需要measure
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 如果自定義View中沒有調用setMeasuredDimension(),會拋出異常。
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// 保存最新測量規格
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
// 緩存當前測量結果
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
可以看到系統對 View 的測量工作做了大量的優化,只為有效減少無謂的測量工作,提高 UI 渲染性能。
- 注意代碼中 if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET),如果我們在自定義 View 過程中,最后沒有給 View 設置測量大小,即 setMeasuredDimension() ,此時將會拋出異常。后面會分析到。
如果需要測量,此時調用 onMeasure 方法,onMeasure 方法的設計與 measure 方法不同,measure 方法在 View 中設計為 final,而 onMeasure 方法旨在子 View 重寫該方法,這也很容易理解,View 的最終大小需要自行去測量。
- 注意:onMeasure 方法是需要具體 View 自行實現,所以在 ViewGroup 中沒有實現該方法。
我們先來看下 View 的默認測量過程 onMeasure 方法如下:
/**
* View默認的onMeasure方法
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//getSuggestedMinimumWidth確定當前View的最小尺寸,根據最小尺寸與背景尺寸取較大值
//getDefaultSize()確定子View的尺寸
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
- getSuggestedMinimumXxx(),從名字看是獲取 View 最小建議尺寸,以寬度為例:
/**
* 確定View的最小寬度,根據最小寬度和背景寬度取較大值
*/
protected int getSuggestedMinimumWidth() {
// 沒有背景圖,則使用最小寬度
// 否則取最小寬度和背景寬度的較大值
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
- getDefaultSize(),根據最小建議尺寸和測量大小決定 View 的最終尺寸:
/**
* size為當前View的最小尺寸
* measureSpec,當前View的測量規格
*/
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:
//如果測量模式為不確定
//此時尺寸就是View的最小尺寸
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
//此時為View的測量大小
result = specSize;
break;
}
return result;
}
- setMeasuredDimension() 最終執行到 setMeasuredDimensionRaw() 設置 View 的測量大小:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
// 賦值給View成員
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// 標志,已經設置View的測量大小
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
記得上面說到的自定義 View ,如果在其 onMeasure 方法中沒有調用 setMeasureDimension 方法,將會拋出異常,此時在方法最后修改該標志位:
// 標志,已經設置View的測量大小
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
注意,文章開頭提出的問題 getMeasuredWidth() / getMeasuredHeight() 在什么時候才會獲取到值?答案就在這里,setMeasuredDimensionRaw 方法執行結束,此時調用 View 的 getMeasureXxx(),便可以拿到 View 的測量大小了。
// 獲取測量寬度
public final int getMeasuredWidth() {
// measuredWidth & 0x00ffffff,舍去高2為測量模式,取低30位測量大小
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
// 獲取測量高度
public final int getMeasuredHeight() {
// 原理一致
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
即 View 的測量大小在 measure 階段完成之后便可以獲取到。注意此時 getWidth() / getHeight() 仍然無法爭取獲取!
分析完了 View 的默認測量規則,但由于 DecorView 繼承自 FrameLayout,所以此時 onMeasure 實際調用的是 FrameLayout 的 onMeasure 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 獲取子View數量
int count = getChildCount();
// 確定當前View寬高測量模式存在非EXACTLY,注意這將有可能導致FrameLayout二次測量
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
// 遍歷子View
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 為什么GONE不占用空間就在這里
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 測量子View的大小
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 獲取子View的LayoutParams,獲取其他布局參數
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 記錄當前最大寬度
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 記錄當前最大高度
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//如果當前FrameLayout寬高存在不是EXACTLY。
if (measureMatchParentChildren) {
//如果子View存在需要依賴父容器的測量大小
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
//加入需要二次測量
mMatchParentChildren.add(child);
}
}
}
}
// 最大寬度累加自身padding
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
// 最大高度累加自身padding
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// 與最小高度取較大值
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
// 與最小寬度取較大值
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
//如果存在foreground drawable
if (drawable != null) {
// 判斷與foreground drawable 高度取較大值
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
// 判斷與foreground drawable 寬度取較大值
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
//設置測量大小
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
/**
* 1.如果FrameLayout 的寬高測量模式存在非EXACTLY
* 2.與包含的子View需要依賴父View的測量大小時,(子View存在MATCH_PARENT)
* 此時需要二次測量
* */
count = mMatchParentChildren.size();
//此時需要二次測量
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
注意 measureMatchParentChildren 變量,它用于標注當前 FrameLayout 寬/高測量模式是否存在非 MeasureSpec.EXACTLY。這會導致 FrameLayout 在測量階段的性能問題 — 二次測量,后面分析到。
第一個 for 循環開始遍歷測量所有子 View,注意條件:
child.getVisibility() != GONE
這就是為什么 View.GONE 不會占用布局空間,View.GONE 在測量階段默認被忽略。
開始測量子 View 過程,measureChildWithMargins 方法如下:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 子View的寬度測量規格,根據父容器施加的規則,加上寬度內邊距
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
// 子View的高度測量規格,根據父容器施加的規則,加上高度內邊距
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// 調用子View的measure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
看下如何確定子 View 的測量規格,getChildMeasureSpec 方法如下:
/**
* 根據父容器的測量規格確定子View的測量規格
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父容器施加的測量模式
int specMode = MeasureSpec.getMode(spec);
//獲取父容器施加的測量大小
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
//返回當前View的測量大小
int resultSize = 0;
//返回當前View的測量模式
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
//如果子View的尺寸是固定的
//測量大小就是View設置的具體值childDimension(lp.width)
//測量模式就是精確的EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
//如果是MATCH_PARENT,此時表示子View使用父容器尺寸
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//子View的大小不確定,但是最大不超過父容器大小
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
//此時子View的尺寸也是確定的
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
//此時子View的最大大小為父容器大小
//測量模式是AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
// 子View的尺寸不能確定,但是最大不能超過父容器
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
// 子View的尺寸是確定的
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
//子View需要動態的測量
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
//子View需要動態的測量
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
//生成子View的測量規格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
前面也有簡單提到子 View 要根據父 View 施加的測量規格決定自己的測量大小。關于子 View 的測量規格這里做下簡單總結:
如果子 View 的 LayoutParams 為具體數值,此時無論父 View 施加測量模式是什么,子 View 的測量規格都為 EXACTLY + childDimension(在 LayoutParams 中設置的具體數值)。
如果子 View 的 LayoutParams 為 MATCH_PARENT,當父 View 的測量模式為 EXACTLY 時,子 View 的測量規格為 EXACTLY + 小于等于父 View 的測量大小;當父 View 的測量模式為 AT_MOST 時,子 View 的測量規格為 AT_MOST + 小于等于父 View 的測量大小;當父 View 的測量大小為 UNSPECIFIED 時,子 View 的測量規格為 UNSPECIFIED + 0。
如果子 View 的 LayoutParams 為 WRAP_CONTENT,當父 View 的測量模式為 EXACTLY,子 View 的測量規格為 AT_MOST + 小于等于父 View 測量大小;當父 View 的測量規格為 AT_MOST 時,子 View 的測量規格為 AT_MOST + 小于等于父 View 的測量大小;當父 View 的測量模式為 UNSPECIFIED 時,子 View 的測量規格為 UNSPECIFIED + 0。
在 measureChildWithMargins 方法最后,調用 View 的 measure 方法完成測量結果,關于 View 的默認測量流程前面已經做過分析,感興趣的朋友可以去分析下例如 ImageView、TextView 的測量過程。
重新回到 FrameLayout 的 onMeasure 方法,注意看在第一個 for 循環,如果當前 FrameLayout 的寬 / 高測量模式存在非 EXACTLY(即 measureMatchParentChildren == true),此時它所包含的子 View 存在 LayoutParams 為 MATCH_PARENT 時,會將該 View 記錄在 mMatchParentChilden 中。被記錄下的 View 需要二次測量確定大小。
小結
- 用一張圖再來了解下 View 的整個測量過程:
- 用一張表格總結下子 View 的測量規格:
ParentSpceMode | ParentSpceSize | ChildDimension | ChildSpecMode | ChildSpceSize |
---|---|---|---|---|
EXACTLY | Size | >= 0 | EXACTLY | ChildDimension |
同上 | 同上 | MATCH_PARENT | EXACTLY | Size |
同上 | 同上 | WRAP_CONTENT | AT_MOST | Size |
AT_MOST | 同上 | >= 0 | EXACTLY | childDimension |
同上 | 同上 | MATCH_PARENT | AT_MOST | Size |
同上 | 同上 | WRAP_CONTENT | AT_MOST | Size |
UNSPECIFIED | 同上 | >= 0 | EXACTLY | 0 |
同上 | 同上 | MATCH_PARENT | UNSPECIFIED | 0 |
同上 | 同上 | WRAP_CONTENT | UNSPECFIFE | 0 |
- 應盡可能避開在使用 FrameLayout 時發生二次測量。
確定大小的 FrameLayout,即就是保證 FrameLayout 的測量模式為 MeasureSpec.EXACTLY。
確定大小的 ChildView,或者使用 WRAP_CONTENT 。
至此 View 的測量過程就分析完了,不過測量過程涉及的細節內容非常多,感興趣的朋友可以繼續深入分析。
measure 階段實際就是確定 View 的大小,那接下來的 layout 階段就要開始擺放 View 的在容器中的位置了。
2. performLayout()
相比起 View 的測量過程,布局階段可能相對簡單一些。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
// mView是DecorView
final View host = mView;
if (host == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
// layout就是確定View的擺放位置
try {
// host為DecorView
// 由于DecorView的LayoutParams為MATCH_PARENT,故它的left和top都為0
// 獲取到DecorView的測量寬度,left + 測量寬度即 right
// 獲取到DecorView的測量高度,top + 測量高度即 bottom
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// ... 省略
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
注意 host 仍然是 DecorView,layout 方法與 measure 方法在 View 中的策略類似,不過 layout 方法并沒有被聲明為 final。
// View 的 layout
public void layout(int l, int t, int r, int b) {
// layout之前需要先進行measure測量工作
// 注意前面分析measure階段,如果當前需要測量,但是發現已經緩存了該測量結果時,measure階段
// 并沒有真正執行onMeasure,只是將mPrivateFlags3標記為PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 原來坐標位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 根據是否有光影效果
// changed標志View坐標是否發生變化
// setFrame 保存新的坐標位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 這也是做一層優化,避免無謂的遍歷layout過程
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 執行布局擺放,當前實際是 DecorView,這里實際調用了FrameLayout的onLayout
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if (mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 回調OnLayoutChange,表示當前布局坐標發生新的變化
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
changed 變量同樣是避免無謂的 layout 操作,這里重點看下 setFrame 方法(setOpticalFrame() 最終也是調用了 setFrame()):
protected boolean setFrame(int left, int top, int right, int bottom) {
// 標志View坐標是否真的發生變化
boolean changed = false;
// 判斷View的坐標信息是否發生變化
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
// 表示當前View坐標發生變化
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
// 原寬度
int oldWidth = mRight - mLeft;
// 原高度
int oldHeight = mBottom - mTop;
// 新寬度
int newWidth = right - left;
// 新高度
int newHeight = bottom - top;
// View大小是否發生變化,
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
// 保存View最后坐標位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
// View的sizeChange方法被回調
// 注意如果僅是坐標位置發生變化,View自身尺寸未發生變化sizeChange是不會被調用的
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
// ... 省略
}
return changed;
}
setFrame 方法就是為 View 賦值新的 left、right、top 和 bottom 四個坐標點,View 的坐標位置真正發生變化, changed 變量才會返回 true。
- 注意看 sizeChanged 變量,只有當 View 的寬 / 高發生變化才會回調 sizeChange 方法。
這里還需要重點關注下 View 的寬 / 高獲取,注意結合上面 View 的四個坐標點:
// 獲取View的寬度,right - left 即 View 寬度
public final int getWidth() {
return mRight - mLeft;
}
// 獲取View的高度,bottom - top 即 View 高度
public final int getHeight(){
return mBottom - mTop;
}
也就是說 View 的 getWidth() / getHeight() 是在 layout 階段完成之后,才能夠正確獲取值。
大家肯定有過這樣的疑問,如何能在 Activity 的 onCreate 方法獲取到 View 的寬高呢?要知道此時 View 的繪制流程還未開始,這里推薦 2 種思路供大家參考。
- ViewTreeObserver
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// performLayout方法執行結束之后,回調
// 此時表示所有的View都已經布局完成,可以獲取到任何View組件的寬度、高度、左邊、右邊等信息
// 需要注意多次調用帶來的影響
}
});
performLayout 方法執行完畢,此時所有的 View 已經布局完成,便會回調 onGlobalLayout 通知。
- view.post()
相信很多人都使用過該方法,并且知道任務會被添加到主線程(當前分析主線程渲染)消息隊列等待執行;但是它背后的執行原理可能大多數開發者并不一定了解,簡單來說,它保證了在 View 繪制流程結束后回調相關任務,此時我們就可以正確獲取到 View 的寬高了。具體你可以參考《Android 之你真的了解 View.post() 原理嗎?》
重新回到 DecorView 的 layout 方法,如果需要布局則調用 onLayout 方法,onLayout() 在 View 中默認為空實現,但是在 ViewGroup 將其重寫為 abstract,即強制 ViewGroup 的子類重寫該方法,因為布局容器必須實現 childView 的布局擺放任務。
DecorView 繼承自 FrameLayout,此時實際調用 FrameLayout 的 onLayout():
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 遍歷子View完成擺放
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
開始遍歷執行所有子 View 的 layout 過程如下:
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
// 獲取View數量
final int count = getChildCount();
// 獲取父View左側起始點,就是 - paddingLeft
final int parentLeft = getPaddingLeftWithForeground();
// 獲取父View的右側結束點,寬度 - paddRight
final int parentRight = right - left - getPaddingRightWithForeground();
// 獲取父View的top點
final int parentTop = getPaddingTopWithForeground();
// 獲取父View的bottom結束點
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
// 遍歷擺放所有子View
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 忽略Visibility為GONE的View,在測量階段它已經被忽略掉了
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 獲取View的測量寬度
final int width = child.getMeasuredWidth();
// 獲取View的測量高度
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
// 確定Left坐標
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
//水平居中
case Gravity.CENTER_HORIZONTAL:
// (parentRight - parentLeft - width) / 2 找到中間坐標
// parentLeft+, 表示確定啟示坐標
// 最后根據View設置的邊距
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
// 確定Top坐標
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
// 確定了ChildView的left,和top
// left + width 即 right
// top + height 即使 bottom
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
可以看到在 layout 階段默認也會忽略 View.GONE。實際上 layout 過程就是確定 View 四個點的坐標信息,計算出 left 點坐標后,left + View 測量寬度即 right 點坐標,同理計算出 top 點坐標后,top + View 測量高度即 bottom 點坐標。
至此 View 繪制流程的測量和布局兩大階段就已經分析完了,不過你是否能夠依照前面的分析,自己實現一個流式布局呢?
另外,大家是否有思考過 ScollView 里面嵌套 ListView,ListView 為什么只能顯示第一行的高度?感興趣的朋友可以去分析下它們的測量過程。
3. performDraw()
private void performDraw() {
// 屏幕是否已經關閉
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
// DecorView == null
return;
}
// 是否需要全部重繪
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
// 正在繪制標記
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
// 調用draw方法
draw(fullRedrawNeeded);
} finally {
// 繪制完成修改標志位
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
執行繪制任務 draw 方法如下:
private void draw(boolean fullRedrawNeeded) {
// 窗口都關聯有一個 Surface
// 在 Android 中,所有的元素都在 Surface 這張畫紙上進行繪制和渲染,
// 普通 View(例如非 SurfaceView 或 TextureView) 是沒有 Surface 的,
// 一般 Activity 包含多個 View 形成 View Hierachy 的樹形結構,只有最頂層的 DecorView 才是對 WindowManagerService “可見的”。
// 而為普通 View 提供 Surface 的正是 ViewRootImpl。
Surface surface = mSurface;
if (!surface.isValid()) {
// Surface 是否還有效
return;
}
// 跟蹤FPS
if (DEBUG_FPS) {
trackFPS();
}
// ... 省略
// View 滑動通知
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
// getViewTreeObserver().addOnScrollChangedListener()
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
// ... 省略
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
// 通知View開始繪制
// getViewTreeObserver().addOnDrawListener();
mAttachInfo.mTreeObserver.dispatchOnDraw();
// ... 省略
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
// 開啟硬件加速繪制執行這里,最終還是執行View的draw開始
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
// 最終調用到drawSoftware
// surface,每個 View 都由某一個窗口管理,而每一個窗口都關聯有一個 Surface
// mDirty.set(0, 0, mWidth, mHeight); dirty 表示畫紙尺寸,對于DecorView,left = 0,
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
}
Surface,前面文章也有多次提到,在 Android 中,Window 是 View 的容器,每個窗口都會關聯一個 Surface,為窗口提供 Surface 的正是 ViewRootImpl。
- Surface。每個 View 都由某一個窗口管理,而每一個窗口都關聯有一個 Surface。
在 Android 3.0 之前,或者沒有啟用硬件加速時,系統都會使用軟件方式來渲染 UI。軟件繪制需要依賴 CPU,不過 CPU 對于圖形處理并不是那么高效,這個過程完全沒有利用到 GPU 的高性能。
所以從 Android 3.0 開始,Android 開始支持硬件加速,直到 Android 4.0 時,才默認開啟硬件加速。
雖然硬件加速繪制與軟件繪制整個流程差異非常大,但是在 View 層繪制邏輯是一樣的,這里僅以軟件繪制流程為例:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
final Canvas canvas;
try {
// 繪制區域矩形
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
// Canvas 實際代表某塊繪制區域在Sruface
// Canvas 可以簡單理解為 Skia 底層接口的封裝
canvas = mSurface.lockCanvas(dirty);
// The dirty rectangle can be modified by Surface.lockCanvas()
if (left != dirty.left || top != dirty.top || right != dirty.right
|| bottom != dirty.bottom) {
// 需要繪制的矩形區域有變化
attachInfo.mIgnoreDirtyState = true;
}
// 設置像素密度
// mDensity = context.getResources().getDisplayMetrics().densityDpi;
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
}
try {
// ... 省略
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
// mView實際類型是DecorView
// draw調用到View中
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
if (!attachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
attachInfo.mIgnoreDirtyState = false;
}
}
} finally {
try {
surface.unlockCanvasAndPost(canvas);
} catch (IllegalArgumentException e) {
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
}
}
return true;
}
注意 Canvas 的獲取,通過 Surface 的 lock 方法獲得一個 Canvas,Canvas 可以簡單理解為 Skia 底層接口的封裝。
Canvas 作為參數,調用 DecorView 的 draw 方法,實際調用其父類 View 的 draw()。
關于繪制流程的三個階段在 View 源碼中都提供了默認的規則 measure()、layout() 和 draw(),只不過 Android 強制將 measure() 聲明為 final。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, 如果需要,繪制背景
int saveCount;
// 背景不是透明的
if (!dirtyOpaque) {
// 繪制View的背景
// 重復設置背景色,會導致過度繪制
// 避免在布局容器重復設置背景
drawBackground(canvas);
}
// 如果可能,跳過第2步和第5步(常見情況)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 繪制View視圖內容
// 通常情況下自定義 ViewGroup 不會回調onDraw
if (!dirtyOpaque) onDraw(canvas);
// Step 4, 繪制子視圖
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
// 繪制浮動View視圖
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, 繪制裝飾(前景,滾動條)
onDrawForeground(canvas);
// Step 7, 繪制默認的焦點突出顯示
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
}
Google 工程師非常貼心,將繪制階段的任務和步驟做了詳細介紹。
- 繪制 Vew 背景
- 如果需要,保存畫布的圖層以備褪色
- 繪制視圖內容
- 分發繪制子視圖
- 如果需要,畫出漸退的邊緣并恢復圖層
- 繪制裝飾(例如,滾動條)
繪制背景,就是繪制通過 setBackground 設置的 Drawable,關于背景設置,我們要避免重復設置,這會帶來過渡繪制的問題。比如被完全遮蓋的布局容器是沒有必要為其設置背景的。
通常情況下,在定義 ViewGroup 時不會回調 onDraw 方法,這取決于是否設置了背景。
dispatchDraw 方法主要分發給 childView 進行繪制任務,在自定義 ViewGroup 實現繪制邏輯時一般會重寫 dispatchDraw() 而不是 onDraw()。
在開發過程中,大家是否有注意 LinearLayout、FrameLayout 和 RelativeLayout 的渲染性能更好?其實三者在 layout、draw 的耗時相差不大,性能差異主要體現在 measure 階段。LinearLayout 只會測量一次,水平或垂直方向,但需要注意 weight 的問題;FrameLayout 如果使用正確也會測量一次;而 RelativeLayout 要測量多次來確定水平和垂直方向的關聯關系,但在扁平化布局更具有優勢,這就需要在根據業務場景選擇更優的布局容器。
- ps:現在 Google 更加推薦使用 ConstraintLayout,感興趣的朋友可以深入了解下。
Android 的整個 UI 渲染框架的設計是非常龐大和復雜的,經過三篇文章介紹 View 繪制流程其實也僅僅是涉及皮毛而已,如果需要更深入了解這塊內容,還需要不斷地學習和查閱相關資料。
UI 渲染這一塊也是 Google 長期以來非常重視的,基本每次 Google I/O 都會花很多篇幅講這一塊。為了彌補跟 iOS 的差距,在每個版本都做了大量的優化。在后面文章也會聊一聊 Android 渲染的演進,一起來看下 Google 工程師都做了哪些努力!
至此 View 繪制流程就已經分析完了,正如文中所講這只不過是整個渲染框架的皮毛而已,感興趣的朋友可以繼續深入研究學習。文中如有不妥或有更好的分析結果,歡迎您的指出。
文章如果對你有幫助,請留個贊吧!
推薦閱讀
其他系列專題