ListView嵌套ViewPager+Fragment引起的Bug

發現問題

之前發現過一次,情景一樣,當時是將ListView替換爲LinearLayout然後動態添加view解決,這次又發現這個問題,感覺得從根本上找出原因所在,畢竟listview嵌套多層viewpager+fragment場景還是存在很多的(如資產詳情帶圖表切換),報的crash

分析問題

定位問題

根據報錯日誌No view found for id 0x7f0f03f8定位源碼位置FragmentManager#MoveToState(1051)

`                case Fragment.CREATED:
                if (newState > Fragment.CREATED) {
                    if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
                    if (!f.mFromLayout) {
                        ViewGroup container = null;
                        if (f.mContainerId != 0) {
                            container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);
                            if (container == null && !f.mRestored) {
                                throwException(new IllegalArgumentException(
                                        "No view found for id 0x"
                                        + Integer.toHexString(f.mContainerId) + " ("
                                        + f.getResources().getResourceName(f.mContainerId)
                                        + ") for fragment " + f));
                            }
                        }
                        f.mContainer = container;
                        f.mView = f.performCreateView(f.getLayoutInflater(
                                f.mSavedFragmentState), container, f.mSavedFragmentState);`

可以看出fragment創建時時會根據它所記錄的容器id去找到之前的容器,比如沒有找到就會發生這個Error

單步debug

【onCreate】看出最外層的vpProfitContainer(viewpager)的id就是說找不到的#7f0f03f8

查看Actvity中的管理的Fragments

可以看出actvity持有兩個fragment外面tab的fragment他們的容器Id就是vpProfitContainer(#7f0f03f8)

在Onresume回來時


debug到vpProfitContainer.setAdapter(profitTrendsPageAdapter);時候就發生crash,而此時fragment是存在的。

可以得出結論此時的fragment確實已經存在但是無法找到之前的容器vpProfitContainer(#7f0f03f8),那就是說明

在OnResume時actvity的視圖重建還未好,但是viewpager的已經加載了,即時序出現了問題

詳細原因描述:

這個異常通常發生在listview嵌套viewpager且視圖重建時,此時在 【fragments在被添加到viewpager】這個過程發生在【viewpager被添加到父容器】之前。換而言之就是 getView()方法返回值前,fragments已經被inflated,此時在fragment中(見前面源碼)已經調用container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);來尋找viewapger容器,由於getView方法還未來得及返回此時viewPager處於detach狀態的,從而根據f.mContainerId找不到fragment的容器

並且做了如下嘗試

方法 crash
將adater創建添加放到afterViewCreate中,bindview方setAdapter Y
setAdapter加上300ms延時 N

從而驗證了這個問題。

解決問題

1、目前採用是將這一塊操作放到afterViewCreate中,從而避開了再走getView方法區獲得子view(bindView)

2、在bindView中處理加上延時,確保getview已經處理完返回

Fragment 源碼分析

  • 什麼是FragmentTransaction(BackStackRecord中實現)

它封裝了一系列對fragment的操作,並一次性執行這些操作,如add、remove、replace、show、hide等
通過雙向鏈表結構

`static final int OP_NULL = 0;
static final int OP_ADD = 1;
static final int OP_REPLACE = 2;
static final int OP_REMOVE = 3;
static final int OP_HIDE = 4;
static final int OP_SHOW = 5;
static final int OP_DETACH = 6;
static final int OP_ATTACH = 7;

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

Op mHead;
Op mTail;
int mNumOp;
int mEnterAnim;
int mExitAnim;
int mPopEnterAnim;
int mPopExitAnim;
int mTransition;
int mTransitionStyle;
boolean mAddToBackStack;
boolean mAllowAddToBackStack = true;`

閱讀源碼發現Fragment中replace、add、remove、hide、show ————> addOp,如

`    public FragmentTransaction show(Fragment fragment) {
    Op op = new Op();
    op.cmd = OP_SHOW;
    op.fragment = fragment;
    addOp(op);

    return this;
}
`

addOp

`    void addOp(Op op) {
    if (mHead == null) {
        mHead = mTail = op;
    } else {
        op.prev = mTail;
        mTail.next = op;
        mTail = op;
    }
    //記錄轉場動畫的信息
    op.enterAnim = mEnterAnim;
    op.exitAnim = mExitAnim;
    op.popEnterAnim = mPopEnterAnim;
    op.popExitAnim = mPopExitAnim;
    mNumOp++;
}`

記錄鏈表信息的雙向鏈表操作,主要是操作不同cmd的Op對象,通過雙鏈表維護起來,由於Fragment在activity層面維護了一個回退棧,在使用Fragment時候,如果不開啓回退棧它是直接銷燬再重建,但是如果將Fragment任務添加回退棧後,則不會銷燬,按回退就會顯示到棧頂,

`addToBackStack(null)
 popBackStack()
 getBackStackEntryCount()
`

commit

BackStatckRecord#commitInternal(mManager.enqueueAction(this, allowStateLoss);)可以看到commit方法並沒有立即執行之歌動作,而是入隊了action,系統會在下次eventloop到來時來執行它,這裏的this就是指代BackStackRecord#run方法,因爲BackStackRecord實現了Runnable接口,這個run方法主要就是向前遍歷Op雙向鏈表,根據cmd的不同調用FragmentManagerImpl的以下方法 ,add、repleace、remove等方法,可以看出replace相當於先執行remove再add
case OP_REPLACE: {
Fragment f = op.fragment;
if (mManager.mAdded != null) {
for (int i=0; i<mManager.mAdded.size(); i++) {
Fragment old = mManager.mAdded.get(i);
if (FragmentManagerImpl.DEBUG) Log.v(TAG,
"OP_REPLACE: adding=" + f + " old=" + old);
if (f == null || old.mContainerId == f.mContainerId) {
if (old == f) {
op.fragment = f = null;
} else {
if (op.removed == null) {
op.removed = new ArrayList<Fragment>();
}
op.removed.add(old);
old.mNextAnim = exitAnim;
if (mAddToBackStack) {
old.mBackStackNesting += 1;
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
+ old + " to " + old.mBackStackNesting);
}
mManager.removeFragment(old, transition, transitionStyle);
}
}
}
}
if (f != null) {
f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
}
} break;

`  case OP_HIDE: {
                Fragment f = op.fragment;
                f.mNextAnim = exitAnim;
                mManager.hideFragment(f, transition, transitionStyle);
            } break;
            case OP_SHOW: {
                Fragment f = op.fragment;
                f.mNextAnim = enterAnim;
                mManager.showFragment(f, transition, transitionStyle);
            } break;`

對應該FragmentImpl中的實現函數

`public void hideFragment(Fragment fragment, int transition, int transitionStyle) {
    if (DEBUG) Log.v(TAG, "hide: " + fragment);
    if (!fragment.mHidden) {
        fragment.mHidden = true;
        if (fragment.mView != null) {
            Animation anim = loadAnimation(fragment, transition, false,
                    transitionStyle);
            if (anim != null) {
                fragment.mView.startAnimation(anim);
            }
            fragment.mView.setVisibility(View.GONE);
        }
        if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) {
            mNeedMenuInvalidate = true;
        }
        fragment.onHiddenChanged(true);
    }
}

public void showFragment(Fragment fragment, int transition, int transitionStyle) {
    if (DEBUG) Log.v(TAG, "show: " + fragment);
    if (fragment.mHidden) {
        fragment.mHidden = false;
        if (fragment.mView != null) {
            Animation anim = loadAnimation(fragment, transition, true,
                    transitionStyle);
            if (anim != null) {
                fragment.mView.startAnimation(anim);
            }
            fragment.mView.setVisibility(View.VISIBLE);
        }
        if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) {
            mNeedMenuInvalidate = true;
        }
        fragment.onHiddenChanged(false);
    }
}`

可以看出show/hideFragment只是改變fragment根View的visibility,最多帶上個動畫效果,另外只有本身是hidden的fragment,調用show才起作用,否則沒用的,fragment.onHiddenChanged會被觸發;其次不會有生命週期callback觸發
這些Op處理完之後,就調用了mManager.moveToState(mManager.mCurState, transition, transitionStyle, true);開始進入FragmentManagerImpl中處理

什麼是FragmentManager(FragmentManagerImpl中實現)

它是和某個activity相關聯的,並不是全局唯一的,而是每個actvity都有自己的FragmentManager,內部都有自己的狀態mCurState,對應外部activity的生命週期狀態,它提供和activity中fragment交互的API
FragmentManager是一個抽象類,裏面有一個實現類FragmentManagerImpl和一個狀態管理類FragmentManagerState

  1. Fragment狀態管理

FM裏面維護一個自己的狀態,當導入一個Fragment的時候,FM的目的就是爲了讓Fragment個自己的狀態基本保持一致.它有自身的狀態機,而它的狀態可以理解爲與Actvity本身同步

  1. 關鍵函數

FragmentManager#moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)

FragmentManager的每一次狀態變更都會引起mActive裏面的Fragment的狀態變更,而mActivity是所有納入FM管理的Fragment容器,

關於狀態變更參見Fragment

`static final int INITIALIZING = 0;     // Not yet created.
static final int CREATED = 1;           // Created.
static final int ACTIVITY_CREATED = 2;  // The activity has finished its creation.
static final int STOPPED = 3;           // Fully created, not started.
static final int STARTED = 4;           // Created and started, not resumed.
static final int RESUMED = 5;           // Created started and resumed.

int mState = INITIALIZING;`

可以看出狀態越靠後值越大,在FM管理時也是直接通過狀態數值比較來決定,如

`if (f.mState < newState)   
...  
else {  
... `

這些正好是和act的生命週期對應起來,也就是說這些方法是隨着act進入到不同的生命週期而被調用的,即mCurState的值是被這些方法觸發設置的。比如act進入到了Resume狀態,那麼FragmentManagerImpl.mCurState也就等於Fragment.RESUMED。主要是通過通過dispatchXXX函數調用對應的moveToState

  1. 關鍵函數分析

這個方法最終會將FragmentManager的狀態賦值給fragment,另外這個方法會根據不同的state調用各種onAttach, Fragment.performXXX,進而調到用戶自己override的fragment的各種生命週期方法,比如onCreate、onCreateView等等。主要注意的是 CREATED狀態

`case Fragment.CREATED:
                if (newState > Fragment.CREATED) {
                    if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
                    if (!f.mFromLayout) {
                        ViewGroup container = null;
                        if (f.mContainerId != 0) {
                            container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);
                            if (container == null && !f.mRestored) {
                                throwException(new IllegalArgumentException(
                                        "No view found for id 0x"
                                        + Integer.toHexString(f.mContainerId) + " ("
                                        + f.getResources().getResourceName(f.mContainerId)
                                        + ") for fragment " + f));
                            }
                        }`

每次都是查找自己的container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);容器`

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章