Android ViewPager Adapter 内存管理的坑

好久没有记录bug了的,因为最近一直在解bug... 之所以写这个文章,确实是出于无奈,毕竟同一个问题,项目上遇到了两次。这里主要是提一下 ViewPager + Fragment。

ViewPager + Fragment 有 '两种' Adapter

  1. FragmentPagerAdapter

https://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter
Implementation of [PagerAdapter](https://developer.android.com/reference/android/support/v4/view/PagerAdapter.html) that represents each page as a [Fragment](https://developer.android.com/reference/android/support/v4/app/Fragment.html) that is persistently kept in the fragment manager as long as the user can return to the page.

示例:

   public static class MyAdapter extends FragmentPagerAdapter {
        public MyAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public int getCount() {
            return NUM_ITEMS;
        }

        @Override
        public Fragment getItem(int position) {
            return ArrayListFragment.newInstance(position);
        }
    }

实现上的核心代码:

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        final long itemId = getItemId(position);
        // 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;
    }
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        mCurTransaction.detach((Fragment)object);
    }
  1. FragmentStatePagerAdapter

https://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter
Implementation of [PagerAdapter](https://developer.android.com/reference/android/support/v4/view/PagerAdapter.html) that uses a [Fragment](https://developer.android.com/reference/android/support/v4/app/Fragment.html) to manage each page. This class also handles saving and restoring of fragment's state.

示例:

  public static class MyAdapter extends FragmentStatePagerAdapter {
        public MyAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public int getCount() {
            return NUM_ITEMS;
        }

        @Override
        public Fragment getItem(int position) {
            return ArrayListFragment.newInstance(position);
        }
    }

实现上的核心代码:

   @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);
        return fragment;
    }
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
        mFragments.set(position, null);
        mCurTransaction.remove(fragment);
    }

相同点: 二者在Adapter初始化上看上去没什么差别,都是基于 PagerAdapter 实现,只需要重写 getItem 返回每一页对应的 fragment 即可。

重点说一下不同点: 就像官方描述那样,我就不做翻译了的,从内部实现也确实印证了这一点.

前者 FragmentPagerAdapter 通过 mFragmentManager.findFragmentByTag(name) 从视图上把之前的视图找出来,然后执行 attach, 找不到就回调 getItem 问实现方要 fragment, 然而这个attach 有一个很隐藏的坑,我们都知道viewpager可以设置offsetLimit, 也就是说会销毁limit之外的page,然而在使用 FragmentPagerAdapter 你会发现,当你再次滑动回来的时候,因为之前 destroyItem 仅仅走的是 detach方法,所以通过 findFragmentByTag(name) 依旧能找到上次使用的 fragment,但此时由于 ViewPager 本身实现层面上已经把它踢出了的,所以当走 mCurTransaction.attach(fragment) 的时候会走 fragmentonCreateView方法重新loadView, 但是由于这个'fragment'实例还是上次初始化的那个实例,意味着其实数据(成员变量都在), 此时如果我们不重置一下数据的话就会出现一些逻辑上的bug. 所以网上就有人说可以这么搞:

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        //if (rootView == null) {
            rootView = inflater?.inflate(getRootViewLayoutResId(), container, false)
        /*} else run {
            val p = rootView!!.getParent() as? ViewGroup
            if (p != null) {
                p.removeAllViewsInLayout()
            }
        }*/

简单的说就是缓存一下上一次使用的view, 下次重新加载的时候就把缓存的rootView 重新添加到屏幕上,然而这种方案会不会让我们的内存占比比较大呢?

后者 FragmentStatePagerAdapter 是用一个数组ArrayList<Fragment>去管理fragment,当走到destroyItem的时候fragment 会从这个ArrayList<Fragment>中踢出掉:

 mFragments.set(position, null);

所以每次确实是重新创建一个fragment 实例。但这句话也不完全正确,因为还有一个关键信息是State. 我们可以看到 FragmentStatePagerAdapter中有一个ArrayList<Fragment.SavedState> mSavedState来管理页面的状态,当走到 destroyItem 的时候会保留即将销毁的这个fragment的状态:

  mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));

总结

当我们在使用两个adapter切记要管理好fragment的状态。。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容