轉(zhuǎn)載于:請(qǐng)叫我大蘇的 Android屏幕刷新機(jī)制
我主要的目的是跟著文章的思路從新走一遍稽揭,讓自己更好的理解相關(guān)的知識(shí)點(diǎn)翔忽。建議該文章最好能讀三遍呼渣,第一遍快速閱讀蔑祟,了解主要講解了什么內(nèi)容,第二遍細(xì)讀忌傻,了解實(shí)現(xiàn)的細(xì)節(jié)大脉,第三遍結(jié)合文章在源碼上了解相關(guān)的細(xì)節(jié)及流程。
注:
本文中會(huì)將原文中圖片替換成源碼水孩,額外添加一些注解與圖片镰矿。
提問環(huán)節(jié)
閱讀源碼還是得帶著問題或目的性的去閱讀,這樣閱讀過程中比較有條理性俘种,不會(huì)跟偏或太深入秤标,所以,還是先來幾個(gè)問題吧:
大伙都清楚宙刘,Android 每隔 16.6ms 會(huì)刷新一次屏幕苍姜。
Q1:但是大伙想過沒有,這個(gè) 16.6ms 刷新一次屏幕到底是什么意思呢荐类?是指每隔 16.6ms 調(diào)用 onDraw() 繪制一次么怖现?
Q2:如果界面一直保持沒變的話茁帽,那么還會(huì)每隔 16.6ms 刷新一次屏幕么玉罐?
Q3:界面的顯示其實(shí)就是一個(gè) Activity 的 View 樹里所有的 View 都進(jìn)行測(cè)量、布局潘拨、繪制操作之后的結(jié)果呈現(xiàn),那么如果這部分工作都完成后,屏幕會(huì)馬上就刷新么娄帖?
Q4:網(wǎng)上都說避免丟幀的方法之一是保證每次繪制界面的操作要在 16.6ms 內(nèi)完成勿决,但如果這個(gè) 16.6ms 是一個(gè)固定的頻率的話,請(qǐng)求繪制的操作在代碼里被調(diào)用的時(shí)機(jī)是不確定的啊琅束,那么如果某次用戶點(diǎn)擊屏幕導(dǎo)致的界面刷新操作是在某一個(gè) 16.6ms 幀快結(jié)束的時(shí)候扭屁,那么即使這次繪制操作小于 16.6 ms,按道理不也會(huì)造成丟幀么涩禀?這又該如何理解料滥?
Q5:大伙都清楚,主線程耗時(shí)的操作會(huì)導(dǎo)致丟幀艾船,但是耗時(shí)的操作為什么會(huì)導(dǎo)致丟幀葵腹?它是如何導(dǎo)致丟幀發(fā)生的高每?
本篇主要就是搞清楚這幾個(gè)問題,分析的源碼基本只涉及 ViewRootImpl 和 Choreographer 這兩個(gè)類践宴。
源碼分析
ps:本篇分析的源碼均是 android-25 版本鲸匿,版本不一樣,源碼可能會(huì)有些許差異阻肩,大伙過的時(shí)候注意一下带欢。
基本概念
首先,先來過一下一些基本概念磺浙,摘抄自網(wǎng)上文章android屏幕刷新顯示機(jī)制:
在一個(gè)典型的顯示系統(tǒng)中洪囤,一般包括CPU、GPU撕氧、display三個(gè)部分瘤缩, CPU負(fù)責(zé)計(jì)算數(shù)據(jù),把計(jì)算好數(shù)據(jù)交給GPU,GPU會(huì)對(duì)圖形數(shù)據(jù)進(jìn)行渲染伦泥,渲染好后放到buffer里存起來剥啤,然后display(有的文章也叫屏幕或者顯示器)負(fù)責(zé)把buffer里的數(shù)據(jù)呈現(xiàn)到屏幕上。
顯示過程不脯,簡單的說就是CPU/GPU準(zhǔn)備好數(shù)據(jù)府怯,存入buffer,display每隔一段時(shí)間去buffer里取數(shù)據(jù)防楷,然后顯示出來牺丙。display讀取的頻率是固定的,比如每個(gè)16ms讀一次复局,但是CPU/GPU寫數(shù)據(jù)是完全無規(guī)律的冲簿。
上述內(nèi)容概括一下,大體意思就是說亿昏,屏幕的刷新包括三個(gè)步驟:CPU 計(jì)算屏幕數(shù)據(jù)峦剔、GPU 進(jìn)一步處理和緩存、最后 display 再將緩存中(buffer)的屏幕數(shù)據(jù)顯示出來角钩。
(ps:開發(fā)過程中應(yīng)該接觸不到 GPU吝沫、display 這些層面的東西,所以我把這部分工作都稱作底層的工作了递礼,下文出現(xiàn)的底層指的就是除了 CPU 計(jì)算屏幕數(shù)據(jù)之外的工作惨险。)
對(duì)于 Android 而言,第一個(gè)步驟:CPU 計(jì)算屏幕數(shù)據(jù)指的就是 View 樹的繪制過程脊髓,也就是 Activity 對(duì)應(yīng)的視圖樹從根布局 DecorView 開始層層遍歷每個(gè) View辫愉,分別執(zhí)行測(cè)量、布局供炼、繪制三個(gè)操作的過程一屋。
也就是說窘疮,我們常說的 Android 每隔 16.6ms 刷新一次屏幕其實(shí)是指:底層以固定的頻率,比如每 16.6ms 將 buffer 里的屏幕數(shù)據(jù)顯示出來冀墨。
如果還不清楚闸衫,那再看一張網(wǎng)上很常見的圖(摘自上面同一篇文章):
結(jié)合這張圖,再來講講 16.6 ms 屏幕刷新一次的意思诽嘉。
Display 這一行可以理解成屏幕蔚出,所以可以看到,底層是以固定的頻率發(fā)出 VSync 信號(hào)的虫腋,而這個(gè)固定頻率就是我們常說的每 16.6ms 發(fā)送一個(gè) VSync 信號(hào)骄酗,至于什么叫 VSync 信號(hào),我們可以不用深入去了解悦冀,只要清楚這個(gè)信號(hào)就是屏幕刷新的信號(hào)就可以了趋翻。
繼續(xù)看圖,Display 黃色的這一行里有一些數(shù)字:0, 1, 2, 3, 4
盒蟆,可以看到每次屏幕刷新信號(hào)到了的時(shí)候踏烙,數(shù)字就會(huì)變化,所以這些數(shù)字其實(shí)可以理解成每一幀屏幕顯示的畫面历等。也就是說讨惩,屏幕每一幀的畫面可以持續(xù) 16.6ms,當(dāng)過了 16.6ms寒屯,底層就會(huì)發(fā)出一個(gè)屏幕刷新信號(hào)荐捻,而屏幕就會(huì)去顯示下一幀的畫面。
以上都是一些基本概念寡夹,也都是底層的工作处面,我們了解一下就可以了。接下去就還是看這圖要出,然后講講我們 app 層該干的事了:
繼續(xù)看圖鸳君,CPU 藍(lán)色的這行农渊,上面也說過了患蹂,CPU 這塊的耗時(shí)其實(shí)就是我們 app 繪制當(dāng)前 View 樹的時(shí)間,而這段時(shí)間就跟我們自己寫的代碼有關(guān)系了砸紊,如果你的布局很復(fù)雜传于,層次嵌套很多,每一幀內(nèi)需要刷新的 View 又很多時(shí)醉顽,那么每一幀的繪制耗時(shí)自然就會(huì)多一點(diǎn)沼溜。
繼續(xù)看圖,CPU 藍(lán)色這行里也有一些數(shù)字游添,其實(shí)這些數(shù)字跟 Display 黃色的那一行里的數(shù)字是對(duì)應(yīng)的系草,在 Display 里我們解釋過這些數(shù)字表示的是每一幀的畫面通熄,那么在 CPU 這一行里,其實(shí)就是在計(jì)算對(duì)應(yīng)幀的畫面數(shù)據(jù)找都,也叫屏幕數(shù)據(jù)唇辨。也就是說,在當(dāng)前幀內(nèi)能耻,CPU 是在計(jì)算下一幀的屏幕畫面數(shù)據(jù)赏枚,當(dāng)屏幕刷新信號(hào)到的時(shí)候,屏幕就去將 CPU 計(jì)算的屏幕畫面數(shù)據(jù)顯示出來晓猛;同時(shí) CPU 也接收到屏幕刷新信號(hào)饿幅,所以也開始去計(jì)算下一幀的屏幕畫面數(shù)據(jù)。
CPU 跟 Display 是不同的硬件戒职,它們是可以并行工作的栗恩。要理解的一點(diǎn)是,我們寫的代碼洪燥,只是控制讓 CPU 在接收到屏幕刷新信號(hào)的時(shí)候開始去計(jì)算下一幀的畫面工作摄凡。而底層在每一次屏幕刷新信號(hào)來的時(shí)候都會(huì)去切換這一幀的畫面,這點(diǎn)我們是控制不了的蚓曼,是底層的工作機(jī)制亲澡。之所以要講這點(diǎn),是因?yàn)槿野妫?dāng)我們的 app 界面沒有必要再刷新時(shí)(比如用戶不操作了床绪,當(dāng)前界面也沒動(dòng)畫),這個(gè)時(shí)候其弊,我們 app 是接收不到屏幕刷新信號(hào)的癞己,所以也就不會(huì)讓 CPU 去計(jì)算下一幀畫面數(shù)據(jù),但是底層仍然會(huì)以固定的頻率來切換每一幀的畫面梭伐,只是它后面切換的每一幀畫面都一樣痹雅,所以給我們的感覺就是屏幕沒刷新。
所以糊识,我覺得上面那張圖還可以再繼續(xù)延深幾幀的長度绩社,這樣就更容易理解了:
我在那張圖的基礎(chǔ)上延長了幾幀,我想這樣應(yīng)該可以更容易理解點(diǎn)赂苗。
看我畫的這張圖愉耙,前三幀跟原圖一樣,從第三幀之后拌滋,因?yàn)槲覀兊?app 界面不需要刷新了(用戶不操作了朴沿,界面也沒有動(dòng)畫),那么這之后我們 app 就不會(huì)再接收到屏幕刷新信號(hào)了,所以也就不會(huì)再讓 CPU 去繪制視圖樹來計(jì)算下一幀畫面了赌渣。但是魏铅,底層還是會(huì)每隔 16.6ms 發(fā)出一個(gè)屏幕刷新信號(hào),只是我們 app 不會(huì)接收到而已坚芜,Display 還是會(huì)在每一個(gè)屏幕刷新信號(hào)到的時(shí)候去顯示下一幀畫面沦零,只是下一幀畫面一直是第4幀的內(nèi)容而已。
結(jié)論梳理
到這里 Q1货岭,Q2路操,Q3 都可以先回答一半了,那么我們就先稍微來梳理一下:
我們常說的 Android 每隔 16.6 ms 刷新一次屏幕其實(shí)是指底層會(huì)以這個(gè)固定頻率來切換每一幀的畫面千贯。
這個(gè)每一幀的畫面也就是我們的 app 繪制視圖樹(View 樹)計(jì)算而來的屯仗,這個(gè)工作是交由 CPU 處理,耗時(shí)的長短取決于我們寫的代碼:布局復(fù)不復(fù)雜搔谴,層次深不深魁袜,同一幀內(nèi)刷新的 View 的數(shù)量多不多。
CPU 繪制視圖樹來計(jì)算下一幀畫面數(shù)據(jù)的工作是在屏幕刷新信號(hào)來的時(shí)候才開始工作的敦第,而當(dāng)這個(gè)工作處理完畢后峰弹,也就是下一幀的畫面數(shù)據(jù)已經(jīng)全部計(jì)算完畢,也不會(huì)馬上顯示到屏幕上芜果,而是會(huì)等下一個(gè)屏幕刷新信號(hào)來的時(shí)候再交由底層將計(jì)算完畢的屏幕畫面數(shù)據(jù)顯示出來鞠呈。
當(dāng)我們的 app 界面不需要刷新時(shí)(用戶無操作,界面無動(dòng)畫)右钾,app 就接收不到屏幕刷新信號(hào)所以也就不會(huì)讓 CPU 再去繪制視圖樹計(jì)算畫面數(shù)據(jù)工作蚁吝,但是底層仍然會(huì)每隔 16.6 ms 切換下一幀的畫面,只是這個(gè)下一幀畫面一直是相同的內(nèi)容舀射。
這部分雖然說是一些基本概念窘茁,但其實(shí)也包含了一些結(jié)論了,所以可能大伙看著會(huì)有些困惑:
為什么界面不刷新時(shí)app就接收不到屏幕刷新信號(hào)了脆烟?為什么繪制視圖樹計(jì)算下一幀畫面的工作會(huì)是在屏幕刷新信號(hào)來的時(shí)候才開始的山林?
等等。
emmm邢羔,有這些困惑很棒驼抹,這樣,我們下面一起通過源碼去尋找答案時(shí)张抄,大伙就更有目的性了砂蔽,這樣過源碼我覺得效率是比較高一點(diǎn)的洼怔。
ViewRootImpl 與 DecorView 的綁定
閱讀源碼從哪開始看起一直都是個(gè)頭疼的問題署惯,所以找一個(gè)合適的切入點(diǎn)來跟的話,整個(gè)梳理的過程可能會(huì)順暢一點(diǎn)镣隶。本篇是研究屏幕的刷新极谊,那么建議就是從某個(gè)會(huì)導(dǎo)致屏幕刷新的方法入手诡右,比如 View#invalidate()
。下面是關(guān)于invalidate()方法的一些分析轻猖,會(huì)比較長帆吻,不感興趣的可以直接跳到結(jié)論部分繼續(xù)往下看;
View#invalidate()分析
` 是請(qǐng)求重繪的一個(gè)操作咙边,所以我們切入點(diǎn)可以從這個(gè)方法開始一步步跟下去猜煮。
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View is contents or
* dimensions have not changed.
*/
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}
// 這里判斷該子View是否可見或者是否處于動(dòng)畫中
if (skipInvalidate()) {
return;
}
// 根據(jù)View的標(biāo)記位來判斷該子View是否需要重繪,
// 假如View沒有任何變化,那么就不需要重繪
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
//把需要重繪的區(qū)域傳遞給父容器
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
//調(diào)用父容器的方法败许,向上傳遞事件
' p.invalidateChild(this, damage);'
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
// Damage the entire IsolatedZVolume receiving this view is shadow.
if (isHardwareAccelerated() && getZ() != 0) {
damageShadowReceiver();
}
}
}
接著我們看ViewGroup#invalidateChild:
/**
* Do not call or override this method. It is used for the implementation of
* the view hierarchy.
*/
@Override
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// If the child is drawing an animation, we want to copy this flag onto
// ourselves and the parent to make sure the invalidate request goes
// through
final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
== PFLAG_DRAW_ANIMATION;
// Check whether the child that requests the invalidate is fully opaque
// Views being animated or transformed are not considered opaque because we may
// be invalidating their old position and need the parent to paint behind them.
Matrix childMatrix = child.getMatrix();
final boolean isOpaque = child.isOpaque() && !drawAnimation &&
child.getAnimation() == null && childMatrix.isIdentity();
// Mark the child as dirty, using the appropriate flag
// Make sure we do not set both flags at the same time
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
if (child.mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
//儲(chǔ)存子View的mLeft和mTop值
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
if (!childMatrix.isIdentity() ||
(mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
......
}
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
//調(diào)用ViewGrup的invalidateChildInParent
// 如果已經(jīng)達(dá)到最頂層view,則調(diào)用ViewRootImpl的invalidateChildInParent王带。
'parent = parent.invalidateChildInParent(location, dirty);'
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
}
可以看到,在該方法內(nèi)部有一個(gè)do while循環(huán)市殷,該循環(huán)主要的作用是不斷向上回溯父容器愕撰,求得父容器和子View需要重繪的區(qū)域的并集(dirty)。
接下來我們來看看這個(gè)方法醋寝,ViewGroup#invalidateChildInParent:
/**
* Do not call or override this method. It is used for the implementation of
* the view hierarchy.
*
* This implementation returns null if this ViewGroup does not have a parent,
* if this ViewGroup is already fully invalidated or if the dirty rectangle
* does not intersect with this ViewGroup is bounds.
*/
@Override
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
FLAG_OPTIMIZE_INVALIDATE) {
//將dirty中的坐標(biāo)轉(zhuǎn)化為父容器中的坐標(biāo)搞挣,考慮mScrollX和mScrollY的影響
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
//求并集,結(jié)果是把子視圖的dirty區(qū)域轉(zhuǎn)化為父容器的dirty區(qū)域
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
} else {
mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;
//記錄當(dāng)前視圖的mLeft和mTop值音羞,在下一次循環(huán)中會(huì)把當(dāng)前值再向父容器的坐標(biāo)轉(zhuǎn)化
location[CHILD_LEFT_INDEX] = mLeft;
location[CHILD_TOP_INDEX] = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
} else {
// in case the dirty rect extends outside the bounds of this container
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
}
}
return null;
}
可以看出囱桨,這個(gè)方法做的工作主要有:調(diào)用offset方法,把當(dāng)前dirty區(qū)域的坐標(biāo)轉(zhuǎn)化為父容器中的坐標(biāo)嗅绰,接著調(diào)用union方法蝇摸,把子dirty區(qū)域與父容器的區(qū)域求并集,換句話說办陷,dirty區(qū)域變成父容器區(qū)域貌夕。最后返回當(dāng)前視圖的父容器,以便進(jìn)行下一次循環(huán)民镜。
回到上面所說的do...while...循環(huán)啡专,由于不斷向上調(diào)用父容器的方法,到最后會(huì)調(diào)用到ViewRootImpl的invalidateChildInParent方法制圈,我們來看看它的源碼们童,ViewRootImpl#invalidateChildInParent:
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
if (mCurScrollY != 0 || mTranslator != null) {
mTempRect.set(dirty);
dirty = mTempRect;
if (mCurScrollY != 0) {
dirty.offset(0, -mCurScrollY);
}
if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(dirty);
}
if (mAttachInfo.mScalingRequired) {
dirty.inset(-1, -1);
}
}
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}
// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
// Intersect with the bounds of the window to skip
// updates that lie outside of the visible region
final float appScale = mAttachInfo.mApplicationScale;
final boolean intersected = localDirty.intersect(0, 0,
(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
if (!intersected) {
localDirty.setEmpty();
}
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
'scheduleTraversals();'
}
}
可以看出,該方法所做的工作與上面的差不多鲸鹦,都進(jìn)行了offset和union對(duì)坐標(biāo)的調(diào)整慧库,然后把dirty區(qū)域的信息保存在mDirty中,最后調(diào)用了scheduleTraversals方法馋嗜,觸發(fā)View的工作流程.
結(jié)論:
我們跟著 invalidate()
一步步往下走的時(shí)候齐板,發(fā)現(xiàn)最后跟到了 ViewRootImpl#scheduleTraversals()
就停止了。而 ViewRootImpl 就是今天我們要介紹的重點(diǎn)對(duì)象了。
DevorView 和 ViewRootImpl 綁定時(shí)機(jī)
大伙都清楚甘磨,Android 設(shè)備呈現(xiàn)到界面上的大多數(shù)情況下都是一個(gè) Activity橡羞,真正承載視圖的是一個(gè) Window,每個(gè) Window 都有一個(gè) DecorView济舆,我們調(diào)用 setContentView()
其實(shí)是將我們自己寫的布局文件添加到以 DecorView 為根布局的一個(gè) ViewGroup 里卿泽,構(gòu)成一顆 View 樹。
這些大伙都清楚滋觉,每個(gè) Activity 對(duì)應(yīng)一顆以 DecorView 為根布局的 View 樹签夭,但其實(shí) DecorView 還有 mParent,而且就是 ViewRootImpl椎侠,而且每個(gè)界面上的 View 的刷新覆致,繪制,點(diǎn)擊事件的分發(fā)其實(shí)都是由 ViewRootImpl 作為發(fā)起者的肺蔚,由 ViewRootImpl 控制這些操作從 DecorView 開始遍歷 View 樹去分發(fā)處理煌妈。
在上一篇?jiǎng)赢嫹治龅牟┛屠铮治?View#invalidate()
時(shí),也可以看到內(nèi)部其實(shí)是有一個(gè) do{}while() 循環(huán)來不斷尋找 mParent,所以最終才會(huì)走到 ViewRootImpl 里去嚎货,那么可能大伙就會(huì)疑問了关摇,為什么 DecorView 的 mParent 會(huì)是 ViewRootImpl 呢?換個(gè)問法也就是,在什么時(shí)候?qū)?DevorView 和 ViewRootImpl 綁定起來?
Activity 的啟動(dòng)是在 ActivityThread 里完成的,handleLaunchActivity()
會(huì)依次間接的執(zhí)行到 Activity 的 onCreate()
, onStart()
, onResume()
比被。在執(zhí)行完這些后 ActivityThread 會(huì)調(diào)用 WindowManager#addView()
,而這個(gè) addView()
最終其實(shí)是調(diào)用了 WindowManagerGlobal 的 addView()
方法泼舱,我們就從這里開始看:
// 這里傳的 view 實(shí)際為DecorView
263 public void addView(View view, ViewGroup.LayoutParams params,
264 Display display, Window parentWindow) {
265 if (view == null) {
266 throw new IllegalArgumentException("view must not be null");
267 }
268 if (display == null) {
269 throw new IllegalArgumentException("display must not be null");
270 }
271 if (!(params instanceof WindowManager.LayoutParams)) {
272 throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
273 }
274
275 ......
288
289 'ViewRootImpl root;'
290 View panelParentView = null;
291
......
330
331 root = new ViewRootImpl(view.getContext(), display);
332
333 view.setLayoutParams(wparams);
334
335 mViews.add(view);
336 'mRoots.add(root);'
337 mParams.add(wparams);
338 }
339
340 // do this last because it fires off messages to start doing things
341 try {
342 root.setView(view, wparams, panelParentView);
343 } catch (RuntimeException e) {
344 // BadTokenException or InvalidDisplayException, clean up.
345 synchronized (mLock) {
346 final int index = findViewLocked(view, false);
347 if (index >= 0) {
348 removeViewLocked(index, true);
349 }
350 }
351 throw e;
352 }
353 }
WindowManager 維護(hù)著所有 Activity 的 DecorView 和 ViewRootImpl等缀。這里初始化了一個(gè) ViewRootImpl,然后調(diào)用了它的 setView()
方法娇昙,將 DevorView 作為參數(shù)傳遞了進(jìn)去尺迂。所以看看 ViewRootImpl 中的 setView()
做了什么:
/**
* We have one child
*/
// 這里傳的 view 實(shí)際為DecorView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
......
// 發(fā)起布局請(qǐng)求
requestLayout();
......
// this對(duì)象為ViewRootImpl對(duì)象
view.assignParent(this);
......
}
}
}
在 setView()
方法里調(diào)用了 DecorView 的 assignParent()
方法,所以去看看 View 的這個(gè)方法:
/*
* Caller is responsible for calling requestLayout if necessary.
* (This allows addViewInLayout to not request a new layout.)
*/
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
參數(shù)是 ViewParent冒掌,而 ViewRootImpl 是實(shí)現(xiàn)了 ViewParent 接口的噪裕,所以在這里就將 DecorView 和 ViewRootImpl 綁定起來了。每個(gè)Activity 的根布局都是 DecorView股毫,而 DecorView 的 parent 又是 ViewRootImpl膳音,所以在子 View 里執(zhí)行 invalidate()
之類的操作,循環(huán)找 parent 時(shí)铃诬,最后都會(huì)走到 ViewRootImpl 里來祭陷。
跟界面刷新相關(guān)的方法里應(yīng)該都會(huì)有一個(gè)循環(huán)找 parent 的方法苍凛,或者是不斷調(diào)用 parent 的方法,這樣最終才都會(huì)走到 ViewRootImpl 里颗胡,也就是說實(shí)際上 View 的刷新都是由 ViewRootImpl 來控制的毫深。
即使是界面上一個(gè)小小的 View 發(fā)起了重繪請(qǐng)求時(shí)吩坝,都要層層走到 ViewRootImpl毒姨,由它來發(fā)起重繪請(qǐng)求,然后再由它來開始遍歷 View 樹钉寝,一直遍歷到這個(gè)需要重繪的 View 再調(diào)用它的 onDraw()
方法進(jìn)行繪制弧呐。
我們重新看回 ViewRootImpl 的 setView()
這個(gè)方法,這個(gè)方法里還調(diào)用了一個(gè) requestLayout()
方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
這里調(diào)用了一個(gè) scheduleTraversals()
嵌纲,還記得當(dāng) View 發(fā)起重繪操作 invalidate()
時(shí)俘枫,最后也調(diào)用了 scheduleTraversals()
這個(gè)方法么。其實(shí)這個(gè)方法就是屏幕刷新的關(guān)鍵逮走,它是安排一次繪制 View 樹的任務(wù)等待執(zhí)行鸠蚪,具體后面再說。
也就是說师溅,其實(shí)打開一個(gè) Activity茅信,當(dāng)它的 onCreate---onResume 生命周期都走完后,才將它的 DecoView 與新建的一個(gè) ViewRootImpl 對(duì)象綁定起來墓臭,同時(shí)開始安排一次遍歷 View 任務(wù)也就是繪制 View 樹的操作等待執(zhí)行蘸鲸,然后將 DecoView 的 parent 設(shè)置成 ViewRootImpl 對(duì)象.
這也就是為什么在 onCreate---onResume
里獲取不到 View 寬高的原因,因?yàn)樵谶@個(gè)時(shí)刻 ViewRootImpl 甚至都還沒創(chuàng)建窿锉,更不用說是否已經(jīng)執(zhí)行過測(cè)量操作了酌摇。
還可以得到一點(diǎn)信息是,一個(gè) Activity 界面的繪制嗡载,其實(shí)是在 onResume()
之后才開始的窑多。
ViewRootImpl#scheduleTraversals
到這里,我們梳理清楚了洼滚,調(diào)用一個(gè) View 的 invalidate()
請(qǐng)求重繪操作怯伊,內(nèi)部原來是要層層通知到 ViewRootImpl 的 scheduleTraversals()
里去。而且打開一個(gè)新的 Activity判沟,它的界面繪制原來是在 onResume()
之后也層層通知到 ViewRootImpl 的 scheduleTraversals()
里去耿芹。雖然其他關(guān)于 View 的刷新操作,比如 requestLayout()
等等之類的方法我們還沒有去看挪哄,但我們已經(jīng)可以大膽猜測(cè)吧秕,這些跟 View 刷新有關(guān)的操作最終也都會(huì)層層走到 ViewRootImpl 中的 scheduleTraversals()
方法里去的。
那么這個(gè)方法究竟干了些什么迹炼,我們就要好好來分析了:
void scheduleTraversals() {
// 過濾一幀內(nèi)重復(fù)的刷新請(qǐng)求
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
mTraversalScheduled 這個(gè) boolean 變量的作用等會(huì)再來看砸彬,先看看 mChoreographer.postCallback()
這個(gè)方法颠毙,傳入了三個(gè)參數(shù),第二個(gè)參數(shù)是一個(gè) Runnable 對(duì)象砂碉,先來看看這個(gè) Runnable:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
這個(gè) Runnable 做的事很簡單蛀蜜,就調(diào)用了一個(gè)方法,doTraversal()
:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
看看這個(gè)方法做的事增蹭,跟 scheduleTraversals()
正好相反滴某,一個(gè)將變量置成 true,這里置成 false滋迈,一個(gè)是 postSyncBarrier()
霎奢,這里是 removeSyncBarrier()
,具體作用等會(huì)再說饼灿,繼續(xù)先看看 performTraversals()
幕侠,這個(gè)方法也是屏幕刷新的關(guān)鍵:
private void performTraversals() {
......
if (......) {
......
'performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);'
}
......
if (didLayout) {
'performLayout(lp, mWidth, mHeight);'
.....
}
......
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
'performDraw();'
}
}
View 的測(cè)量、布局碍彭、繪制三大流程都是交由 ViewRootImpl 發(fā)起晤硕,而且還都是在 performTraversals()
方法中發(fā)起的,所以這個(gè)方法的邏輯很復(fù)雜庇忌,因?yàn)槊看味夹枰鶕?jù)相應(yīng)狀態(tài)判斷是否需要三個(gè)流程都走舞箍,有時(shí)可能只需要執(zhí)行 performDraw()
繪制流程,有時(shí)可能只執(zhí)行 performMeasure()
測(cè)量和 performLayout()
布局流程(一般測(cè)量和布局流程是一起執(zhí)行的)漆枚。不管哪個(gè)流程都會(huì)遍歷一次 View 樹创译,所以其實(shí)界面的繪制是需要遍歷很多次的,如果頁面層次太過復(fù)雜墙基,每一幀需要刷新的 View 又很多時(shí)软族,耗時(shí)就會(huì)長一點(diǎn)。
當(dāng)然残制,測(cè)量立砸、布局、繪制這些流程在遍歷時(shí)并不一定會(huì)把整顆 View 樹都遍歷一遍初茶,ViewGroup 在傳遞這些流程時(shí)颗祝,還會(huì)再根據(jù)相應(yīng)狀態(tài)判斷是否需要繼續(xù)往下傳遞。
了解了 performTraversals()
是刷新界面的源頭后恼布,接下去就需要了解下它是什么時(shí)候執(zhí)行的螺戳,和 scheduleTraversals()
又是什么關(guān)系?
performTraversals()
是在 doTraversal()
中被調(diào)用的折汞,而 doTraversal()
又被封裝到一個(gè) Runnable 里倔幼,那么關(guān)鍵就是這個(gè) Runnable 什么時(shí)候被執(zhí)行了?
Choreographer
scheduleTraversals()
里調(diào)用了 Choreographer 的 postCallback()
將 Runnable 作為參數(shù)傳了進(jìn)去爽待,所以跟進(jìn)去看看:
/**
* Posts a callback to run on the next frame.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @param callbackType The callback type.
* @param action The callback action to run during the next frame.
* @param token The callback token, or null if none.
*
* @see #removeCallbacks
* @hide
*/
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
/**
* Posts a callback to run on the next frame after the specified delay.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @param callbackType The callback type.
* @param action The callback action to run during the next frame after the specified delay.
* @param token The callback token, or null if none.
* @param delayMillis The delay time in milliseconds.
*
* @see #removeCallback
* @hide
*/
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
}
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
// delayMillis傳入值為0损同,所有會(huì)走到這里
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
因?yàn)?postCallback()
調(diào)用 postCallbackDelayed()
時(shí)傳了 delay = 0 進(jìn)去翩腐,所以在 postCallbackDelayedInternal()
里面會(huì)先根據(jù)當(dāng)前時(shí)間戳將這個(gè) Runnable 保存到一個(gè) mCallbackQueue 隊(duì)列里,這個(gè)隊(duì)列跟 MessageQueue 很相似膏燃,里面待執(zhí)行的任務(wù)都是根據(jù)一個(gè)時(shí)間戳來排序茂卦。然后走了 scheduleFrameLocked()
方法這邊,看看做了些什么:
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
// USE_VSYNC 默認(rèn)為true
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}
// If running on the Looper thread, then schedule the vsync immediately,
// otherwise post a message to schedule the vsync from the UI thread
// as soon as possible.
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
我們能看到當(dāng)USE_VSYNC為true時(shí)组哩,如果是運(yùn)行在UI線程中等龙,會(huì)馬上進(jìn)行vsync,如果不是在主線程中禁炒,則盡快post一個(gè)message去進(jìn)行vsync而咆,這個(gè)消息做的事肯定很重要霍比,因?yàn)閷?duì)這個(gè) Message 設(shè)置了異步的標(biāo)志而且用了sendMessageAtFrontOfQueue()
方法幕袱,這個(gè)方法是將這個(gè) Message 直接放到 MessageQueue 隊(duì)列里的頭部,可以理解成設(shè)置了這個(gè) Message 為最高優(yōu)先級(jí)悠瞬,那么先看看這個(gè) Message 做了些什么:
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
void doScheduleVsync() {
synchronized (mLock) {
if (mFrameScheduled) {
scheduleVsyncLocked();
}
}
}
所以這個(gè) Message 最后做的事就是 scheduleVsyncLocked()
们豌。我們回到 scheduleFrameLocked()
這個(gè)方法里,當(dāng)走 if 里的代碼時(shí)浅妆,直接調(diào)用了 scheduleVsyncLocked()
望迎,當(dāng)走 else 里的代碼時(shí),發(fā)了一個(gè)最高優(yōu)先級(jí)的 Message凌外,這個(gè) Message 也是執(zhí)行 scheduleVsyncLocked()
辩尊。既然兩邊最后調(diào)用的都是同一個(gè)方法,那么為什么這么做呢康辑?
關(guān)鍵在于 if 條件里那個(gè)方法摄欲,我的理解那個(gè)方法是用來判斷當(dāng)前是否是在主線程的,我們知道主線程也是一直在執(zhí)行著一個(gè)個(gè)的 Message疮薇,那么如果在主線程的話胸墙,直接調(diào)用這個(gè)方法,那么這個(gè)方法就可以直接被執(zhí)行了按咒,如果不是在主線程迟隅,那么 post 一個(gè)最高優(yōu)先級(jí)的 Message 到主線程去,保證這個(gè)方法可以第一時(shí)間得到處理励七。
那么這個(gè)方法是干嘛的呢智袭,為什么需要在最短時(shí)間內(nèi)被執(zhí)行呢,而且只能在主線程掠抬?
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}
調(diào)用了 native 層的一個(gè)方法吼野,那跟到這里就跟不下去了。
那到這里剿另,我們先來梳理一下:
到這里為止箫锤,我們知道一個(gè) View 發(fā)起刷新的操作時(shí)贬蛙,會(huì)層層通知到 ViewRootImpl 的 scheduleTraversals() 里去,然后這個(gè)方法會(huì)將遍歷繪制 View 樹的操作 performTraversals() 封裝到 Runnable 里谚攒,傳給 Choreographer阳准,以當(dāng)前的時(shí)間戳放進(jìn)一個(gè) mCallbackQueue 隊(duì)列里,然后調(diào)用了 native 層的一個(gè)方法就跟不下去了馏臭。所以這個(gè) Runnable 什么時(shí)候會(huì)被執(zhí)行還不清楚野蝇。那么,下去的重點(diǎn)就是搞清楚它什么時(shí)候從隊(duì)列里被拿出來執(zhí)行了括儒?
接下去只能換種方式繼續(xù)跟了绕沈,既然這個(gè) Runnable 操作被放在一個(gè) mCallbackQueue 隊(duì)列里,那就從這個(gè)隊(duì)列著手帮寻,看看這個(gè)隊(duì)列的取操作在哪被執(zhí)行了:
private final class CallbackQueue {
private CallbackRecord mHead;
public boolean hasDueCallbacksLocked(long now) {
return mHead != null && mHead.dueTime <= now;
}
// 取操作
public CallbackRecord extractDueCallbacksLocked(long now) {
CallbackRecord callbacks = mHead;
if (callbacks == null || callbacks.dueTime > now) {
return null;
}
CallbackRecord last = callbacks;
CallbackRecord next = last.next;
while (next != null) {
if (next.dueTime > now) {
last.next = null;
break;
}
last = next;
next = next.next;
}
mHead = next;
return callbacks;
}
// 加入操作
public void addCallbackLocked(long dueTime, Object action, Object token) {
......
}
public void removeCallbacksLocked(Object action, Object token) {
.....
}
}
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
// We use "now" to determine when callbacks become due because it is possible
// for earlier processing phases in a frame to post callbacks that should run
// in a following phase, such as an input event that causes an animation to start.
final long now = System.nanoTime();
'callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);'
......
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG_FRAMES) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
// 在這里回調(diào)了TraversalRunnable 進(jìn)行刷新界面
c.run(frameTimeNanos);
}
......
}
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
// 執(zhí)行 action 即 TraversalRunnable 進(jìn)行刷新界面
((Runnable)action).run();
}
}
}
void doFrame(long frameTimeNanos, int frame) {
......
try {
.....
mFrameInfo.markPerformTraversalsStart();
'doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);'
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
......
}
還記得我們說過在 ViewRootImpl 的 scheduleTraversals()
里會(huì)將遍歷 View 樹繪制的操作封裝到 Runnable 里乍狐,然后調(diào)用 Choreographer 的 postCallback()
將這個(gè) Runnable 放進(jìn)隊(duì)列里么,而當(dāng)時(shí)調(diào)用 postCallback()
時(shí)傳入了多個(gè)參數(shù)固逗,這是因?yàn)?Choreographer 里有多個(gè)隊(duì)列浅蚪,而第一個(gè)參數(shù) Choreographer.CALLBACK_TRAVERSAL 這個(gè)參數(shù)是用來區(qū)分隊(duì)列的,可以理解成各個(gè)隊(duì)列的 key 值烫罩。
那么這樣一來惜傲,就找到關(guān)鍵的方法了:doFrame()
,這個(gè)方法里會(huì)根據(jù)一個(gè)時(shí)間戳去隊(duì)列里取任務(wù)出來執(zhí)行贝攒,而這個(gè)任務(wù)就是 ViewRootImpl 封裝起來的 doTraversal()
操作盗誊,而 doTraversal()
會(huì)去調(diào)用 performTraversals()
開始根據(jù)需要測(cè)量、布局隘弊、繪制整顆 View 樹哈踱。所以剩下的問題就是 doFrame()
這個(gè)方法在哪里被調(diào)用了。
doFrame()
有幾個(gè)調(diào)用的地方长捧,但有個(gè)地方很關(guān)鍵:
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
// 信號(hào)到來的時(shí)間參數(shù)
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
// Ignore vsync from secondary display.
// This can be problematic because the call to scheduleVsync() is a one-shot.
// We need to ensure that we will still receive the vsync from the primary
// display which is the one we really care about. Ideally we should schedule
// vsync for a particular display.
// At this time Surface Flinger will not send us vsyncs for secondary displays
// but that could change in the future so let is log a message to help us remember
// that we need to fix this.
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
Log.d(TAG, "Received vsync from secondary display, but we don't support "
+ "this case yet. Choreographer needs a way to explicitly request "
+ "vsync for a specific display to ensure it doesn't lose track "
+ "of its scheduled vsync.");
scheduleVsync();
return;
}
// Post the vsync event to the Handler.
// The idea is to prevent incoming vsync events from completely starving
// the message queue. If there are no messages in the queue with timestamps
// earlier than the frame time, then the vsync event will be processed immediately.
// Otherwise, messages that predate the vsync event will be handled first.
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
'doFrame(mTimestampNanos, mFrame);'
}
}
關(guān)鍵的地方來了嚣鄙,這個(gè)繼承自 DisplayEventReceiver 的 FrameDisplayEventReceiver 類的作用很重要。跟進(jìn)去看注釋串结,我只能理解它是用來接收底層信號(hào)用的哑子。但看了網(wǎng)上的解釋后,所有的都理解過來了:
FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號(hào)開始處理UI過程肌割。VSync信號(hào)由SurfaceFlinger實(shí)現(xiàn)并定時(shí)發(fā)送卧蜓。FrameDisplayEventReceiver收到信號(hào)后,調(diào)用onVsync方法組織消息發(fā)送到主線程處理把敞。這個(gè)消息主要內(nèi)容就是run方法里面的doFrame了弥奸,這里mTimestampNanos是信號(hào)到來的時(shí)間參數(shù)。
也就是說奋早,onVsync()
是底層會(huì)回調(diào)的盛霎,可以理解成每隔 16.6ms 一個(gè)幀信號(hào)來的時(shí)候赠橙,底層就會(huì)回調(diào)這個(gè)方法,當(dāng)然前提是我們得先注冊(cè)愤炸,這樣底層才能找到我們 app 并回調(diào)期揪。當(dāng)這個(gè)方法被回調(diào)時(shí),內(nèi)部發(fā)起了一個(gè) Message规个,注意看代碼對(duì)這個(gè) Message 設(shè)置了 callback 為 this凤薛,Handler 在處理消息時(shí)會(huì)先查看 Message 是否有 callback,有則優(yōu)先交由 Message 的 callback 處理消息诞仓,沒有的話再去看看Handler 有沒有 callback缤苫,如果也沒有才會(huì)交由 handleMessage()
這個(gè)方法執(zhí)行。
這里這么做的原因墅拭,我猜測(cè)可能 onVsync()
是由底層回調(diào)的活玲,那么它就不是運(yùn)行在我們 app 的主線程上,畢竟上層 app 對(duì)底層是隱藏的帜矾。但這個(gè) doFrame()
是個(gè) ui 操作翼虫,它需要在主線程中執(zhí)行屑柔,所以才通過 Handler 切到主線程中屡萤。
還記得我們前面分析 scheduleTraversals()
方法時(shí),最后跟到了一個(gè) native 層方法就跟不下去了么掸宛,現(xiàn)在再回過來想想這個(gè) native 層方法的作用是什么死陆,應(yīng)該就比較好猜測(cè)了。
/**
* Schedules a single vertical sync pulse to be delivered when the next
* display frame begins.
*/
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}}
英文不大理解唧瘾,大體上可能是說安排接收一個(gè) vsync 信號(hào)措译。而根據(jù)我們的分析,如果這個(gè) vsync 信號(hào)發(fā)出的話饰序,底層就會(huì)回調(diào) DisplayEventReceiver 的 onVsync()
方法领虹。
那如果只是這樣的話,就有一點(diǎn)說不通了求豫,首先上層 app 對(duì)于這些發(fā)送 vsync 信號(hào)的底層來說肯定是隱藏的塌衰,也就是說底層它根本不知道上層 app 的存在,那么在它的每 16.6ms 的幀信號(hào)來的時(shí)候蝠嘉,它是怎么找到我們的 app最疆,并回調(diào)它的方法呢?
這就有點(diǎn)類似于觀察者模式蚤告,或者說發(fā)布-訂閱模式努酸。既然上層 app 需要知道底層每隔 16.6ms 的幀信號(hào)事件,那么它就需要先注冊(cè)監(jiān)聽才對(duì)杜恰,這樣底層在發(fā)信號(hào)的時(shí)候获诈,直接去找這些觀察者通知它們就行了仍源。
這是我的理解,所以舔涎,這樣一來镜会,scheduleVsync()
這個(gè)調(diào)用到了 native 層方法的作用大體上就可以理解成注冊(cè)監(jiān)聽了,這樣底層也才找得到上層 app终抽,并在每 16.6ms 刷新信號(hào)發(fā)出的時(shí)候回調(diào)上層 app 的 onVsync() 方法戳表。這樣一來,應(yīng)該就說得通了昼伴。
還有一點(diǎn)匾旭,scheduleVsync()
注冊(cè)的監(jiān)聽?wèi)?yīng)該只是監(jiān)聽下一個(gè)屏幕刷新信號(hào)的事件而已,而不是監(jiān)聽所有的屏幕刷新信號(hào)圃郊。比如說當(dāng)前監(jiān)聽了第一幀的刷新信號(hào)事件价涝,那么當(dāng)?shù)谝粠乃⑿滦盘?hào)來的時(shí)候,上層 app 就能接收到事件并作出反應(yīng)持舆。但如果還想監(jiān)聽第二幀的刷新信號(hào)俗扇,那么只能等上層 app 接收到第一幀的刷新信號(hào)之后再去監(jiān)聽下一幀。
雖然現(xiàn)在能力還不足以跟蹤到 native 層性芬,這些結(jié)論雖然是猜測(cè)的慨蓝,但都經(jīng)過調(diào)試,對(duì)注釋竹伸、代碼理解之后梳理出來的結(jié)論泥栖,跟原理應(yīng)該不會(huì)偏差太多,這樣子的理解應(yīng)該是可以的勋篓。
這里我再把流程理一理吧享,首先我們?cè)趕cheduleTraversals方法中通過postCallback方法將我們需要執(zhí)行的任務(wù)添加到callbackQueue中,使系統(tǒng)發(fā)送Vsync信號(hào)譬嚣,如果我們注冊(cè)了FrameDisplayEventReceiver接口钢颂,那么在接收到Vsync信號(hào)時(shí)會(huì)將自身作為callback發(fā)送message,進(jìn)而執(zhí)行doFrame等方法實(shí)現(xiàn)view的繪制拜银;
本篇內(nèi)容確實(shí)有點(diǎn)多殊鞭,所以到這里還是繼續(xù)來先來梳理一下目前的信息,防止都忘記上面講了些什么:
我們知道一個(gè) View 發(fā)起刷新的操作時(shí)盐股,最終是走到了 ViewRootImpl 的 scheduleTraversals() 里去钱豁,然后這個(gè)方法會(huì)將遍歷繪制 View 樹的操作 performTraversals() 封裝到 Runnable 里,傳給 Choreographer疯汁,以當(dāng)前的時(shí)間戳放進(jìn)一個(gè) mCallbackQueue 隊(duì)列里牲尺,然后調(diào)用了 native 層的方法向底層注冊(cè)監(jiān)聽下一個(gè)屏幕刷新信號(hào)事件。
當(dāng)下一個(gè)屏幕刷新信號(hào)發(fā)出的時(shí)候,如果我們 app 有對(duì)這個(gè)事件進(jìn)行監(jiān)聽谤碳,那么底層它就會(huì)回調(diào)我們 app 層的 onVsync() 方法來通知溃卡。當(dāng) onVsync() 被回調(diào)時(shí),會(huì)發(fā)一個(gè) Message 到主線程蜒简,將后續(xù)的工作切到主線程來執(zhí)行瘸羡。
切到主線程的工作就是去 mCallbackQueue 隊(duì)列里根據(jù)時(shí)間戳將之前放進(jìn)去的 Runnable 取出來執(zhí)行,而這些 Runnable 有一個(gè)就是遍歷繪制 View 樹的操作 performTraversals()搓茬。在這次的遍歷操作中犹赖,就會(huì)去繪制那些需要刷新的 View。
所以說卷仑,當(dāng)我們調(diào)用了 invalidate()峻村,requestLayout(),等之類刷新界面的操作時(shí)锡凝,并不是馬上就會(huì)執(zhí)行這些刷新的操作粘昨,而是通過 ViewRootImpl 的 scheduleTraversals() 先向底層注冊(cè)監(jiān)聽下一個(gè)屏幕刷新信號(hào)事件,然后等下一個(gè)屏幕刷新信號(hào)來的時(shí)候窜锯,才會(huì)去通過 performTraversals() 遍歷繪制 View 樹來執(zhí)行這些刷新操作张肾。
過濾一幀內(nèi)重復(fù)的刷新請(qǐng)求
整體上的流程我們已經(jīng)梳理出來的,但還有幾點(diǎn)問題需要解決锚扎。我們?cè)谝粋€(gè) 16.6ms 的一幀內(nèi)吞瞪,代碼里可能會(huì)有多個(gè) View 發(fā)起了刷新請(qǐng)求,這是非常常見的場(chǎng)景了工秩,比如某個(gè)動(dòng)畫是有多個(gè) View 一起完成尸饺,比如界面發(fā)生了滑動(dòng)等等。
按照我們上面梳理的流程助币,只要 View 發(fā)起了刷新請(qǐng)求最終都會(huì)走到 ViewRootImpl 中的 scheduleTraversals()
里去,是吧螟碎。而這個(gè)方法又會(huì)封裝一個(gè)遍歷繪制 View 樹的操作 performTraversals()
到 Runnable 然后扔到隊(duì)列里等刷新信號(hào)來的時(shí)候取出來執(zhí)行眉菱,沒錯(cuò)吧。
那如果多個(gè) View 發(fā)起了刷新請(qǐng)求掉分,豈不是意味著會(huì)有多次遍歷繪制 View 樹的操作俭缓?
其實(shí),這點(diǎn)不用擔(dān)心酥郭,還記得我們?cè)谧铋_始分析 scheduleTraverslas()
的時(shí)候先跳過了一些代碼么华坦?現(xiàn)在我們回過來繼續(xù)看看這些代碼:
void scheduleTraversals() {
if (!mTraversalScheduled) {
......
}
}
我們上面分析的 scheduleTraversals()
干的那一串工作,前提是 mTraversalScheduled 這個(gè) boolean 類型變量等于 false 才會(huì)去執(zhí)行不从。那這個(gè)變量在什么時(shí)候被賦值被 false 了呢:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
......
}
}
}
只有三個(gè)被賦值為 false 的地方惜姐,一個(gè)是上圖的 doTraversal()
,還有就是聲明時(shí)默認(rèn)為 false,剩下一個(gè)是在取消遍歷繪制 View 操作 unscheduleTraversals()
里歹袁。這兩個(gè)可以先不去看坷衍,就看看 doTraversal()
。還記得這個(gè)方法吧条舔,就是在 scheduleTraversals()
中封裝到 Runnable 里的那個(gè)方法枫耳。
也就是說,當(dāng)我們調(diào)用了一次 scheduleTraversals()
之后孟抗,直到下一個(gè)屏幕刷新信號(hào)來的時(shí)候迁杨,doTraversal()
被取出來執(zhí)行。在這期間重復(fù)調(diào)用 scheduleTraversals()
都會(huì)被過濾掉的凄硼。那么為什么需要這樣呢仑最?
其實(shí),想想就能明白了帆喇。View 最終是怎么刷新的呢警医,就是在執(zhí)行 performTraversals()
遍歷繪制 View 樹過程中層層遍歷到需要刷新的 View,然后去繪制它的吧坯钦。既然是遍歷预皇,那么不管上一幀內(nèi)有多少個(gè) View 發(fā)起了刷新的請(qǐng)求,在這一次的遍歷過程中全部都會(huì)去處理的吧婉刀。這也是我們從代碼上看到的吟温,每一個(gè)屏幕刷新信號(hào)來的時(shí)候,只會(huì)去執(zhí)行一次 performTraversals()
突颊,因?yàn)橹恍璞闅v一遍鲁豪,就能夠刷新所有的 View 了。
而 performTraversals()
會(huì)被執(zhí)行的前提是調(diào)用了 scheduleTraversals()
來向底層注冊(cè)監(jiān)聽了下一個(gè)屏幕刷新信號(hào)事件律秃,所以在同一個(gè) 16.6ms 的一幀內(nèi)爬橡,只需要第一個(gè)發(fā)起刷新請(qǐng)求的 View 來走一遍 scheduleTraversals()
干的事就可以了,其他不管還有多少 View 發(fā)起了刷新請(qǐng)求棒动,沒必要再去重復(fù)向底層注冊(cè)監(jiān)聽下一個(gè)屏幕刷新信號(hào)事件了糙申,反正只要有一次遍歷繪制 View 樹的操作就可以對(duì)它們進(jìn)行刷新了。
postSyncBarrier()---同步屏障消息
還剩最后一個(gè)問題船惨,scheduleTraversals()
里我們還有一行代碼沒分析柜裸。這個(gè)問題是這樣的:
我們清楚主線程其實(shí)是一直在處理 MessageQueue 消息隊(duì)列里的 Message,每個(gè)操作都是一個(gè) Message粱锐,打開 Activity 是一個(gè) Message疙挺,遍歷繪制 View 樹來刷新屏幕也是一個(gè) Message。
而且怜浅,上面梳理完我們也清楚铐然,遍歷繪制 View 樹的操作是在屏幕刷新信號(hào)到的時(shí)候,底層回調(diào)我們 app 的 onVsync()
,這個(gè)方法再去將遍歷繪制 View 樹的操作 post 到主線程的 MessageQueue 中去等待執(zhí)行锦爵。主線程同一時(shí)間只能處理一個(gè) Message舱殿,這些 Message 就肯定有先后的問題,那么會(huì)不會(huì)出現(xiàn)下面這種情況呢:
[圖片上傳失敗...(image-784ac6-1525502047023)]
也就是說险掀,當(dāng)我們的 app 接收到屏幕刷新信號(hào)時(shí)沪袭,來不及第一時(shí)間就去執(zhí)行刷新屏幕的操作,這樣一來樟氢,即使我們將布局優(yōu)化得很徹底冈绊,保證繪制當(dāng)前 View 樹不會(huì)超過 16ms,但如果不能第一時(shí)間優(yōu)先處理繪制 View 的工作埠啃,那等 16.6 ms 過了死宣,底層需要去切換下一幀的畫面了,我們 app 卻還沒處理完碴开,這樣也照樣會(huì)出現(xiàn)丟幀了吧毅该。而且這種場(chǎng)景是非常有可能出現(xiàn)的吧,畢竟主線程需要處理的事肯定不僅僅是刷新屏幕的事而已潦牛,那么這個(gè)問題是怎么處理的呢眶掌?
所以我們繼續(xù)回來看 scheduleTraversals()
:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 發(fā)送同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
......
}
}
在邏輯走進(jìn) Choreographer 前會(huì)先往隊(duì)列里發(fā)送一個(gè)同步屏障,而當(dāng) doTraversal()
被調(diào)用時(shí)才將同步屏障移除巴碗。這個(gè)同步屏障又涉及到消息機(jī)制了朴爬,不深入了,這里就只給出結(jié)論橡淆。
這個(gè)同步屏障的作用可以理解成攔截同步消息的執(zhí)行召噩,主線程的 Looper 會(huì)一直循環(huán)調(diào)用 MessageQueue 的 next()
來取出隊(duì)頭的 Message 執(zhí)行,當(dāng) Message 執(zhí)行完后再去取下一個(gè)逸爵。當(dāng) next()
方法在取 Message 時(shí)發(fā)現(xiàn)隊(duì)頭是一個(gè)同步屏障的消息時(shí)具滴,就會(huì)去遍歷整個(gè)隊(duì)列,只尋找設(shè)置了異步標(biāo)志的消息痊银,如果有找到異步消息抵蚊,那么就取出這個(gè)異步消息來執(zhí)行,否則就讓 next()
方法陷入阻塞狀態(tài)溯革。如果 next()
方法陷入阻塞狀態(tài),那么主線程此時(shí)就是處于空閑狀態(tài)的谷醉,也就是沒在干任何事致稀。所以,如果隊(duì)頭是一個(gè)同步屏障的消息的話俱尼,那么在它后面的所有同步消息就都被攔截住了抖单,直到這個(gè)同步屏障消息被移除出隊(duì)列,否則主線程就一直不會(huì)去處理同步屏幕后面的同步消息。
注:
內(nèi)存屏障和編譯屏障就是用來告訴CPU和編譯器停止優(yōu)化的方法矛绘。
內(nèi)存屏障是在代碼中插入特殊指令耍休,CPU遇到這些特殊指令,要等待前面指令執(zhí)行完成了货矮,才執(zhí)行后面的指令羊精。這些指令的作用好像是一道屏障把前后指令隔開,這樣就防止了CPU把前后兩段指令顛倒執(zhí)行囚玫。
而所有消息默認(rèn)都是同步消息喧锦,只有手動(dòng)設(shè)置了異步標(biāo)志,這個(gè)消息才會(huì)是異步消息抓督。另外燃少,同步屏障消息只能由內(nèi)部來發(fā)送,這個(gè)接口并沒有公開給我們使用铃在。
最后阵具,仔細(xì)看上面 Choreographer 里所有跟 message 有關(guān)的代碼,你會(huì)發(fā)現(xiàn)定铜,都手動(dòng)設(shè)置了異步消息的標(biāo)志阳液,所以這些操作是不受到同步屏障影響的。這樣做的原因可能就是為了盡可能保證上層 app 在接收到屏幕刷新信號(hào)時(shí)宿稀,可以在第一時(shí)間執(zhí)行遍歷繪制 View 樹的工作趁舀。
因?yàn)橹骶€程中如果有太多消息要執(zhí)行,而這些消息又是根據(jù)時(shí)間戳進(jìn)行排序祝沸,如果不加一個(gè)同步屏障的話矮烹,那么遍歷繪制 View 樹的工作就可能被迫延遲執(zhí)行,因?yàn)樗残枰抨?duì)罩锐,那么就有可能出現(xiàn)當(dāng)一幀都快結(jié)束的時(shí)候才開始計(jì)算屏幕數(shù)據(jù)奉狈,那即使這次的計(jì)算少于 16.6ms,也同樣會(huì)造成丟幀現(xiàn)象涩惑。
那么仁期,有了同步屏障消息的控制就能保證每次一接收到屏幕刷新信號(hào)就第一時(shí)間處理遍歷繪制 View 樹的工作么?
只能說竭恬,同步屏障是盡可能去做到跛蛋,但并不能保證一定可以第一時(shí)間處理。因?yàn)槿叮狡琳鲜窃?scheduleTraversals()
被調(diào)用時(shí)才發(fā)送到消息隊(duì)列里的赊级,也就是說,只有當(dāng)某個(gè) View 發(fā)起了刷新請(qǐng)求時(shí)岔绸,在這個(gè)時(shí)刻后面的同步消息才會(huì)被攔截掉理逊。如果在 scheduleTraversals()
之前就發(fā)送到消息隊(duì)列里的工作仍然會(huì)按順序依次被取出來執(zhí)行橡伞。
界面刷新控制者--ViewRootImpl
最后,就是上文經(jīng)常說的一點(diǎn)晋被,所有跟界面刷新相關(guān)的操作兑徘,其實(shí)最終都會(huì)走到 ViewRootImpl 中的 scheduleTraversals()
去的。
大伙可以想想羡洛,跟界面刷新有關(guān)的操作有哪些挂脑,大概就是下面幾種場(chǎng)景吧:
- invalidate(請(qǐng)求重繪)
- requestLayout(重新布局)
- requestFocus(請(qǐng)求焦點(diǎn))
- startActivity(打開新界面)
- onRestart(重新打開界面)
- KeyEvent(遙控器事件,本質(zhì)上是焦點(diǎn)導(dǎo)致的刷新)
- Animation(各種動(dòng)畫翘县,本質(zhì)上是請(qǐng)求重繪導(dǎo)致的刷新)
- RecyclerView滑動(dòng)(頁面滑動(dòng)最域,本質(zhì)上是動(dòng)畫導(dǎo)致的刷新)
- setAdapter(各種adapter的更新)
- ...
在上一篇分析動(dòng)畫的博客里,我們跟蹤了 invalidate()
锈麸,確實(shí)也是這樣镀脂,至于其他的我并沒有一一去驗(yàn)證,大伙有興趣可以看看忘伞,我猜測(cè)薄翅,這些跟界面刷新有關(guān)的方法內(nèi)部要么就是一個(gè) do{}while() 循環(huán)尋找 mParent,要么就是直接不斷的調(diào)用 mParent 的方法氓奈。而一顆 View 樹最頂端的 mParent 就是 ViewRootImpl翘魄,所以這些跟界面刷新相關(guān)的方法,在 ViewRootImpl 肯定也是可以找到的:
@Override
public void requestChildFocus(View child, View focused) {
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "Request child focus: focus now " + focused);
}
checkThread();
'scheduleTraversals();'
}
@Override
public void clearChildFocus(View child) {
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "Clearing child focus");
}
checkThread();
'scheduleTraversals();'
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
'scheduleTraversals();'
}
}
其實(shí)舀奶,以前我一直以為如果界面上某個(gè)小小的 View 發(fā)起了 invalidate()
重繪之類的操作暑竟,那么應(yīng)該就只是它自己的 onLayout()
, onDraw()
被調(diào)用來重繪而已。最后才清楚育勺,原來但荤,即使再小的 View,如果發(fā)起了重繪的請(qǐng)求涧至,那么也需要先層層走到 ViewRootImpl 里去腹躁,而且還不是馬上就執(zhí)行重繪操作,而是需要等待下一個(gè)屏幕刷新信號(hào)來的時(shí)候南蓬,再從 DecorView 開始層層遍歷到這些需要刷新的 View 里去重繪它們纺非。
總結(jié)
本篇篇幅確實(shí)很長,因?yàn)檫@部分內(nèi)容要理清楚不容易赘方,要講清楚更不容易烧颖,大伙如果有時(shí)間,可以靜下心來慢慢看窄陡,從頭看下來倒信,我相信,多少會(huì)有些收獲的泳梆。如果沒時(shí)間鳖悠,那么也可以直接看看總結(jié)。
- 界面上任何一個(gè) View 的刷新請(qǐng)求最終都會(huì)走到 ViewRootImpl 中的 scheduleTraversals() 里來安排一次遍歷繪制 View 樹的任務(wù)优妙;
- scheduleTraversals() 會(huì)先過濾掉同一幀內(nèi)的重復(fù)調(diào)用乘综,在同一幀內(nèi)只需要安排一次遍歷繪制 View 樹的任務(wù)即可,這個(gè)任務(wù)會(huì)在下一個(gè)屏幕刷新信號(hào)到來時(shí)調(diào)用 performTraversals() 遍歷 View 樹套硼,遍歷過程中會(huì)將所有需要刷新的 View 進(jìn)行重繪卡辰;
- 接著 scheduleTraversals() 會(huì)往主線程的消息隊(duì)列中發(fā)送一個(gè)同步屏障,攔截這個(gè)時(shí)刻之后所有的同步消息的執(zhí)行邪意,但不會(huì)攔截異步消息九妈,以此來盡可能的保證當(dāng)接收到屏幕刷新信號(hào)時(shí)可以盡可能第一時(shí)間處理遍歷繪制 View 樹的工作;
- 發(fā)完同步屏障后 scheduleTraversals() 才會(huì)開始安排一個(gè)遍歷繪制 View 樹的操作雾鬼,作法是把 performTraversals() 封裝到 Runnable 里面萌朱,然后調(diào)用 Choreographer 的 postCallback() 方法;
- postCallback() 方法會(huì)先將這個(gè) Runnable 任務(wù)以當(dāng)前時(shí)間戳放進(jìn)一個(gè)待執(zhí)行的隊(duì)列里策菜,然后如果當(dāng)前是在主線程就會(huì)直接調(diào)用一個(gè)native 層方法晶疼,如果不是在主線程,會(huì)發(fā)一個(gè)最高優(yōu)先級(jí)的 message 到主線程又憨,讓主線程第一時(shí)間調(diào)用這個(gè) native 層的方法翠霍;
- native 層的這個(gè)方法是用來向底層注冊(cè)監(jiān)聽下一個(gè)屏幕刷新信號(hào)揪罕,當(dāng)下一個(gè)屏幕刷新信號(hào)發(fā)出時(shí)话速,底層就會(huì)回調(diào) Choreographer 的onVsync() 方法來通知上層 app;
- onVsync() 方法被回調(diào)時(shí)呻率,會(huì)往主線程的消息隊(duì)列中發(fā)送一個(gè)執(zhí)行 doFrame() 方法的消息躏将,這個(gè)消息是異步消息锄弱,所以不會(huì)被同步屏障攔截住耸携;
- doFrame() 方法會(huì)去取出之前放進(jìn)待執(zhí)行隊(duì)列里的任務(wù)來執(zhí)行棵癣,取出來的這個(gè)任務(wù)實(shí)際上是 ViewRootImpl 的 doTraversal() 操作;
- 上述第4步到第8步涉及到的消息都手動(dòng)設(shè)置成了異步消息夺衍,所以不會(huì)受到同步屏障的攔截狈谊;
- doTraversal() 方法會(huì)先移除主線程的同步屏障,然后調(diào)用 performTraversals() 開始根據(jù)當(dāng)前狀態(tài)判斷是否需要執(zhí)行performMeasure() 測(cè)量沟沙、perfromLayout() 布局河劝、performDraw() 繪制流程,在這幾個(gè)流程中都會(huì)去遍歷 View 樹來刷新需要更新的View矛紫;
再來一張時(shí)序圖結(jié)尾赎瞎,大伙想自己過源碼時(shí)可以跟著時(shí)序圖來,建議在電腦上閱讀:
[圖片上傳失敗...(image-8f3bbf-1525502047023)]
QA
Q1:Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思颊咬?是指每隔 16.6ms 調(diào)用 onDraw() 繪制一次么务甥?
Q2:如果界面一直保持沒變的話牡辽,那么還會(huì)每隔 16.6ms 刷新一次屏幕么?
答:我們常說的 Android 每隔 16.6 ms 刷新一次屏幕其實(shí)是指底層會(huì)以這個(gè)固定頻率來切換每一幀的畫面敞临,而這個(gè)每一幀的畫面數(shù)據(jù)就是我們 app 在接收到屏幕刷新信號(hào)之后去執(zhí)行遍歷繪制 View 樹工作所計(jì)算出來的屏幕數(shù)據(jù)态辛。而 app 并不是每隔 16.6ms 的屏幕刷新信號(hào)都可以接收到,只有當(dāng) app 向底層注冊(cè)監(jiān)聽下一個(gè)屏幕刷新信號(hào)之后挺尿,才能接收到下一個(gè)屏幕刷新信號(hào)到來的通知奏黑。而只有當(dāng)某個(gè) View 發(fā)起了刷新請(qǐng)求時(shí),app 才會(huì)去向底層注冊(cè)監(jiān)聽下一個(gè)屏幕刷新信號(hào)编矾。
也就是說熟史,只有當(dāng)界面有刷新的需要時(shí),我們 app 才會(huì)在下一個(gè)屏幕刷新信號(hào)來時(shí)窄俏,遍歷繪制 View 樹來重新計(jì)算屏幕數(shù)據(jù)蹂匹。如果界面沒有刷新的需要,一直保持不變時(shí)裆操,我們 app 就不會(huì)去接收每隔 16.6ms 的屏幕刷新信號(hào)事件了怒详,但底層仍然會(huì)以這個(gè)固定頻率來切換每一幀的畫面,只是后面這些幀的畫面都是相同的而已踪区。
Q3:界面的顯示其實(shí)就是一個(gè) Activity 的 View 樹里所有的 View 都進(jìn)行測(cè)量昆烁、布局、繪制操作之后的結(jié)果呈現(xiàn)缎岗,那么如果這部分工作都完成后静尼,屏幕會(huì)馬上就刷新么?
答:我們 app 只負(fù)責(zé)計(jì)算屏幕數(shù)據(jù)而已传泊,接收到屏幕刷新信號(hào)就去計(jì)算鼠渺,計(jì)算完畢就計(jì)算完畢了。至于屏幕的刷新眷细,這些是由底層以固定的頻率來切換屏幕每一幀的畫面拦盹。所以即使屏幕數(shù)據(jù)都計(jì)算完畢,屏幕會(huì)不會(huì)馬上刷新就取決于底層是否到了要切換下一幀畫面的時(shí)機(jī)了溪椎。
Q4:網(wǎng)上都說避免丟幀的方法之一是保證每次繪制界面的操作要在 16.6ms 內(nèi)完成普舆,但如果這個(gè) 16.6ms 是一個(gè)固定的頻率的話,請(qǐng)求繪制的操作在代碼里被調(diào)用的時(shí)機(jī)是不確定的啊校读,那么如果某次用戶點(diǎn)擊屏幕導(dǎo)致的界面刷新操作是在某一個(gè) 16.6ms 幀快結(jié)束的時(shí)候沼侣,那么即使這次繪制操作小于 16.6 ms,按道理不也會(huì)造成丟幀么歉秫?這又該如何理解蛾洛?
答:之所以提了這個(gè)問題,是因?yàn)橹笆且詾槿绻硞€(gè) View 發(fā)起了刷新請(qǐng)求雁芙,比如調(diào)用了 invalidte()
轧膘,那么它的重繪工作就馬上開始執(zhí)行了钞螟,所以以前在看網(wǎng)上那些介紹屏幕刷新機(jī)制的博客時(shí),經(jīng)撤龉看見下面這張圖:
[圖片上傳失敗...(image-b64cc4-1525502047023)]
那個(gè)時(shí)候就是不大理解筛圆,為什么每一次 CPU 計(jì)算的工作都剛剛好是在每一個(gè)信號(hào)到來的那個(gè)瞬間開始的呢?畢竟代碼里發(fā)起刷新屏幕的操作是動(dòng)態(tài)的椿浓,不可能每次都剛剛好那么巧。
梳理完屏幕刷新機(jī)制后就清楚了闽晦,代碼里調(diào)用了某個(gè) View 發(fā)起的刷新請(qǐng)求扳碍,這個(gè)重繪工作并不會(huì)馬上就開始,而是需要等到下一個(gè)屏幕刷新信號(hào)來的時(shí)候才開始仙蛉,所以現(xiàn)在回過頭來看這些圖就清楚多了笋敞。
Q5:大伙都清楚,主線程耗時(shí)的操作會(huì)導(dǎo)致丟幀荠瘪,但是耗時(shí)的操作為什么會(huì)導(dǎo)致丟幀夯巷?它是如何導(dǎo)致丟幀發(fā)生的?
答:造成丟幀大體上有兩類原因哀墓,一是遍歷繪制 View 樹計(jì)算屏幕數(shù)據(jù)的時(shí)間超過了 16.6ms趁餐;二是,主線程一直在處理其他耗時(shí)的消息篮绰,導(dǎo)致遍歷繪制 View 樹的工作遲遲不能開始后雷,從而超過了 16.6 ms 底層切換下一幀畫面的時(shí)機(jī)。
第一個(gè)原因就是我們寫的布局有問題了吠各,需要進(jìn)行優(yōu)化了臀突。而第二個(gè)原因則是我們常說的避免在主線程中做耗時(shí)的任務(wù)。
針對(duì)第二個(gè)原因贾漏,系統(tǒng)已經(jīng)引入了同步屏障消息的機(jī)制候学,盡可能的保證遍歷繪制 View 樹的工作能夠及時(shí)進(jìn)行,但仍沒辦法完全避免纵散,所以我們還是得盡可能避免主線程耗時(shí)工作梳码。
其實(shí)第二個(gè)原因,可以拿出來細(xì)講的困食,比如有這種情況边翁, message 不怎么耗時(shí),但數(shù)量太多硕盹,這同樣可能會(huì)造成丟幀符匾。如果有使用一些圖片框架的,它內(nèi)部下載圖片都是開線程去下載瘩例,但當(dāng)下載完成后需要把圖片加載到綁定的 view 上啊胶,這個(gè)工作就是發(fā)了一個(gè) message 切到主線程來做甸各,如果一個(gè)界面這種 view 特別多的話,隊(duì)列里就會(huì)有非常多的 message焰坪,雖然每個(gè)都 message 并不怎么耗時(shí)趣倾,但經(jīng)不起量多啊。后面有時(shí)間的話某饰,看看要不要專門整理一篇文章來講卡頓和丟幀的事儒恋。