标签:
接手别人的老项目。新版本测试提出一个bug:
点击Home最小化的应用—>系统设置界面 改变字体后—>点击进入应用—>3个由viewpager 的fragmentadapter管理的 tab页面点击都没反应。
这是一个比较蛋疼的bug,猜想了很多原因,都不对。 
项目的结构是 activity 内有mainfragment,mainfragment又 包含viewpager,viewpager 使用FragmentPagerAdapter 管理3个页面。所以是 activity套2层fragment的结构。
(注:本文写得比较乱,仅仅是个人笔记,很多地方带?表明本人也不确定,也没弄清楚) 
public abstract class FragmentPagerAdapter extends PagerAdapter
    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }
    return fragment;
实际上 只是在instantiateItem 中 调用了getItem(position); 这个抽象方法。 其他方法这个adapter 已经基本实现,所以user使用fpadapter的时候 实现getItem方法 就 是给 instantiateItem返回 fragment了。
然而 返回的是一个fragment,并不是view,且PagerAdapter中instantiateItem 要求的只是一个object。 那么ViewPager如何处理这个object? 
    ItemInfo addNewItem(int position, int index) { 
        ItemInfo ii = new ItemInfo(); 
        ii.position = position; 
        ii.object = mAdapter.instantiateItem(this, position); 
        ii.widthFactor = mAdapter.getPageWidth(position); 
        if (index < 0 || index >= mItems.size()) { 
            mItems.add(ii); 
        } else { 
            mItems.add(index, ii); 
        } 
        return ii; 
    }
可见会在addNewItem中 将 这个 object封装 成 一个 iteminfo。 
private final ArrayList mItems = new ArrayList(); 
同时将这个info 放在了一个arraylist中、
看看调用栈 
04-07 18:43:48.446 14842-14842/? E/AndroidRuntime: FATAL EXCEPTION: main 
                                                   Process: com.example.testeveryting, PID: 14842 
                                                   java.lang.NullPointerException 
                                                       at android.support.v4.app.FragmentPagerAdapter.instantiateItem(FragmentPagerAdapter.java:85) 
                                                       at android.support.v4.view.ViewPager.addNewItem(ViewPager.java:943) 
                                                       at android.support.v4.view.ViewPager.populate(ViewPager.java:1091) 
                                                       at android.support.v4.view.ViewPager.populate(ViewPager.java:1025) 
                                                       at android.support.v4.view.ViewPager.onMeasure(ViewPager.java:1545) 
                                                       at android.view.View.measure(View.java:17616) 
                                                       at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:719) 
                                                       at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:455) 
                                                       at android.view.View.measure(View.java:17616) 
                                                       at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5428) 
                                                       at android.widget.FrameLayout.onMeasure(FrameLayout.java:310) 
                                                       at android.view.View.measure(View.java:17616) 
                                                       at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5428) 
                                                       at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1413) 
                                                       at android.widget.LinearLayout.measureVertical(LinearLayout.java:696) 
                                                       at android.widget.LinearLayout.onMeasure(LinearLayout.java:589) 
                                                       at android.view.View.measure(View.java:17616) 
                                                       at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5428) 
                                                       at android.widget.FrameLayout.onMeasure(FrameLayout.java:310) 
                                                       at com.android.internal.policy.impl.PhoneWindow
                                                       at android.view.Choreographer
                                                       at android.os.Handler.handleCallback(Handler.java:733) 
                                                       at android.os.Handler.dispatchMessage(Handler.java:95) 
                                                       at android.os.Looper.loop(Looper.java:146) 
                                                       at android.app.ActivityThread.main(ActivityThread.java:5756) 
                                                       at java.lang.reflect.Method.invokeNative(Native Method) 
                                                       at java.lang.reflect.Method.invoke(Method.java:515) 
                                                       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291) 
                                                       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107) 
                                                       at dalvik.system.NativeStart.main(Native Method)
正在使用 MyPagerAdapter extends PagerAdapter 的情况下,instantiateItem方法 
        @Override 
        public Object instantiateItem(ViewGroup container, int position) { 
            View view = View.inflate(MainActivity.this, R.layout.adapter_ad, null); 
            ImageView imageView = (ImageView) view.findViewById(R.id.image); 
            imageView.setBackgroundResource(list.get(position%list.size()).getIconResId());
        //将view对象添加到viewpager,交给它管理
        container.addView(view);
        return view;
    }
实际上每次返回的view 都要user手动 添加到viewpager中, destory的时候 要手动remove。 保证 viewpager 缓存pager 数目固定 ,与viewpager内部的items数组 数目应该也保持一致。 
猜测fragmentpageradapter应该是在下面某句话中 将fragment的视图addview到了viewpager中? 望指正。—–4.8号更新:错,此时fragment 的oncreateview方法还未被 fragmentmanager调用,因此是不会有view视图出现的。下面单步调试中,给出了真正调用的地方。
实if (fragment != null) { 
    if (DEBUG) Log.v(TAG, “Attaching item #” + itemId + “: f=” + fragment); 
    mCurTransaction.attach(fragment); 
} else { 
    fragment = getItem(position); 
    if (DEBUG) Log.v(TAG, “Adding item #” + itemId + “: f=” + fragment); 
    mCurTransaction.add(container.getId(), fragment, 
            makeFragmentName(container.getId(), itemId)); 
} 
addNewItem 会被populate 调用
populate 这个方法200 行,逻辑比较复杂。 大致是 处理 populate(mCurItem) 
负责处理当前位置 int currentItem 的 item的显示,以及 这个位置前后 item的 进出,显示,index位置,在 mItems中是否移除 加入 等。 
// Check width measurement of current pages and drawing sort order. 
// Update LayoutParams as needed. 
final int childCount = getChildCount(); 
for (int i = 0; i < childCount; i++) { 
    final View child = getChildAt(i); 
    final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 
    lp.childIndex = i; 
    if (!lp.isDecor && lp.widthFactor == 0.f) { 
        // 0 means requery the adapter for this, it doesn’t have a valid width. 
        final ItemInfo ii = infoForChild(child); 
        if (ii != null) { 
            lp.widthFactor = ii.widthFactor; 
            lp.position = ii.position; 
        } 
    } 
} 
其中一处代码片段,每次滑动viewpager都会调用,可以看出每次都遍历了子孩子 ,动态改变了他们的layoutparams,达到pager随着手指滑动效果。 
populate在整个viewpager中 有8处 地方被调用。
private final Runnable mEndScrollRunnable = new Runnable() { 
        public void run() { 
            setScrollState(SCROLL_STATE_IDLE); 
            populate(); 
        } 
    }; 
    用处不明
setAdapter时。line447 
     else if (!wasFirstLayout) { 
                populate(); 
            } else { 
                requestLayout(); 
            } 
  看出来应该是第一次setAdapter会造成 第一次layout,所以调用了 populate来 additem,获得 view展示。
public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer)  line 656 
    用户调用此方法改变page的 位置时?
public void setOffscreenPageLimit(int limit) 
            if (limit < DEFAULT_OFFSCREEN_PAGES) { 
            Log.w(TAG, “Requested offscreen page limit ” + limit + ” too small; defaulting to ” + 
                    DEFAULT_OFFSCREEN_PAGES); 
            limit = DEFAULT_OFFSCREEN_PAGES; 
        } 
        if (limit != mOffscreenPageLimit) { 
            mOffscreenPageLimit = limit; 
            populate(); 
        } 
    这是一个比较常用的方法。 可以看到 当user 设置的 offsreen页面 与当前值不符合的时候,  会调用 populate,来 增加或者减少 当前缓存页面。
void smoothScrollTo(int x, int y, int velocity)  line838 
    平滑移动时
onMeasure() 
            // Make sure we have created all fragments that we need to have shown. 
        mInLayout = true; 
        populate(); 
        mInLayout = false; 
        可见,populate 完后,就把 layout标记设置为false。 确实 是用来 在滑动,layout情况下页面改变后加载页面的。
public boolean onInterceptTouchEvent(MotionEvent ev) 
                    // Let the user ‘catch’ the pager as it animates. 
                    mScroller.abortAnimation(); 
                    mPopulatePending = false; 
                    populate(); 
                    mIsBeingDragged = true; 
  在ACTION_DOWN分支中。  
public boolean onTouchEvent(MotionEvent ev)  line 2043 
    依然是在ACTION_DOWN分支中。 
                 mScroller.abortAnimation(); 
                mPopulatePending = false; 
                populate();
            // Remember where the motion event started
一共8处。
除此之外 populate(mItem)这个作为被populate()直接调用的方法,还在void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) 
中被调用,setCurrentItemInternal方法 即setCurrentItem() 所需调用的方法。
1.viewpager 并没有像listview一样,将user(使用这个控件的人) 返回的view 内部自己addview 到 viewgroip的成员mChildren[] 数组中。需要user手动在instantiateItem方法中手动调用一次addview。 
 同时viewpager也没有像listview一样 有从mChildren[]中移除子view的操作,需要用户在destory方法中手动remove。 
为何viewpager要这么设计? 
2.viewpager内部自己定义了一个ArrayList的 mItems成员 来保存 user返回的object(view),猜想 这个mItems的数量应该和mChildren[]数量应该是一致的? 
3.viewpager如果有上百个页面,开始只加载3个,那么在快速滑动的时候 肯定是不断的在创建新的view销毁旧的view,并没有复用机制?viewpager没有复用机制 是因为考虑到每个page条目可能差别较大无法复用?不像listview一样 都属于同一类型?
猜测  改变字体后  应用中所有activity会销毁 再重新创建。 
而在此次创建中,init处的 3个fragment 对象是生成了。如图viewpager的 mItems成员 有3个item对象包含了3个fragment。
但是viewpager的mChildren成员却为空
正常的可以点击显示时应该是这样。
所以猜测viewpager的 setCurrentItem 中的populate应该是操作显示mChildren中的子view来显示ui,而不是控制mItems。 
mChildren为空,自然显示不出来,(但是却显示出了应用最小化之前的fragment页面,且这个页面点击无任何反应,猜测是残留了一个视图?怎么做到的?)
因此viewpager的 addView方法一定没有调用。
void moveToState(Fragment f, int newState, int transit, int transitionStyle, 
            boolean keepActive) 
在case Fragment.INITIALIZING:分支中 
                    if (f.mFromLayout) { 
                        // For fragments that are part of the content view 
                        // layout, we need to instantiate the view immediately 
                        // and the inflater will take care of adding it. 
                        f.mView = f.performCreateView(f.getLayoutInflater( 
                                f.mSavedFragmentState), null, f.mSavedFragmentState); 
                        if (f.mView != null) { 
                            f.mView.setSaveFromParentEnabled(false); 
                            if (f.mHidden) f.mView.setVisibility(View.GONE); 
                            f.onViewCreated(f.mView, f.mSavedFragmentState); 
                        } 
                    }
可见fmimpl中的moveToState方法中调用了performCreateView ,并将返回的view 赋值给f.mView,这一过程 其实是activity的performStart 调用的。
可见doFrame –由wms发消息 viewrootimpl.w收到消息,发给 哪个handler? ActivityThread .H还是viewrootimpl中的 handler? 
—都不对
view包下,Choreographer中的FrameHandler处理 
然后handler处理,调用doFrame。 处理这一帧视图? 
有run()方法,是Post到主线程 调用vrimpl的 performtraversal。经过层层递归的measure和onMeasure的调用 (16次),终于调用到viewpager的onMeasure—populate
populate中,可以看到调用了viewpager与fragmentpageradaper交互的一个重要方法fragmentpageradaper.finishUpdate()
开启了事物,因此会调用到fmimpl中的moveToState 
该方法 对Fragment 的mState进行switch, 
case Fragment.INITIALIZING:  
case Fragment.CREATED: 
case Fragment.ACTIVITY_CREATED: 
case Fragment.STOPPED: 
case Fragment.STARTED:  
等等。 
addview则是在CREATE分支中—意思大概是 我已经是出于create状态中了,fragment对象已经生成了,下面该处理显示,处理将我的视图添加到window当中的逻辑了
仔细看上面的大图 可以看到,此时container就是 viewpager。 
通过container = (ViewGroup)mContainer.onFindViewById(f.mContainerId);找到。 
mContainer是FragmentActivity的callback 对象。
还可以看到此时的mItems数量是2,说明1.fragment对象全部通过inistiateItem放进viewpager的mItems之后(viewpager默认只加载2个pager) 2.才 finishupdate 来addview 
上述第一步 应该是activitystart 的时候调用 由ams管理,后面的这一步 通过 wms控制,viewrootimpl 在measure过程中调用。
Debug看看各方法执行顺序 
instantiateItem —measure—performTraversal  position0 
instantiateItem —measure—performTraversal  position1
Fragment.onCreate—–case Fragment.INITIALIZING: f.performCreate(f.mSavedFragmentState);——-measure—performTraversal position0
Fragment.onCreate—–case Fragment.INITIALIZING: f.performCreate(f.mSavedFragmentState);——-measure—performTraversal position1
Fragment.onCreateView—–case Fragment.CREATED: f.mView = f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), container, f.mSavedFragmentState); position0 
Fragment.onCreateView—–case Fragment.CREATED: f.mView = f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), container, f.mSavedFragmentState);        position1 
猜测addView 在执行这一次CREATED分支中,onCreateView之后执行。作用就是把 fragment视图添加到container中,如果是transaction.replace就是把fragment视图 添加到 replace第一次参数id的视图中,fragmentviewpager 就是add到viewpager中(这里把viewpager的id引用赋值给了其mID)
很久没看了,分析到第五步之后陷入僵局...因为虽然知道了addview没有被调用导致了这个bug,但是并不知道如何修改代码使addview得以调用,或者如何修改能让fragment执行到case CREATE分支后能按正常或者fragment的状态,从而按正常步骤执行下去,生成新fragment对象,装入viewpager中。
通过第五步的分析(请忽略这混乱的截图和语言组织),这个bug产生的原因其实很明显了。addview没有被调用是因为切换语言—回收activity—activity在ondestroy前通知回收fragment—但关键就在这一步 
activity会通知fragment执行detached ,发现Fragment在detached之后都会被reset掉,但是它并没有对ChildFragmentManager做reset,所以会造成ChildFragmentManager的状态错误。 
因此在再次创建的时候,因为错误的状态 而没有生成新的fragment。 
but,我按照别人的方法操作: 
我们需要在Fragment被detached的时候去重置ChildFragmentManager,即:
@Override 
  public void onDetach() { 
    super.onDetach(); 
    try { 
      Field childFragmentManager = Fragment.class 
          .getDeclaredField(“mChildFragmentManager”); 
      childFragmentManager.setAccessible(true); 
      childFragmentManager.set(this, null);
} catch (NoSuchFieldException e) {
  throw new RuntimeException(e);
} catch (IllegalAccessException e) {
  throw new RuntimeException(e);
}
} 
还是没有解决问题。
后来没有办法只好花时间重构了一下代码,将中间的那层mainfragment去掉了。 activity直接通过viewpager来管理3个fragment。bug 解决。 
看来在fragment 嵌套的使用过程中还是要格外小心…一个小bug 坑了好多天。
Fragment嵌套带来的坑--页面点击无反应(顺带ViewPager之 FragmentPagerAdapter简单分析)
标签:
原文地址:http://blog.csdn.net/tmac2000/article/details/51353830