View 動畫 Animation 運行原理解析


本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布

這次想來梳理一下 View 動畫也就是補間動畫(ScaleAnimation, AlphaAnimation, TranslationAnimation...)這些動畫運行的流程解析。內容並不會去分析動畫的呈現原理是什么,諸如 Matrix 這類的原理是什么,因為我也還沒搞懂。本篇主要是分析當調用了 View.startAnimation() 之后,動畫從開始到結束的一個運行流程是什么?

提問環節

看源碼最好是帶着問題去,這樣比較有目的性和針對性,可以防止閱讀源碼時走偏和鑽牛角,所以我們就先來提幾個問題。

Animation 動畫的擴展性很高,系統只是簡單的為我們封裝了幾個基本的動畫:平移、旋轉、透明度、縮放等等,感興趣的可以去看看這幾個動畫的源碼,它們都是繼承自 Animation 類,然后實現了 applyTransformation() 方法,在這個方法里通過 Transformation 和 Matrix 實現各種各樣炫酷的動畫,所以,如果想要做出炫酷的動畫效果,這些還是需要去搞懂的。

目前我也還沒搞懂,能力有限,所以優先分析動畫的一個運行流程。

首先看看 Animation 動畫的基本用法:
基本用法

我們要使用一個 View 動畫時,一般都是先 new 一個動畫,然后配置各種參數,最后調用動畫要作用到的那個 View 的 startAnimation(), 將動畫實例作為參數傳進去,接下去就可以看到動畫運行的效果了。

那么,問題來了:

Q1:不知道大伙想過沒有,當調用了 View.startAnimation() 之后,動畫是馬上就執行了么?

Q2:假如動畫持續時間 300ms,當調用了 View.startAniamtion() 之后,又發起了一次界面刷新的操作,那么界面的刷新是在 300ms 之后也就是動畫執行完畢之后才執行的,還是在動畫執行過程中界面刷新操作就執行了呢?

我們都知道,applyTransformation() 這個方法是動畫生效的地方,這個方法被回調時參數會傳進來當前動畫的進度(0.0 ——— 1.0)。就像數學上的畫曲線,當給的點越多時畫的曲線越光滑,同樣當這個方法被回調越多次時,動畫的效果越流暢。

比如一個從 0 放大到 1280 的 View 放大動畫,如果這過程該方法只回調 3 次的話,那么每次的跨度就會很大,比如 0 —— 600 —— 1280,那么這個動畫效果看起來就會很突兀;相反,如果這過程該方法回調了幾十次的話,那么每次跨度可能就只有 100,這樣一來動畫效果看起來就會很流暢。

相信大伙也都有過在 applyTransformation() 里打日志來查看當前的動畫進度,有時打出的日志有十幾條,有時卻又有幾十條。

那么我們的問題就來了:

Q3:applyTransformation() 這個方法的回調次數是根據什么來決定的?

好了,本篇就是主要講解這三個問題,這三個問題搞明白的話,以后碰到動畫卡頓的時候就懂得如何去分析、定位丟幀的地方了,找到丟幀的問題所在后離解決問題也就不遠了。

源碼分析

ps:本篇分析的源碼全都基於 android-25 版本。以下源碼均采用截圖方式,每張圖最上面是類名+方法名,大伙想自己過一遍的時候,如果不清楚方法屬於哪個類的可以在每張圖最上面查看。

View.startAnimation()

剛開始接觸源碼分析可能不清楚該從哪入手,建議可以從我們使用它的地方來 startAnimation()
startAnimation.png

代碼不多,調用了四個方法,那么一個個跟進去看看,先是 setStartTime()
setStartTime.png

所以這里只是對一些變量進行賦值,並沒有運行動畫的邏輯,繼續看看 setAnimation()
setAnimation.png

View 里面有一個 Animation 類型的成員變量,所以這個方法其實是將我們 new 的 ScaleAnimation 動畫跟 View 綁定起來而已,也沒有運行動畫的邏輯,繼續往下看看 invalidateParentCached()
invalidateParentCached.png

invalidateParentCaches() 這方法更簡單,給 mPrivateFlags 添加了一個標志位,雖然還不清楚干嘛的,但可以先留個心眼,因為 mPrivateFlags 這個變量在閱讀跟 View 相關的源碼時經常碰到,那么可以的話能搞明白就搞明白,但目前跟我們想要找出動畫到底什么時候開始執行的關系好像不大,先略過,繼續跟進 invalidate()
invalidateInternal.png

所以 invalidate() 內部其實是調用了 ViewGroup 的 invalidateChild(),再跟進看看:
invalidateChild.png

這里有一個 do{}while() 的循環操作,第一次循環的時候 parent 是 this,即 ViewGroup 本身,所以接下去就是調用 ViewGroup 本身的 invalidateChildInParent() 方法,然后循環終止條件是 patent == null,所以可以猜測這個方法返回的應該是 ViewGroup 的 parent,跟進看看:
invalidateChildInparent.png

所以關鍵是 PFLAG_DRAWN 和 PFLAG_DRAWING_CACHE_VALID 這兩個是什么時候賦值給 mPrivateFlags,因為只要有兩個標志中的一個時,該方法就會返回 mParent,具體賦值的地方還不大清楚,但能確定的是動畫執行時,它是滿足 if 條件的,也就是這個方法會返回 mParent。

一個具體的 View 的 mParent 是 ViewGroup,ViewGroup 的 mParent 也是 ViewGoup,所以在 do{}while() 循環里會一直不斷的尋找 mParent,而一顆 View 樹最頂端的 mParent 是 ViewRootImpl,所以最終是會走到了 ViewRootImpl 的 invalidateChildInParent() 里去了。

至於一個界面的 View 樹最頂端為什么是 ViewRootImpl,這個就跟 Activity 啟動過程有關了。我們都清楚,在 onCreate 里 setContentView() 的時候,是將我們自己寫的布局文件添加到以 DecorView 為根布局的一個 ViewGroup 里,也就是說 DevorView 才是 View 樹的根布局,那為什么又說 View 樹最頂端其實是 ViewRootImpl 呢?

這是因為在 onResume() 執行完后,WindowManager 將會執行 addView(),然后在這里面會去創建一個 ViewRootImpl 對象,接着將 DecorView 跟 ViewRootImpl 對象綁定起來,並且將 DecorView 的 mParent 設置成 ViewRootImpl,而 ViewRootImpl 是實現了 ViewParent 接口的,所以雖然 ViewRootImpl 沒有繼承 View 或 ViewGroup,但它確實是 DecorView 的 parent。這部分內容應該屬於 Activity 的啟動過程相關原理的,所以本篇只給出結論,不深入分析了,感興趣的可以自行搜索一下。

那么我們繼續返回到尋找動畫執行的地方,我們跟到了 ViewRootImpl 的 invalidateChildInParent() 里去了,看看它做了些什么:
ViewRootImpl#invalidateChildInParent.png

首先第一點,它的所有返回值都是 null,所以之前那個 do{}while() 循環最終就是執行到這里后肯定就會停止了。然后參數 dirty 是在最初 View 的 invalidateInternal() 里層層傳遞過來的,可以肯定的是它不為空,也不是 isEmpty,所以繼續跟到 invalidateRectOnScreen() 方法里看看:
invalidateRectOnScreen.png

跟到這里就可以了,scheduleTraversals() 作用是將 performTraversals() 封裝到一個 Runnable 里面,然后扔到 Choreographer 的待執行隊列里,這些待執行的 Runnable 將會在最近的一個 16.6 ms 屏幕刷新信號到來的時候被執行。而 performTraversals() 是 View 的三大操作:測量、布局、繪制的發起者。

View 樹里面不管哪個 View 發起了布局請求、繪制請求,統統最終都會走到 ViewRootImpl 里的 scheduleTraversals(),然后在最近的一個屏幕刷新信號到了的時候再通過 ViewRootImpl 的 performTraversals() 從根布局 DecorView 開始依次遍歷 View 樹去執行測量、布局、繪制三大操作。這也是為什么一直要求頁面布局層次不能太深,因為每一次的頁面刷新都會先走到 ViewRootImpl 里,然后再層層遍歷到具體發生改變的 View 里去執行相應的布局或繪制操作。

這些內容應該是屬於 Android 屏幕刷新機制的,這里就先只給出結論,具體分析我會在幾天后再發一篇博客出來。

所以,我們從 View.startAnimation() 開始跟進源碼分析的這一過程中,也可以看出,執行動畫,其實內部會調用 View 的重繪請求操作 invalidate() ,所以最終會走到 ViewRootImpl 的 scheduleTraversals(),然后在下一個屏幕刷新信號到的時候去遍歷 View 樹刷新屏幕。

所以,到這里可以得到的結論是:

當調用了 View.startAniamtion() 之后,動畫並沒有馬上就被執行,這個方法只是做了一些變量初始化操作,接着將 View 和 Animation 綁定起來,然后調用重繪請求操作,內部層層尋找 mParent,最終走到 ViewRootImpl 的 scheduleTraversals 里發起一個遍歷 View 樹的請求,這個請求會在最近的一個屏幕刷新信號到來的時候被執行,調用 performTraversals 從根布局 DecorView 開始遍歷 View 樹。

動畫真正執行的地方

那么,到這里,我們可以猜測,動畫其實真正執行的地方應該是在 ViewRootImpl 發起的遍歷 View 樹的這個過程中。測量、布局、繪制,View 顯示到屏幕上的三個基本操作都是由 ViewRootImpl 的 performTraversals() 來控制,而作為 View 樹最頂端的 parent,要控制這顆 Veiw 樹的三個基本操作,只能通過層層遍歷。所以,測量、布局、繪制三個基本操作的執行都會是一次遍歷操作。

我在跟着這三個流程走的時候,最后發現,在跟着繪制流程走的時候,看到了跟動畫相關的代碼,所以我們就跳過其他兩個流程,直接看繪制流程:

繪制流程.png

這張圖不是我畫的,在網上找的,繪制流程的開始是由 ViewRootImpl 發起的,然后從 DecorView 開始遍歷 View 樹。而遍歷的實現,是在 View#draw() 方法里的。我們可以看看這個方法的注釋:
draw.png

這個方法里主要做了上述六件事,大體上就是如果當前 View 需要繪制,就會去調用自己的 onDraw(),然后如果有子 View,就會調用dispatchDraw() 將繪制事件通知給子 View。ViewGroup 重寫了 dispatchDraw(),調用了 drawChild(),而 drawChild() 調用了子 View 的 draw(Canvas, ViewGroup, long),而這個方法又會去調用到 draw(Canvas) 方法,所以這樣就達到了遍歷的效果。整個流程就像上上圖中畫的那樣。

在這個流程中,當跟到 draw(Canvas, ViewGroup, long) 里時,發現了跟動畫相關的代碼:
draw2.png

還記得我們調用 View.startAnimation(Animation) 時將傳進來的 Animation 賦值給 mCurrentAnimation 了么。
getAnimation.png

所以當時傳進來的 Animation ,現在拿出來用了,那么動畫真正執行的地方應該也就是在 applyLegacyAnimation() 方法里了(該方法在 android-22 版本及之前的命名是 drawAnimation)
applyLegacyAnimation.png

這下確定動畫真正開始執行是在什么地方了吧,都看到 onAnimationStart() 了,也看到了對動畫進行初始化,以及調用了 Animation 的 getTransformation,這個方法是動畫的核心,再跟進去看看:
getTransformation.png

這個方法里做了幾件事:

  1. 記錄動畫第一幀的時間
  2. 根據當前時間到動畫第一幀的時間這之間的時長和動畫應持續的時長來計算動畫的進度
  3. 把動畫進度控制在 0-1 之間,超過 1 的表示動畫已經結束,重新賦值為 1 即可
  4. 根據插值器來計算動畫的實際進度
  5. 調用 applyTransformation() 應用動畫效果

所以,到這里我們已經能確定 applyTransformation() 是什么時候回調的,動畫是什么時候才真正開始執行的。那么 Q1 總算是搞定了,Q2 也基本能理清了。因為我們清楚, applyTransformation() 最終是在繪制流程中的 draw() 過程中執行到的,那么顯然在每一幀的屏幕刷新信號來的時候,遍歷 View 樹是為了重新計算屏幕數據,也就是所謂的 View 的刷新,而動畫只是在這個過程中順便執行的。

接下去就是 Q3 了,我們知道 applyTransformation() 是動畫生效的地方,這個方法不斷的被回調時,參數會傳進來動畫的進度,所以呈現效果就是動畫根據進度在運行中。

但是,我們從頭分析下來,找到了動畫真正執行的地方,找到了 applyTransformation() 被調用的地方,但這些地方都沒有看到任何一個 for 或者 while 循環啊,也就是一次 View 樹的遍歷繪制操作,動畫也就只會執行一次而已啊?那么它是怎么被回調那么多次的?

我們知道 applyTransformation() 是在 getTransformation() 里被調用的,而這個方法是有一個 boolean 返回值的,我們看看它的返回邏輯是什么:
getTransformation2.png

也就是說 getTransformation() 的返回值代表的是動畫是否完成,還記得是哪里調用的 getTransformation() 吧,去 applyLegacyAnimation() 里看看取到這個返回值后又做了什么:
applyLegacyAnimation2.png

當動畫如果還沒執行完,就會再調用 invalidate() 方法,層層通知到 ViewRootImpl 再次發起一次遍歷請求,當下一幀屏幕刷新信號來的時候,再通過 performTraversals() 遍歷 View 樹繪制時,該 View 的 draw 收到通知被調用時,會再次去調用 applyLegacyAnimation() 方法去執行動畫相關操作,包括調用 getTransformation() 計算動畫進度,調用 applyTransformation() 應用動畫。

也就是說,動畫很流暢的情況下,其實是每隔 16.6ms 即每一幀到來的時候,執行一次 applyTransformation(),直到動畫完成。所以這個 applyTransformation() 被回調多次是這么來的,而且這個回調次數並沒有辦法人為進行設定。

這就是為什么當動畫持續時長越長時,這個方法打出的日志越多次的原因。

還記得 getTransformation() 方法在計算動畫進度時是根據參數傳進來的 currentTime 的么,而這個 currentTime 可以理解成是發起遍歷操作這個時刻的系統時間(實際 currentTime 是在 Choreographer 的 doFrame() 里經過校驗調整之后的一個時間,但離發起遍歷操作這個時刻的系統時間相差很小,所以不深究的話,可以像上面那樣理解,比較容易明白)。

小結

綜上,我們稍微整理一下:

  1. 首先,當調用了 View.startAnimation() 時動畫並沒有馬上就執行,而是通過 invalidate() 層層通知到 ViewRootImpl 發起一次遍歷 View 樹的請求,而這次請求會等到接收到最近一幀到了的信號時才去發起遍歷 View 樹繪制操作。

  2. 從 DecorView 開始遍歷,繪制流程在遍歷時會調用到 View 的 draw() 方法,當該方法被調用時,如果 View 有綁定動畫,那么會去調用applyLegacyAnimation(),這個方法是專門用來處理動畫相關邏輯的。

  3. 在 applyLegacyAnimation() 這個方法里,如果動畫還沒有執行過初始化,先調用動畫的初始化方法 initialized(),同時調用 onAnimationStart() 通知動畫開始了,然后調用 getTransformation() 來根據當前時間計算動畫進度,緊接着調用 applyTransformation() 並傳入動畫進度來應用動畫。

  4. getTransformation() 這個方法有返回值,如果動畫還沒結束會返回 true,動畫已經結束或者被取消了返回 false。所以 applyLegacyAnimation() 會根據 getTransformation() 的返回值來決定是否通知 ViewRootImpl 再發起一次遍歷請求,返回值是 true 表示動畫沒結束,那么就去通知 ViewRootImpl 再次發起一次遍歷請求。然后當下一幀到來時,再從 DecorView 開始遍歷 View 樹繪制,重復上面的步驟,這樣直到動畫結束。

  5. 有一點需要注意,動畫是在每一幀的繪制流程里被執行,所以動畫並不是單獨執行的,也就是說,如果這一幀里有一些 View 需要重繪,那么這些工作同樣是在這一幀里的這次遍歷 View 樹的過程中完成的。每一幀只會發起一次 perfromTraversals() 操作。

以上,就是本篇所有的內容,將 View 動畫 Animation 的運行流程原理梳理清楚,但要搞清楚為什么動畫會出現卡頓現象的話,還需要理解 Android 屏幕的刷新機制以及消息驅動機制;這些內容將在最近幾天內整理成博客分享出來。

遺留問題

最后仍然遺留一些尚未解決的問題,等待繼續探索:

Q1:大伙都清楚,View 動畫區別於屬性動畫的就是 View 動畫並不會對這個 View 的屬性值做修改,比如平移動畫,平移之后 View 還是在原來的位置上,實際位置並不會隨動畫的執行而移動,那么這點的原理是什么?

Q2:既然 View 動畫不會改變 View 的屬性值,那么如果是縮放動畫時,View 需要重新執行測量操作么?


QQ圖片20180316094923.jpg
最近剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支持~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM