這次想來梳理一下 View 動(dòng)畫也就是補(bǔ)間動(dòng)畫(ScaleAnimation, AlphaAnimation, TranslationAnimation...)這些動(dòng)畫運(yùn)行的流程解析。內(nèi)容并不會(huì)去分析動(dòng)畫的呈現(xiàn)原理是什么,諸如 Matrix 這類的原理是什么,因?yàn)槲乙策€沒搞懂。本篇主要是分析當(dāng)調(diào)用了?View.startAnimation()?之后,動(dòng)畫從開始到結(jié)束的一個(gè)運(yùn)行流程是什么?
提問環(huán)節(jié)
看源碼最好是帶著問題去,這樣比較有目的性和針對性,可以防止閱讀源碼時(shí)走偏和鉆牛角,所以我們就先來提幾個(gè)問題。
Animation 動(dòng)畫的擴(kuò)展性很高,系統(tǒng)只是簡單的為我們封裝了幾個(gè)基本的動(dòng)畫:平移、旋轉(zhuǎn)、透明度、縮放等等,感興趣的可以去看看這幾個(gè)動(dòng)畫的源碼,它們都是繼承自 Animation 類,然后實(shí)現(xiàn)了?applyTransformation()?方法,在這個(gè)方法里通過 Transformation 和 Matrix 實(shí)現(xiàn)各種各樣炫酷的動(dòng)畫,所以,如果想要做出炫酷的動(dòng)畫效果,這些還是需要去搞懂的。
目前我也還沒搞懂,能力有限,所以優(yōu)先分析動(dòng)畫的一個(gè)運(yùn)行流程。
首先看看 Animation 動(dòng)畫的基本用法:
我們要使用一個(gè) View 動(dòng)畫時(shí),一般都是先 new 一個(gè)動(dòng)畫,然后配置各種參數(shù),最后調(diào)用動(dòng)畫要作用到的那個(gè) View 的 startAnimation(), 將動(dòng)畫實(shí)例作為參數(shù)傳進(jìn)去,接下去就可以看到動(dòng)畫運(yùn)行的效果了。
那么,問題來了:
Q1:不知道大伙想過沒有,當(dāng)調(diào)用了 View.startAnimation() 之后,動(dòng)畫是馬上就執(zhí)行了么?
Q2:假如動(dòng)畫持續(xù)時(shí)間 300ms,當(dāng)調(diào)用了 View.startAniamtion() 之后,又發(fā)起了一次界面刷新的操作,那么界面的刷新是在 300ms 之后也就是動(dòng)畫執(zhí)行完畢之后才執(zhí)行的,還是在動(dòng)畫執(zhí)行過程中界面刷新操作就執(zhí)行了呢?
我們都知道,applyTransformation()?這個(gè)方法是動(dòng)畫生效的地方,這個(gè)方法被回調(diào)時(shí)參數(shù)會(huì)傳進(jìn)來當(dāng)前動(dòng)畫的進(jìn)度(0.0 ——— 1.0)。就像數(shù)學(xué)上的畫曲線,當(dāng)給的點(diǎn)越多時(shí)畫的曲線越光滑,同樣當(dāng)這個(gè)方法被回調(diào)越多次時(shí),動(dòng)畫的效果越流暢。
比如一個(gè)從 0 放大到 1280 的 View 放大動(dòng)畫,如果這過程該方法只回調(diào) 3 次的話,那么每次的跨度就會(huì)很大,比如 0 —— 600 —— 1280,那么這個(gè)動(dòng)畫效果看起來就會(huì)很突兀;相反,如果這過程該方法回調(diào)了幾十次的話,那么每次跨度可能就只有 100,這樣一來動(dòng)畫效果看起來就會(huì)很流暢。
相信大伙也都有過在?applyTransformation()?里打日志來查看當(dāng)前的動(dòng)畫進(jìn)度,有時(shí)打出的日志有十幾條,有時(shí)卻又有幾十條。
那么我們的問題就來了:
Q3:applyTransformation() 這個(gè)方法的回調(diào)次數(shù)是根據(jù)什么來決定的?
好了,本篇就是主要講解這三個(gè)問題,這三個(gè)問題搞明白的話,以后碰到動(dòng)畫卡頓的時(shí)候就懂得如何去分析、定位丟幀的地方了,找到丟幀的問題所在后離解決問題也就不遠(yuǎn)了。
源碼分析
ps:本篇分析的源碼全都基于 android-25 版本。以下源碼均采用截圖方式,每張圖最上面是類名+方法名,大伙想自己過一遍的時(shí)候,如果不清楚方法屬于哪個(gè)類的可以在每張圖最上面查看。
View.startAnimation()
剛開始接觸源碼分析可能不清楚該從哪入手,建議可以從我們使用它的地方來?startAnimation():
代碼不多,調(diào)用了四個(gè)方法,那么一個(gè)個(gè)跟進(jìn)去看看,先是?setStartTime()?:
所以這里只是對一些變量進(jìn)行賦值,并沒有運(yùn)行動(dòng)畫的邏輯,繼續(xù)看看?setAnimation():
View 里面有一個(gè) Animation 類型的成員變量,所以這個(gè)方法其實(shí)是將我們 new 的 ScaleAnimation 動(dòng)畫跟 View 綁定起來而已,也沒有運(yùn)行動(dòng)畫的邏輯,繼續(xù)往下看看?invalidateParentCached():
invalidateParentCaches()?這方法更簡單,給 mPrivateFlags 添加了一個(gè)標(biāo)志位,雖然還不清楚干嘛的,但可以先留個(gè)心眼,因?yàn)?mPrivateFlags 這個(gè)變量在閱讀跟 View 相關(guān)的源碼時(shí)經(jīng)常碰到,那么可以的話能搞明白就搞明白,但目前跟我們想要找出動(dòng)畫到底什么時(shí)候開始執(zhí)行的關(guān)系好像不大,先略過,繼續(xù)跟進(jìn)?invalidate():
所以?invalidate()?內(nèi)部其實(shí)是調(diào)用了 ViewGroup 的?invalidateChild(),再跟進(jìn)看看:
這里有一個(gè) do{}while() 的循環(huán)操作,第一次循環(huán)的時(shí)候 parent 是 this,即 ViewGroup 本身,所以接下去就是調(diào)用 ViewGroup 本身的?invalidateChildInParent()?方法,然后循環(huán)終止條件是 patent == null,所以可以猜測這個(gè)方法返回的應(yīng)該是 ViewGroup 的 parent,跟進(jìn)看看:
所以關(guān)鍵是 PFLAG_DRAWN 和 PFLAG_DRAWING_CACHE_VALID 這兩個(gè)是什么時(shí)候賦值給 mPrivateFlags,因?yàn)橹灰袃蓚€(gè)標(biāo)志中的一個(gè)時(shí),該方法就會(huì)返回 mParent,具體賦值的地方還不大清楚,但能確定的是動(dòng)畫執(zhí)行時(shí),它是滿足 if 條件的,也就是這個(gè)方法會(huì)返回 mParent。
一個(gè)具體的 View 的 mParent 是 ViewGroup,ViewGroup 的 mParent 也是 ViewGoup,所以在 do{}while() 循環(huán)里會(huì)一直不斷的尋找 mParent,而一顆 View 樹最頂端的 mParent 是 ViewRootImpl,所以最終是會(huì)走到了 ViewRootImpl 的?invalidateChildInParent()?里去了。
至于一個(gè)界面的 View 樹最頂端為什么是 ViewRootImpl,這個(gè)就跟 Activity 啟動(dòng)過程有關(guān)了。我們都清楚,在 onCreate 里 setContentView() 的時(shí)候,是將我們自己寫的布局文件添加到以 DecorView 為根布局的一個(gè) ViewGroup 里,也就是說 DevorView 才是 View 樹的根布局,那為什么又說 View 樹最頂端其實(shí)是 ViewRootImpl 呢?
這是因?yàn)樵?onResume()?執(zhí)行完后,WindowManager 將會(huì)執(zhí)行?addView(),然后在這里面會(huì)去創(chuàng)建一個(gè) ViewRootImpl 對象,接著將 DecorView 跟 ViewRootImpl 對象綁定起來,并且將 DecorView 的 mParent 設(shè)置成 ViewRootImpl,而 ViewRootImpl 是實(shí)現(xiàn)了 ViewParent 接口的,所以雖然 ViewRootImpl 沒有繼承 View 或 ViewGroup,但它確實(shí)是 DecorView 的 parent。這部分內(nèi)容應(yīng)該屬于 Activity 的啟動(dòng)過程相關(guān)原理的,所以本篇只給出結(jié)論,不深入分析了,感興趣的可以自行搜索一下。
那么我們繼續(xù)返回到尋找動(dòng)畫執(zhí)行的地方,我們跟到了 ViewRootImpl 的?invalidateChildInParent()?里去了,看看它做了些什么:
首先第一點(diǎn),它的所有返回值都是 null,所以之前那個(gè) do{}while() 循環(huán)最終就是執(zhí)行到這里后肯定就會(huì)停止了。然后參數(shù) dirty 是在最初 View 的?invalidateInternal()?里層層傳遞過來的,可以肯定的是它不為空,也不是 isEmpty,所以繼續(xù)跟到?invalidateRectOnScreen()?方法里看看:
跟到這里就可以了,scheduleTraversals()?作用是將?performTraversals()?封裝到一個(gè) Runnable 里面,然后扔到 Choreographer 的待執(zhí)行隊(duì)列里,這些待執(zhí)行的 Runnable 將會(huì)在最近的一個(gè) 16.6 ms 屏幕刷新信號(hào)到來的時(shí)候被執(zhí)行。而?performTraversals()?是 View 的三大操作:測量、布局、繪制的發(fā)起者。
View 樹里面不管哪個(gè) View 發(fā)起了布局請求、繪制請求,統(tǒng)統(tǒng)最終都會(huì)走到 ViewRootImpl 里的 scheduleTraversals(),然后在最近的一個(gè)屏幕刷新信號(hào)到了的時(shí)候再通過 ViewRootImpl 的 performTraversals() 從根布局 DecorView 開始依次遍歷 View 樹去執(zhí)行測量、布局、繪制三大操作。這也是為什么一直要求頁面布局層次不能太深,因?yàn)槊恳淮蔚捻撁嫠⑿露紩?huì)先走到 ViewRootImpl 里,然后再層層遍歷到具體發(fā)生改變的 View 里去執(zhí)行相應(yīng)的布局或繪制操作。
這些內(nèi)容應(yīng)該是屬于 Android 屏幕刷新機(jī)制的,這里就先只給出結(jié)論,具體分析我會(huì)在幾天后再發(fā)一篇博客出來。
所以,我們從?View.startAnimation()?開始跟進(jìn)源碼分析的這一過程中,也可以看出,執(zhí)行動(dòng)畫,其實(shí)內(nèi)部會(huì)調(diào)用 View 的重繪請求操作?invalidate()?,所以最終會(huì)走到 ViewRootImpl 的?scheduleTraversals(),然后在下一個(gè)屏幕刷新信號(hào)到的時(shí)候去遍歷 View 樹刷新屏幕。
所以,到這里可以得到的結(jié)論是:
當(dāng)調(diào)用了 View.startAniamtion() 之后,動(dòng)畫并沒有馬上就被執(zhí)行,這個(gè)方法只是做了一些變量初始化操作,接著將 View 和 Animation 綁定起來,然后調(diào)用重繪請求操作,內(nèi)部層層尋找 mParent,最終走到 ViewRootImpl 的 scheduleTraversals 里發(fā)起一個(gè)遍歷 View 樹的請求,這個(gè)請求會(huì)在最近的一個(gè)屏幕刷新信號(hào)到來的時(shí)候被執(zhí)行,調(diào)用 performTraversals 從根布局 DecorView 開始遍歷 View 樹。
動(dòng)畫真正執(zhí)行的地方
那么,到這里,我們可以猜測,動(dòng)畫其實(shí)真正執(zhí)行的地方應(yīng)該是在 ViewRootImpl 發(fā)起的遍歷 View 樹的這個(gè)過程中。測量、布局、繪制,View 顯示到屏幕上的三個(gè)基本操作都是由 ViewRootImpl 的?performTraversals()來控制,而作為 View 樹最頂端的 parent,要控制這顆 Veiw 樹的三個(gè)基本操作,只能通過層層遍歷。所以,測量、布局、繪制三個(gè)基本操作的執(zhí)行都會(huì)是一次遍歷操作。
我在跟著這三個(gè)流程走的時(shí)候,最后發(fā)現(xiàn),在跟著繪制流程走的時(shí)候,看到了跟動(dòng)畫相關(guān)的代碼,所以我們就跳過其他兩個(gè)流程,直接看繪制流程:
這張圖不是我畫的,在網(wǎng)上找的,繪制流程的開始是由 ViewRootImpl 發(fā)起的,然后從 DecorView 開始遍歷 View 樹。而遍歷的實(shí)現(xiàn),是在 View#draw() 方法里的。我們可以看看這個(gè)方法的注釋:
這個(gè)方法里主要做了上述六件事,大體上就是如果當(dāng)前 View 需要繪制,就會(huì)去調(diào)用自己的?onDraw(),然后如果有子 View,就會(huì)調(diào)用dispatchDraw()?將繪制事件通知給子 View。ViewGroup 重寫了?dispatchDraw(),調(diào)用了?drawChild(),而?drawChild()?調(diào)用了子 View 的?draw(Canvas, ViewGroup, long),而這個(gè)方法又會(huì)去調(diào)用到?draw(Canvas)?方法,所以這樣就達(dá)到了遍歷的效果。整個(gè)流程就像上上圖中畫的那樣。
在這個(gè)流程中,當(dāng)跟到?draw(Canvas, ViewGroup, long)?里時(shí),發(fā)現(xiàn)了跟動(dòng)畫相關(guān)的代碼:
還記得我們調(diào)用?View.startAnimation(Animation)?時(shí)將傳進(jìn)來的 Animation 賦值給 mCurrentAnimation 了么。
所以當(dāng)時(shí)傳進(jìn)來的 Animation ,現(xiàn)在拿出來用了,那么動(dòng)畫真正執(zhí)行的地方應(yīng)該也就是在?applyLegacyAnimation()?方法里了(該方法在 android-22 版本及之前的命名是 drawAnimation)
這下確定動(dòng)畫真正開始執(zhí)行是在什么地方了吧,都看到?onAnimationStart()?了,也看到了對動(dòng)畫進(jìn)行初始化,以及調(diào)用了 Animation 的?getTransformation,這個(gè)方法是動(dòng)畫的核心,再跟進(jìn)去看看:
這個(gè)方法里做了幾件事:
記錄動(dòng)畫第一幀的時(shí)間
根據(jù)當(dāng)前時(shí)間到動(dòng)畫第一幀的時(shí)間這之間的時(shí)長和動(dòng)畫應(yīng)持續(xù)的時(shí)長來計(jì)算動(dòng)畫的進(jìn)度
把動(dòng)畫進(jìn)度控制在 0-1 之間,超過 1 的表示動(dòng)畫已經(jīng)結(jié)束,重新賦值為 1 即可
根據(jù)插值器來計(jì)算動(dòng)畫的實(shí)際進(jìn)度
調(diào)用 applyTransformation() 應(yīng)用動(dòng)畫效果
所以,到這里我們已經(jīng)能確定?applyTransformation()?是什么時(shí)候回調(diào)的,動(dòng)畫是什么時(shí)候才真正開始執(zhí)行的。那么 Q1 總算是搞定了,Q2 也基本能理清了。因?yàn)槲覀兦宄?applyTransformation()?最終是在繪制流程中的?draw()?過程中執(zhí)行到的,那么顯然在每一幀的屏幕刷新信號(hào)來的時(shí)候,遍歷 View 樹是為了重新計(jì)算屏幕數(shù)據(jù),也就是所謂的 View 的刷新,而動(dòng)畫只是在這個(gè)過程中順便執(zhí)行的。
接下去就是 Q3 了,我們知道?applyTransformation()?是動(dòng)畫生效的地方,這個(gè)方法不斷的被回調(diào)時(shí),參數(shù)會(huì)傳進(jìn)來動(dòng)畫的進(jìn)度,所以呈現(xiàn)效果就是動(dòng)畫根據(jù)進(jìn)度在運(yùn)行中。
但是,我們從頭分析下來,找到了動(dòng)畫真正執(zhí)行的地方,找到了 applyTransformation() 被調(diào)用的地方,但這些地方都沒有看到任何一個(gè) for 或者 while 循環(huán)啊,也就是一次 View 樹的遍歷繪制操作,動(dòng)畫也就只會(huì)執(zhí)行一次而已啊?那么它是怎么被回調(diào)那么多次的?
我們知道?applyTransformation()?是在?getTransformation()?里被調(diào)用的,而這個(gè)方法是有一個(gè) boolean 返回值的,我們看看它的返回邏輯是什么:
也就是說?getTransformation()?的返回值代表的是動(dòng)畫是否完成,還記得是哪里調(diào)用的?getTransformation()?吧,去?applyLegacyAnimation()?里看看取到這個(gè)返回值后又做了什么:
當(dāng)動(dòng)畫如果還沒執(zhí)行完,就會(huì)再調(diào)用?invalidate()?方法,層層通知到 ViewRootImpl 再次發(fā)起一次遍歷請求,當(dāng)下一幀屏幕刷新信號(hào)來的時(shí)候,再通過?performTraversals()?遍歷 View 樹繪制時(shí),該 View 的 draw 收到通知被調(diào)用時(shí),會(huì)再次去調(diào)用?applyLegacyAnimation()?方法去執(zhí)行動(dòng)畫相關(guān)操作,包括調(diào)用?getTransformation()?計(jì)算動(dòng)畫進(jìn)度,調(diào)用?applyTransformation()?應(yīng)用動(dòng)畫。
也就是說,動(dòng)畫很流暢的情況下,其實(shí)是每隔 16.6ms 即每一幀到來的時(shí)候,執(zhí)行一次?applyTransformation(),直到動(dòng)畫完成。所以這個(gè)?applyTransformation()?被回調(diào)多次是這么來的,而且這個(gè)回調(diào)次數(shù)并沒有辦法人為進(jìn)行設(shè)定。
這就是為什么當(dāng)動(dòng)畫持續(xù)時(shí)長越長時(shí),這個(gè)方法打出的日志越多次的原因。
還記得?getTransformation()?方法在計(jì)算動(dòng)畫進(jìn)度時(shí)是根據(jù)參數(shù)傳進(jìn)來的 currentTime 的么,而這個(gè) currentTime 可以理解成是發(fā)起遍歷操作這個(gè)時(shí)刻的系統(tǒng)時(shí)間(實(shí)際 currentTime 是在 Choreographer 的 doFrame() 里經(jīng)過校驗(yàn)調(diào)整之后的一個(gè)時(shí)間,但離發(fā)起遍歷操作這個(gè)時(shí)刻的系統(tǒng)時(shí)間相差很小,所以不深究的話,可以像上面那樣理解,比較容易明白)。
小結(jié)
綜上,我們稍微整理一下:
首先,當(dāng)調(diào)用了 View.startAnimation() 時(shí)動(dòng)畫并沒有馬上就執(zhí)行,而是通過 invalidate() 層層通知到 ViewRootImpl 發(fā)起一次遍歷 View 樹的請求,而這次請求會(huì)等到接收到最近一幀到了的信號(hào)時(shí)才去發(fā)起遍歷 View 樹繪制操作。
從 DecorView 開始遍歷,繪制流程在遍歷時(shí)會(huì)調(diào)用到 View 的 draw() 方法,當(dāng)該方法被調(diào)用時(shí),如果 View 有綁定動(dòng)畫,那么會(huì)去調(diào)用applyLegacyAnimation(),這個(gè)方法是專門用來處理動(dòng)畫相關(guān)邏輯的。
在 applyLegacyAnimation() 這個(gè)方法里,如果動(dòng)畫還沒有執(zhí)行過初始化,先調(diào)用動(dòng)畫的初始化方法 initialized(),同時(shí)調(diào)用 onAnimationStart() 通知?jiǎng)赢嬮_始了,然后調(diào)用 getTransformation() 來根據(jù)當(dāng)前時(shí)間計(jì)算動(dòng)畫進(jìn)度,緊接著調(diào)用 applyTransformation() 并傳入動(dòng)畫進(jìn)度來應(yīng)用動(dòng)畫。
getTransformation() 這個(gè)方法有返回值,如果動(dòng)畫還沒結(jié)束會(huì)返回 true,動(dòng)畫已經(jīng)結(jié)束或者被取消了返回 false。所以 applyLegacyAnimation() 會(huì)根據(jù) getTransformation() 的返回值來決定是否通知 ViewRootImpl 再發(fā)起一次遍歷請求,返回值是 true 表示動(dòng)畫沒結(jié)束,那么就去通知 ViewRootImpl 再次發(fā)起一次遍歷請求。然后當(dāng)下一幀到來時(shí),再從 DecorView 開始遍歷 View 樹繪制,重復(fù)上面的步驟,這樣直到動(dòng)畫結(jié)束。
有一點(diǎn)需要注意,動(dòng)畫是在每一幀的繪制流程里被執(zhí)行,所以動(dòng)畫并不是單獨(dú)執(zhí)行的,也就是說,如果這一幀里有一些 View 需要重繪,那么這些工作同樣是在這一幀里的這次遍歷 View 樹的過程中完成的。每一幀只會(huì)發(fā)起一次 perfromTraversals() 操作。
以上,就是本篇所有的內(nèi)容,將 View 動(dòng)畫 Animation 的運(yùn)行流程原理梳理清楚,但要搞清楚為什么動(dòng)畫會(huì)出現(xiàn)卡頓現(xiàn)象的話,還需要理解 Android 屏幕的刷新機(jī)制以及消息驅(qū)動(dòng)機(jī)制;這些內(nèi)容將在最近幾天內(nèi)整理成博客分享出來。
遺留問題
最后仍然遺留一些尚未解決的問題,等待繼續(xù)探索:
Q1:大伙都清楚,View 動(dòng)畫區(qū)別于屬性動(dòng)畫的就是 View 動(dòng)畫并不會(huì)對這個(gè) View 的屬性值做修改,比如平移動(dòng)畫,平移之后 View 還是在原來的位置上,實(shí)際位置并不會(huì)隨動(dòng)畫的執(zhí)行而移動(dòng),那么這點(diǎn)的原理是什么?
Q2:既然 View 動(dòng)畫不會(huì)改變 View 的屬性值,那么如果是縮放動(dòng)畫時(shí),View 需要重新執(zhí)行測量操作么?