標籤:

Android 坑檔案:背黑鍋的 Fragment

每一個行業都有自己的坑,所謂經驗多寡無非是有沒有踩過坑。我把 Android 開發中遇到的各種坑彙集起來,於是就有了這部『 Android 坑檔案』。

0.故事背景

故事發生在前幾天,洒家因為改版需要,為app的Fragment跳轉加上了Animation——本來就是一個很簡單的修改,洒家當然就不會想太多了。然而,往往看似最不起眼的改動總能引起地震,不信的話你可以去問問元芳,狄大人是不是說過這一點。果然,大人真乃神人也,兩天之後,項目組的其他成員開始抱怨:『Fragment的onDestroy不能回調了!!』『onDettach也不調了!!』……

你們可以盡情想像某種生物絕塵而去的場景,莫非Android這廝又在戲弄洒家??

1.追本溯源:Fragment的切換

前面說到,onDestroy就像一個犯了罪的壞蛋,丫的居然躲起來了,不像話。

怎麼追查呢?先說說我們的Fragment是怎麼切換的:

FragmentTransaction ft = getFragmentManager().beginTransaction();nint outAnim = ……;n// 回退到的界面的重新進入動畫nint inAnim = ……;nft.setCustomAnimations(inAnim, outAnim);nft.replace(R.id.fragment_container, targetFragment, targetFragmentTag);nft.commitAllowingStateLoss();n

就是一個簡簡單單的 『replace』,onDestroy你就居然隨隨便便的玩消失,你跟你爹Google打過招呼了么你……

不過問題總算還是簡單的,我們只要看看Transaction都幹了哪些壞事兒,問題就浮出水面了。我們看到beginTransaction帶回來的是一個叫BackStackRecord的傢伙。

嫌疑人:BackStackRecord落網

@Overridenpublic FragmentTransaction beginTransaction() {n return new BackStackRecord(this);n}n

據它交代,它把Fragment交給了一個叫Op的傢伙:

public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {n doAddOp(containerViewId, fragment, tag, OP_REPLACE);n return this;n}n

private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {n fragment.mFragmentManager = mManager;nn if (tag != null) {n fragment.mTag = tag;n }nn if (containerViewId != 0) {n fragment.mContainerId = fragment.mFragmentId = containerViewId;n }nn Op op = new Op();n op.cmd = opcmd;n op.fragment = fragment;n addOp(op);n}n

看來,是時候對Op進行圍堵了。

2.一個一個來:BackStackRecord處理隊列

等等,Op好像不是一個人!

static final class Op {n Op next;n Op prev;n int cmd;n Fragment fragment;n int enterAnim;n int exitAnim;n int popEnterAnim;n int popExitAnim;n ArrayList<Fragment> removed;n }n

Oooops!!居然是團伙作案,你們看看還有上線(prev)和下線(next)……局挺大額。有公安局大么。抓!

哎,奇怪,發現這麼多涉案人員,感覺都是小嘍啰啊,它們作案也都是流水線作業。那麼問題來了,它們背後究竟是誰在操縱著這一切?又有誰能有如此手段在法律的面前逍遙自在?

別急別急,BackStackRecord在安排好Op任務以後,就打了個報告給上面:

ft.commitAllowingStateLoss();n

哼,小樣兒,我還治不了你了我就:

public int commitAllowingStateLoss() {n return commitInternal(true);n }nn int commitInternal(boolean allowStateLoss) {n mCommitted = true;n if (mAddToBackStack) {n mIndex = mManager.allocBackStackIndex(this);n } else {n mIndex = -1;n }n mManager.enqueueAction(this, allowStateLoss);n return mIndex;n }n

喲嗬,背後還有個Manager啊。啊,怎麼把它給忘了。FragmentManager果然嫌疑最大!接著看enqueueAction是做什麼呢

public void enqueueAction(Runnable action, boolean allowStateLoss) {n if (!allowStateLoss) {n checkStateLoss();n }n synchronized (this) {n if (mPendingActions == null) {n mPendingActions = new ArrayList<Runnable>();n }n //看這裡看這裡!!n mPendingActions.add(action);n if (mPendingActions.size() == 1) {n mActivity.mHandler.removeCallbacks(mExecCommit);n mActivity.mHandler.post(mExecCommit);n }n }n }n

看來時候提審一下這個mPendingActions了。還有個共犯,mExecCommit。趕緊審,不然小心這倆越獄(Runnable,你看人家名字就是深諳『走為上策』的真諦啊)。

mExecCommit交代說它會等mActivity.mHandler給它的信兒,等到之後就來處理這些mPendingActions。

/**n * Only call from main thread!n */n public boolean execPendingActions() {n boolean didSomething = false;nn while (true) {n int numActions;n n synchronized (this) {n if (mPendingActions == null || mPendingActions.size() == 0) {n break;n }n n numActions = mPendingActions.size();n if (mTmpActions == null || mTmpActions.length < numActions) {n mTmpActions = new Runnable[numActions];n }n mPendingActions.toArray(mTmpActions);n mPendingActions.clear();n mActivity.mHandler.removeCallbacks(mExecCommit);n }n n mExecutingActions = true;n for (int i=0; i<numActions; i++) {n mTmpActions[i].run();n mTmpActions[i] = null;n }n mExecutingActions = false;n didSomething = true;n }nn if (mHavePendingDeferredStart) {n boolean loadersRunning = false;n for (int i=0; i<mActive.size(); i++) {n Fragment f = mActive.get(i);n if (f != null && f.mLoaderManager != null) {n loadersRunning |= f.mLoaderManager.hasRunningLoaders();n }n }n if (!loadersRunning) {n mHavePendingDeferredStart = false;n startPendingDeferredFragments();n }n }n return didSomething;n }n

mPendingActions當中存的就是BackStackRecord,好多好多個。之後遍歷它們,執行它們的run方法,而這個run方法其實本質上就是處理BackStackRecord持有的Op。

3.深入虎穴:Op的職責

剛才說到我們發現了Op的工作地點:BackStackRecord.run,看來我們是時候要進去看看它到底在幹什麼了。前面其實已經不難知道,BackStackRecord當中持有一個Op鏈表,在run方法當中會依次處理這些Op的操作。嗯,流水線作業。

對應到我們這個場景,就是一個replace的Op:

BackStackRecord.java run方法

case OP_REPLACE: {n Fragment f = op.fragment;n if (mManager.mAdded != null) {n for (int i = 0; i < mManager.mAdded.size(); i++) {n Fragment old = mManager.mAdded.get(i);n if (FragmentManagerImpl.DEBUG) {n Log.v(TAG,"OP_REPLACE: adding=" + f + " old=" + old);n }n if (f == null || old.mContainerId == f.mContainerId) {n if (old == f) {n op.fragment = f = null;n } else {n if (op.removed == null) {n op.removed = new ArrayList<Fragment>();n }n op.removed.add(old);n old.mNextAnim = op.exitAnim;n if (mAddToBackStack) {n old.mBackStackNesting += 1;n if (FragmentManagerImpl.DEBUG) {n Log.v(TAG, "Bump nesting of "+ old + " to " + old.mBackStackNesting);n }n }n mManager.removeFragment(old, mTransition, mTransitionStyle);n }n }n }n }n if (f != null) {n f.mNextAnim = op.enterAnim;n mManager.addFragment(f, false);n }n}nbreak;n

這老小子一下子就招了這麼多,不過關鍵的就一句:

mManager.removeFragment(old, mTransition, mTransitionStyle);n

話說我們這個操作是replace的嘛,所以當前所有在FragmentManager當中的,containerId與參數一致的Fragment都將成為old,並將接受組織的處理:remove。由於我們並沒有設置transition,所以其他兩個參數可以先不管。

『哼,今天就是你們的末日到了!』(狄大人在蛇靈案最後一集的這句話都入選高考題了……)

public void removeFragment(Fragment fragment, int transition, int transitionStyle) { n final boolean inactive = !fragment.isInBackStack();n if (!fragment.mDetached || inactive) {n if (mAdded != null) {n mAdded.remove(fragment);n }n if (fragment.mHasMenu && fragment.mMenuVisible) {n mNeedMenuInvalidate = true;n }n fragment.mAdded = false;n fragment.mRemoving = true;n moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,transition, transitionStyle, false);n }n}n

因為我們當前的場景裡面,被移除的Fragment並沒有被放入BackStack,所以inactive是true,也就是說,在moveToState時,傳入的newState是Fragment.INITIALIZING。

4.我有我的姿態:Fragment的狀態

Fragment的狀態設計得比較有趣,狀態總共有下面幾種:

static final int INVALID_STATE = -1; // Invalid state used as a null value.nstatic final int INITIALIZING = 0; // Not yet created.nstatic final int CREATED = 1; // Created.nstatic final int ACTIVITY_CREATED = 2; // The activity has finished its creation.nstatic final int STOPPED = 3; // Fully created, not started.nstatic final int STARTED = 4; // Created and started, not resumed.nstatic final int RESUMED = 5; // Created started and resumed.n

需要注意的是,這些狀態的值是有邏輯意義的,從0到5,從5到0,生命周期的變化就像出棧入棧一樣,狀態的變化也一定是相鄰狀態依次變化——這些都可以在moveToState當中看到。

我們直接奔赴moveToState的位置,看到有下面一段代碼:

……n ncase Fragment.ACTIVITY_CREATED:n if (newState < Fragment.ACTIVITY_CREATED) {n if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);n if (f.mView != null) {n // Need to save the current view state if notn // done already.n if (!mActivity.isFinishing() && f.mSavedViewState == null) {n saveFragmentViewState(f);n }n }n f.performDestroyView();n if (f.mView != null && f.mContainer != null) {n Animation anim = null;n if (mCurState > Fragment.INITIALIZING && !mDestroyed) {n anim = loadAnimation(f, transit, false,n transitionStyle);n }n if (anim != null) {n final Fragment fragment = f;n f.mAnimatingAway = f.mView;n f.mStateAfterAnimating = newState;n anim.setAnimationListener(new AnimationListener() {n @Overriden public void onAnimationEnd(Animation animation) {n if (fragment.mAnimatingAway != null) {n fragment.mAnimatingAway = null;n moveToState(fragment, fragment.mStateAfterAnimating,0, 0, false);n }n }n @Overriden public void onAnimationRepeat(Animation animation) {n }n @Overriden public void onAnimationStart(Animation animation) {n }n });n f.mView.startAnimation(anim);n }n f.mContainer.removeView(f.mView);n }n f.mContainer = null;n f.mView = null;n f.mInnerView = null;n }ncase Fragment.CREATED:n if (newState < Fragment.CREATED) {n if (mDestroyed) {n if (f.mAnimatingAway != null) {n // The fragments containing activity isn // being destroyed, but this fragment isn // currently animating away. Stop then // animation right now -- it is not needed,n // and we cant wait any more on destroyingn // the fragment.n View v = f.mAnimatingAway;n f.mAnimatingAway = null;n v.clearAnimation();n }n }n if (f.mAnimatingAway != null) {n // We are waiting for the fragments view to finishn // animating away. Just make a note of the staten // the fragment now should move to once the animationn // is done.n f.mStateAfterAnimating = newState;n newState = Fragment.CREATED;n } else {n if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f);n if (!f.mRetaining) {n f.performDestroy();n }nn f.mCalled = false;n f.onDetach();n if (!f.mCalled) {n throw new SuperNotCalledException("Fragment " + fn + " did not call through to super.onDetach()");n }n if (!keepActive) {n if (!f.mRetaining) {n makeInactive(f);n } else {n f.mActivity = null;n f.mParentFragment = null;n f.mFragmentManager = null;n f.mChildFragmentManager = null;n }n }n }n }n

有點長啊,不過其實也很清楚的:

  • 先保存Fragment的view狀態,然後f.performDestroyView();
  • 如果有移除的動畫,載入動畫開始執行動畫
  • 把Fragment的view從view hierarchy當中移除
  • f.mStateAfterAnimating = newState, 這個主要用於動畫結束之後的回調,動畫結束之後狀態會進一步轉移到f.mStateAfterAnimating,實際上就是Fragment.INITIALIZING
  • 將Fragment的狀態置為Fragment.CREATED,本次moveToState執行完
  • 動畫結束後回調,繼續調用moveToState,將狀態置為Fragment.INITIALIZING,並且調用onDestroy和onDetach方法。

然而,看到紅字沒有,它並沒有執行!!!

5.柳暗花明:Fragment切換的動畫

這可愁死洒家了,看似天衣無縫啊,而且這麼多年了support-v4的代碼都沒有修改過,難道是洒家用錯了??不過,這就好比黎明前的黑暗,問題就在這裡了,看看為什麼沒有回調,只好去看下Animation的運行機制了。

Animation的運行本質上就是在每次View.draw時調用其getTransformation方法計算出當前的動畫之後的變化狀態Transformation outTransformation,之後View再根據這個Transformation outTransformation來繪製自己。

等等,你剛才說什麼?View.draw?也就是說如果View不draw的話,Animation就歇菜了?趕緊爬到前面看看,果然!

……n f.mView.startAnimation(anim);n}nf.mContainer.removeView(f.mView);n……n

對啊,動畫開始之後馬上就把view給移除了,後面還怎麼可能draw呢?

結論:因為這裡View在動畫開始時就被移除,因此動畫並沒有真正意義上的開始,也因而不能有回調,這直接導致了Fragment生命周期的不完整。

6.真相:冤枉啊大人

是不是覺得故事就結束了?No!!我剛剛說的那個結論有個地方時沒有說清楚的,到底是動畫開始前移除,還是開始後移除,這個效果完全不一樣!!我們來看源碼:

ViewGroup.java

private void removeViewInternal(int index, View view) {n ……n if (view.getAnimation() != null ||n (mTransitioningViews != null && mTransitioningViews.contains(view))) {n addDisappearingView(view);n } else if (view.mAttachInfo != null) {n view.dispatchDetachedFromWindow();n }n ……n }n

我們看到,人家ViewGroup是對於有動畫的View做了處理的,可以讓它執行完動畫。不信你試試,如果先startAnimation,然後removeView,實際上這個動畫還是可以執行的。

所以剛才那個結論是錯的咯?

當然也不能完全算是,不過,我們總算接近真相了……直到後來洒家看到下面這段代碼,這段代碼是我們項目中使用的Fragment的公共父類:

@Overriden public void onDestroyView() {n //pageContent是onCreateView返回的view n if(pageContent != null) {n final ViewGroup parent = (ViewGroup) pageContent.getParent();n if(parent != null) {n parent.removeView(pageContent);//防止pageContent被載入到不同的ViewGroup中n }n }n super.onDestroyView();n }n

只不過真相真的有點兒慘不忍睹,原來我們項目中的這個ui框架會在FragmentonDestroy的時候,把Fragment.onCreateView返回view,也就是pageContent提前從view hierarchy當中remove掉,也就是說,實際上,代碼走到moveToState時,Fragment的View已經被拿掉了!!難怪動畫不能執行……神坑啊,結果沒想到是被隊友坑了……

結論:因為這裡View在動畫開始前就被移除,因此動畫並沒有真正意義上的開始,也因而不能有回調,這直接導致了Fragment生命周期的不完整。

本來以為是個Fragment的問題,沒想到是一個View Hierarchy和View Animation的問題。

7.彩蛋:還沒結束呢

下面描述一個場景:

  1. 現在,我們有兩個Button,A和B。
  2. A點擊以後執行下面的代碼:

    Animation animation = new AlphaAnimation(1, 0);nanimation.setDuration(1000);nanimation.setAnimationListener(new Animation.AnimationListener() {n@Overridenpublic void onAnimationStart(Animation animation) {n Log.d(TAG, "onAnimationStart() called with: " + "animation = [" + animation + "]");n}nn@Overridenpublic void onAnimationEnd(Animation animation) {n Log.d(TAG, "onAnimationEnd() called with: " + "animation = [" + animation + "]");n}nn@Overridenpublic void onAnimationRepeat(Animation animation) {n Log.d(TAG, "onAnimationRepeat() called with: " + "animation = [" + animation + "]");n}n});n((ViewGroup) buttonA.getParent()).removeView(buttonA);nbuttonA.startAnimation(animation);n

  3. B點擊以後執行下面的代碼:

    if(buttonA.getParent() == null){n((ViewGroup) buttonB.getParent()).addView(buttonA);n}n

  4. 神奇的事情發生了:點擊A以後,什麼都沒有發生;過一會兒點擊BA被重新添加回View Hierarchy,接著輸出了下面的日誌:

    onAnimationStart() called with: animation = [android.view.animation.AlphaAnimation@a5b4d80]nonAnimationEnd() called with: animation = [android.view.animation.AlphaAnimation@a5b4d80]n

  5. 為什麼呢?詐屍趕腳啊。不過有了前面的分析,我們就很容易想到,這是剛才那個沒有執行的動畫在作祟呢!不過問題來了,都過去那麼久了,動畫為什麼會執行呢?

    View.java

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

    其實我們知道,startAnimation時,view已經被移除了,所以invalidate沒有作用。而所謂的startTime實際上只不過是一個標誌,它並不是個絕對時間,而是從下一幀的時間開始算起,這樣說來,由於下一幀在B點擊之後才到來,那麼自然在B點擊之後A的動畫才真正意義上開始了。

    故事難道真的結束了??No!!

    為了找到問題的根源,我對比了support-v4和android.app下面FragmentManager的實現,還真找到了區別,support-v4的我們在前面已經見到過了,下面給出對應的android.app的實現:

    FragmentManager.java

    case Fragment.ACTIVITY_CREATED:n if (newState < Fragment.ACTIVITY_CREATED) {n if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);n if (f.mView != null) {n // Need to save the current view state if notn // done already.n if (!mActivity.isFinishing() && f.mSavedViewState == null) {n saveFragmentViewState(f);n }n }n f.performDestroyView();n if (f.mView != null && f.mContainer != null) {n Animator anim = null;n if (mCurState > Fragment.INITIALIZING && !mDestroyed) {n anim = loadAnimator(f, transit, false,transitionStyle);n }n if (anim != null) {n final ViewGroup container = f.mContainer;n final View view = f.mView;n final Fragment fragment = f;n container.startViewTransition(view);n f.mAnimatingAway = anim;n f.mStateAfterAnimating = newState;n anim.addListener(new AnimatorListenerAdapter() {n @Overriden public void onAnimationEnd(Animator anim) {n container.endViewTransition(view);n if (fragment.mAnimatingAway != null) {n fragment.mAnimatingAway = null;n moveToState(fragment,fragment.mStateAfterAnimating,0, 0, false);n }n }n });n anim.setTarget(f.mView);n anim.start();nn }n f.mContainer.removeView(f.mView);n }n f.mContainer = null;n f.mView = null;n }n

    重點就是,support-v4用的是Animation,而android.app用的是Animator。這裡我就不在啰嗦了,直接給出結論:

    結論:Animation的執行依賴View的繪製,而Animator是獨立的,並不依賴View的繪製,因此View執行Animator是不會因View是否在Hierarchy中而受到影響的。

    可見Android官方確實也意識到了用Animation來實現Fragment的切換動效可能會因為使用不當出現問題,而問題的本質其實是Fragment的生命周期並不依賴於Fragment的view的繪製,因此其生命周期的回調當然也不能依賴於Animation這樣一個依賴於view繪製的東西,這個問題看似簡單,確是邏輯致命的。為了提升介面的使用體驗,提升Fragment系統的健壯性,Android官方最終選擇使用Animator也是其必然的結果。

    8.結語

    這個故事當中,我們的項目代碼越權將Fragment的View從View Hierarchy當中移除(儘管從代碼角度確實有能力這樣做),導致了問題的產生;另外,兩個沒有直接邏輯關係的行為Fragment的生命周期Fragment的View繪製居然產生了依賴關係,這也為問題的產生埋下了禍根。

    歷史的車輪總是義無反顧的前行,有些風雲人物因為僭越而身敗名裂,有些國家機器因為依附而不能長久。《天龍八部》中掃地僧說『功夫練得越深,越需要高深的佛法來化解』……

推薦閱讀:

Kotlin 資源大全 - 如何學習 Kotlin?
Android 坑檔案:當 WeakReference 遇上了 Lambda
Kotlin 泛型中的 in 和 out

TAG:Kotlin |