1. 程式人生 > >Android動畫Animation執行原理解析

Android動畫Animation執行原理解析

640?wx_fmt=png&wxfrom=5&wx_lazy=1

今日科技快訊

雖然三星釋出了新款旗艦手機S9,但仍難以停止股價的跌勢。從去年底到目前,三星的股價暴跌兩成,損失了600億美元的市值。許多券商給予三星股票評級已降至“持有”( hold),而管理著3710億美元投資基金的亨德森全球投資者公司甚至已經減持了手中的三星股票。

作者簡介

元宵節碰上週五,多麼開心的一天。今天祝所有人元宵節快樂!同時也提前祝大家週末愉快。

本篇來自 請叫我大蘇 的投稿,結合原始碼分析了動畫的執行過程,希望對大家有所幫助。

請叫我大蘇 的部落格地址:

https://www.jianshu.com/u/bb52a2918096

前言

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

提問環節

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

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

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

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

//1.new Animation
ScaleAnimation animation = new ScaleAnimation(0, 1, 0, 1);
//2.設定 Animation 各種引數配置
animation.setDuration(300);
animation.setFillAfter(true);
//3.啟動動畫
view.startAnimation(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()

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

//View#startAnimation()
public void startAnimation(Animation animation) {
   animation.setStartTime(Animation.START_ON_FIRST_FRAME);
   setAnimation(animation);
   invalidateParentCaches();
   invalidate(true);
}

程式碼不多,呼叫了四個方法,那麼一個個跟進去看看,先是 setStartTime() :

//Animation#setStartTime()
public void setStartTime(long startTimeMillis) {
   mStartTime = startTimeMillis;
   mStarted = mEnded = false;
   mCycleFlip = false;
   mRepeated = 0;
   mMore = true;
}

所以這裡只是對一些變數進行賦值,並沒有執行動畫的邏輯,繼續看看 setAnimation():

//View#setAnimation()
public void setAnimation(Animation animation) {
   mCurrentAnimation = animation;
   ...
}

View 裡面有一個 Animation 型別的成員變數,所以這個方法其實是將我們 new 的 ScaleAnimation 動畫跟 View 繫結起來而已,也沒有執行動畫的邏輯,繼續往下看看 invalidateParentCached():

//View#invalidateParentCaches()
protected void invalidateParentCaches() {
   if (mParent instanceof View) {
       ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
   }
}

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

//View#invalidate()
void invalidate(boolean invalidateCache) {
   invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

//View#invalidateInternal()
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
   ...
   if (...) {
       ...
       final AttachInfo ai = mAttachInfo;
       //1.mParent 一般是 ViewGroup,下面其實是呼叫了 ViewGroup 的 invalidateChild()
       final ViewParent p = mParent;
       if (p != null && ai != null && l < r && t < b) {
           final Rect damage = ai.mTmpInvalRect;
           damage.set(l, t, r, b);
           //2.注意這裡的第二個引數 damage,後面會用到,在這裡注意下它不為null,而且也不是isEmpty即可
           p.invalidateChild(this, damage);
       }
       ...
   }
}

所以 invalidate() 內部其實是呼叫了 ViewGroup 的 invalidateChild(),再跟進看看:

//ViewGroup#invalidateChild()  
public final void invalidateChild(View child, final Rect dirty) {
   //1. 注意這裡,下面的do{}while()迴圈會用到
   ViewParent parent = this;

   final AttachInfo attachInfo = mAttachInfo;
   if (attachInfo != null) {
       ...
       do {
           ...  
           //2.第一次迴圈的時候 parent 是 this 即 ViewGroup 本身,迴圈終止條件是 parent == null,所以可以猜測這個方法會返回當前ViewGroup的parent,跟進確認一下
           parent = parent.invalidateChildInParent(location, dirty);
           ...
       } while (parent != null);
   }
}

這裡有一個 do{}while() 的迴圈操作,第一次迴圈的時候 parent 是 this,即 ViewGroup 本身,所以接下去就是呼叫 ViewGroup 本身的 invalidateChildInParent() 方法,然後迴圈終止條件是 patent == null,所以可以猜測這個方法返回的應該是 ViewGroup 的 parent,跟進看看:

//ViewGroup#invalidateChildInParent()  
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
   //1.當滿足if條件時,就會返回 mParent,否則返回 null。
   if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
           (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
       if (...) {
           ...
           return mParent;

       } else {
           ...
           return mParent;
       }
   }
   return null;
}

所以關鍵是 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()  
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
   ...
   //1.所有return 都為null
   if (dirty == null) {
       invalidate();
       return null;
   } else if (dirty.isEmpty() && !mIsAnimating) {
       return null;
   }
   ...
   invalidateRectOnScreen(dirty);
   return null;
}

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

//ViewRootImpl#invalidateRectOnScreen()
private void invalidateRectOnScreen(Rect dirty) {
   ...
   if (!mWillDrawSoon && (intersected || mIsAnimating)) {
       //1.跟到這裡看到這個方法就可以停住了
       scheduleTraversals();
   }
}

跟到這裡就可以了,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 樹的三個基本操作,只能通過層層遍歷。所以,測量、佈局、繪製三個基本操作的執行都會是一次遍歷操作。

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

640?wx_fmt=png

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

//View#draw(Canvas canvas)
public void draw(Canvas canvas) {
   ...
   /*
    * 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 //呼叫onDraw()繪製自己
    *      4. Draw children       //呼叫dispatchDraw()繪製子View
    *      5. If necessary, draw the fading edges and restore layers
    *      6. Draw decorations (scrollbars for instance)
    */

}

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

在這個流程中,當跟到 draw(Canvas, ViewGroup, long) 裡時,發現了跟動畫相關的程式碼:

//View#draw(Canvas canvas, ViewGroup parent, long drawingTime)  
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
   ...

   boolean more = false;
   ...
   //1.獲取View繫結的動畫
   final Animation a = getAnimation();
   if (a != null) {
       //2.如果View有繫結動畫,執行動畫相關邏輯
       more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
       ...
   }

   ...

   return more;
}

還記得我們呼叫 View.startAnimation(Animation) 時將傳進來的 Animation 賦值給 mCurrentAnimation 了麼。

//View#getAnimation()
public Animation getAnimation() {
   return mCurrentAnimation;
}

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

//View#applyLegacyAnimation()
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) {
   Transformation invalidationTransform;
   final int flags = parent.mGroupFlags;
   final boolean initialized = a.isInitialized();
   if (!initialized) {
       //1.動畫初始化
       a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
       a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
       if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
       //2.通知動畫開始
       onAnimationStart();
   }

   final Transformation t = parent.getChildTransformation();
   //3.呼叫動畫的getTransfomation()計算動畫
   boolean more = a.getTransformation(drawingTime, t, 1f);

   ...

   return more;
}

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

//Animation#getTransformation()
public boolean getTransformation(long currentTime, Transformation outTransformation) {
   //1.記錄動畫第一幀時間
   if (mStartTime == -1) {
       mStartTime = currentTime;
   }

   final long startOffset = getStartOffset();//動畫延遲開始時間,預設為0
   final long duration = mDuration;//動畫持續時間
   float normalizedTime;
   if (duration != 0) {
       //2.計算動畫的進度:根據(當前時間 - 動畫第一幀時間) / 動畫持續時間
       normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / (float) duration;
   } ...

   //3.確保動畫進度在 0.0 ---- 1.0 之間
   if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

   if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
       ...

       if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

       ...

       //4.根據插值器計算實際的動畫進度
       final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
       //5.根據動畫進度應用動畫效果
       applyTransformation(interpolatedTime, outTransformation);
   }

   ...    

   return mMore;
}

這個方法裡做了幾件事:

  • 記錄動畫第一幀的時間

  • 根據當前時間到動畫第一幀的時間這之間的時長和動畫應持續的時長來計算動畫的進度

  • 把動畫進度控制在 0-1 之間,超過 1 的表示動畫已經結束,重新賦值為 1 即可

  • 根據插值器來計算動畫的實際進度

  • 呼叫 applyTransformation() 應用動畫效果

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

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

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

我們知道 applyTransformation() 是在 getTransformation() 裡被呼叫的,而這個方法是有一個 boolean 返回值的,我們看看它的返回邏輯是什麼:

//Animation#getTransformation()
public boolean getTransformation(long currentTime, Transformation outTransformation) {
   ...
   //1.expired = true 表示動畫已經結束或者被取消了
   final boolean expired = normalizedTime >= 1.0f || isCanceled();
   mMore = !expired;

   ...    

   if (expired) {
       //2.如果設定了動畫迴圈的次數,那麼當動畫結束的時候判斷一下迴圈次數是否已經達到設定的次數,沒有的話,重置第一幀時間
       if (mRepeatCount == mRepeated || isCanceled()) {
           ...    
       } else {
           ...

           mStartTime = -1;
           mMore = true;

           ...
       }
   }

   //3.綜上,mMore = true 表示動畫進度還沒超過 1.0,也就是動畫還沒結束;false 表示動畫已經結束或者被取消了

   if (!mMore && mOneMoreTime) {
       mOneMoreTime = false;
       return true;
   }

   return mMore;
}

也就是說 getTransformation() 的返回值代表的是動畫是否完成,還記得是哪裡呼叫的 getTransformation() 吧,去 applyLegacyAnimation() 裡看看取到這個返回值後又做了什麼:

//View#applyLegacyAnimation()  
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) {
   ...
   //1.獲取返回值來檢視動畫是否已經結束
   boolean more = a.getTransformation(drawingTime, t, 1f);

   ...
   //2.動畫還沒結束時
   if (more) {
       if (!a.willChangeBounds()) { //除了 Alpha 動畫預設返回 false,其餘基礎動畫都返回 true
           ...
       } else {
           ...
           parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

           ...
           //3.呼叫mParent的請求重繪操作,這個方法在開頭分析startAnimation時分析過了
           parent.invalidate(left, top, left + (int) (region.width() + .5f),
                   top + (int) (region.height() + .5f));
       }
   }
   return more;
}

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

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

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

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