天天看點

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()

代碼不多,調用了四個方法,那麼一個個跟進去看看,先是

setStartTime()

是以這裡隻是對一些變量進行指派,并沒有運作動畫的邏輯,繼續看看

setAnimation()

View 裡面有一個 Animation 類型的成員變量,是以這個方法其實是将我們 new 的 ScaleAnimation 動畫跟 View 綁定起來而已,也沒有運作動畫的邏輯,繼續往下看看

invalidateParentCached()

invalidateParentCaches()

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

invalidate()

是以

invalidate()

内部其實是調用了 ViewGroup 的

invalidateChild()

,再跟進看看:

這裡有一個 do{}while() 的循環操作,第一次循環的時候 parent 是 this,即 ViewGroup 本身,是以接下去就是調用 ViewGroup 本身的

invalidateChildInParent()

方法,然後循環終止條件是 patent == null,是以可以猜測這個方法傳回的應該是 ViewGroup 的 parent,跟進看看:

是以關鍵是 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()

裡去了,看看它做了些什麼:

首先第一點,它的所有傳回值都是 null,是以之前那個 do{}while() 循環最終就是執行到這裡後肯定就會停止了。然後參數 dirty 是在最初 View 的

invalidateInternal()

裡層層傳遞過來的,可以肯定的是它不為空,也不是 isEmpty,是以繼續跟到

invalidateRectOnScreen()

方法裡看看:

跟到這裡就可以了,

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 樹的三個基本操作,隻能通過層層周遊。是以,測量、布局、繪制三個基本操作的執行都會是一次周遊操作。

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

這張圖不是我畫的,在網上找的,繪制流程的開始是由 ViewRootImpl 發起的,然後從 DecorView 開始周遊 View 樹。而周遊的實作,是在 View#draw() 方法裡的。我們可以看看這個方法的注釋:

這個方法裡主要做了上述六件事,大體上就是如果目前 View 需要繪制,就會去調用自己的

onDraw()

,然後如果有子 View,就會調用

dispatchDraw()

将繪制事件通知給子 View。ViewGroup 重寫了

dispatchDraw()

,調用了

drawChild()

,而

drawChild()

調用了子 View 的

draw(Canvas, ViewGroup, long)

,而這個方法又會去調用到

draw(Canvas)

方法,是以這樣就達到了周遊的效果。整個流程就像上上圖中畫的那樣。

在這個流程中,當跟到

draw(Canvas, ViewGroup, long)

裡時,發現了跟動畫相關的代碼:

還記得我們調用

View.startAnimation(Animation)

時将傳進來的 Animation 指派給 mCurrentAnimation 了麼。

是以當時傳進來的 Animation ,現在拿出來用了,那麼動畫真正執行的地方應該也就是在

applyLegacyAnimation()

方法裡了(該方法在 android-22 版本及之前的命名是 drawAnimation)

這下确定動畫真正開始執行是在什麼地方了吧,都看到

onAnimationStart()

了,也看到了對動畫進行初始化,以及調用了 Animation 的

getTransformation

,這個方法是動畫的核心,再跟進去看看:

這個方法裡做了幾件事:

  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 傳回值的,我們看看它的傳回邏輯是什麼:

也就是說

getTransformation()

的傳回值代表的是動畫是否完成,還記得是哪裡調用的

getTransformation()

吧,去

applyLegacyAnimation()

裡看看取到這個傳回值後又做了什麼:

當動畫如果還沒執行完,就會再調用

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 需要重新執行測量操作麼?

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