Fragment 与 FragmentPagerAdapter (一)

前情提要

最近的项目中,又用到了Fragment+FragmentPagerAdapter的组合。
不禁想起当年第一次使用这两者结合的一些窘境。

平常开发使用时,经常别人选定了框架,你负责开枝散叶,而这开枝散叶的第一步经常是Crlt+C 和Crlt+V。把别人写好的FragmentOne复制一份FragmentSecond,然后把View的内容改一改。模仿其初始化,加入 PagerAdapter中。
等到自己第一次从头写FragmentOne的时候,就有点手足无措了。

至于使用Fragment+PagerAdapter,而不是View+PagerAdapter,我一直以来都只有一个原因,对于复杂的布局,那就是Fragment有相对独立的生命周期,一切有迹可循,将代码从Activity中抽离,简化Activity的逻辑。
何况JetPack框架中 ViewModel对于Fragment的支持。

本篇涉及两个点。

  • Fragment的系统回收以及初始化
  • FragmentPagerAdapterFragmentStatePagerAdapter的区别

下一篇则偏PagerAdapter的基础分析以及实战:
Fragment 与 FragmentPagerAdapter (二)

先贴上Fragment 生命周期图:


image.png

1.FragmentPagerAdapter 和 FragmentStatePagerAdapter的区别

再说这两的区别

FragmentPagerAdapter

基本是很多博客举栗子的时候都喜欢用这个,Fragment对象都是创建好放在List中,

val fragmentList = mutableListOf<Fragment>(
    FragmentOne(),
    FragmentTwo(),
    FragmentThree()
)

    val adapter = object : FragmentPagerAdapter(childFragmentManager) {
        override fun getItem(position: Int): Fragment {
            return fragmentList[position]
        }
        override fun getCount(): Int {
            return fragmentList.size
        }

    }

当初年少无知的我,一脸懵逼,为什么总是把Fragment先创建好,这不是浪费内存,和影响回收么。之所以这么想,是因为对该机制还不是很了解啊。

当然这样子写,在内存不足时,该ViewPager所在的Activity被回收的情况下,还是依然会存在问题的。看完文章的最后一节,大家应该也应该可以很容易明白问题出在哪里了。

要知道,FragmentPagerAdapter本身就是用于少量静态页面的处理。

当在不同的位置来回切换,其实adapter.getItem()只会被调用一次用于初始化Fragment。当划出缓存范围时,这个Fragment实例依旧保存在FragmentManager中,并不会被销毁。仅是执行了mCurTransaction.detach(fragment),根据代码注释,也就是类似于加入回退栈---也就是并未Fragment.onDestory(),只销毁了视图。

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull 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); // 将Fragment从视图中移除,而保留实例
    }

再次滑动到该页面时,fragment执行生命周期onCreateView()重新创建UI视图(但不会执行onCreate())。

接着我再来康康ViewPager每个Item的初始化,可以对照下FragmentPagerAdapter的instantiateItem()源码的注释:

FragmentPagerAdapter:

public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // Do we already have this fragment?
        //根据container.getId(), itemId生成TAG, itemId即position
        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 {
            //如果找不到实例,才调用getItem()
            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;
    }

所以即便是动态创建Fragment,也就是起一个延迟初始化的作用。那么fragmentList直接把所有Fragment对象创建好并没有很大影响。除非说,Fragment初始化之时,就保存了大量的数据。否则毕竟只是几个对象而已,内存占用很小。

等到getItem()被调用时,创建好的Fragment才会开始拥有其生命周期,保存有UI视图,真正的占有大量的内存。

当然,如果有需要话,简单的处理,可以在getItem的时候再去初始化。比如说

override fun getItem(position: Int): Fragment {
   return when(position) {
       0 -> FragmentOne()
       1 -> FragmentTwo()
       2 -> FragmentThree()
       else -> Fragment()
   }

既然实例没有被销毁,如果出于某些考虑,比如更快的显示view视图,甚至可以在Fragment实例中用变量缓存原本要被销毁的View,然后在onCreateView中复用。当然这样会占用更多内存。

另外由于视图被销毁,但是实例存在,那么需要考虑好实例的变量的值对新创建的View的影响。

FragmentStatePagerAdapter

FragmentPagerAdapter不同的是,不在ViewPager缓存范围的Fragment实例会从FragmentManager中移走,只保留其状态(各种Bundle参数,包括view状态等),当再次加载该位置时,保留的状态会恢复。

public Object instantiateItem(@NonNull 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); // 与FragmentPagerAdapter不同,直接getItem()
        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);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull 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);
        }
        //保留Fragment的状态
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);
        // 从FragmentManager中移除
        mCurTransaction.remove(fragment);
    }

FragmentStatePagerAdapter中,ViewPager不同位置的Fragment会都被同时保存在一个ArrayList中的,超出缓存位置,执行destroyItem(),ArrayList相应位置也会将其置空,也就是不保留Fragment实例。

但保留有Fragment的状态:

mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);

所以这里踩的一个小坑是:

曾想做一个无限滑动的ViewPager,使得getCount()返回的是Int.MAX_VALUE,而且用的是FragmentStatePagerAdapter,那么在初始化 Fragment会调用mFragments.set(position, fragment);position比较大时直接在ArrayList扩容时直接OOM了。

所以,综上,如果对于少量的静态页面直接使用FragmentPagerAdapter
而如果有大量的动态页面还是使用FragmentStatePagerAdapter,毕竟无需保留所有Fragment的实例。

2. Fragment的系统回收以及初始化

Fragment的初始化,不涉及生命周期的话,其实没多少可以说的,毕竟,不就是一个对象嘛,直接Fragment()创建轻轻松松,或者继承时,构造函数加个参数也没什么大不了的,

 FragmentOne("param")..so easy

当然代码中常见还有这这种,比如上述FragmentPagerAdapter动态初始化Fragment

val map = arrayOf(
        TodayFragment::class.java,
        LastDayFragment::class.java)

fun getFragment(position: Int) : Fragment{
        return Fragment.instantiate(this.context, map[position].name)
}

这里主要要讲的就是Fragment.instantiate()

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
        try {
            Class<?> clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                sClassMap.put(fname, clazz);
            }
            Fragment f = (Fragment) clazz.getConstructor().newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.setArguments(args);
            }
            return f;
}

这里可以看到其实该方法就是直接调用的Fragment的默认构造方法,并执行setArguments(args)来设置参数。

看到这里,有人会不禁的想,既然是调用默认构造方法,我直接使用Fragment()或者在有参数的情况下,直接FragmentOne("param")不是来得更容易?

ok,当然更容易啦。

不过我们要考虑一种情况就是,Activity在非用户主动退出的情况下,Activity被回收,比如横竖屏切换,或者内存紧张后台应用程序回收。

就要注意两个问题

  1. 此时Fragment也被回收,同时系统新建Activity时也会恢复被回收的Fragment。所以需要可能需要判断防止多生成一个Fragment
xxxxActivity extend FragmentActivity:

override fun onCreate(savedInstanceState: Bundle?) {
    //看里面的源码,在onCreate()会恢复Fragment
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    savedInstanceState ?: let { // 这里怎么复用就见招拆招了
        // 直接新建一个替换相同id的Fragment,不复用原数据
        supportFragmentManager.beginTransaction().replace(R.id.main,FragmentOne("param")).commit()
    }
    initView()
}
  1. 系统在恢复Fragment的时候,调用的是Fragment.instantiate()了来创建对象,也就表示Fragment必须有无参构造方法。
class FragmentOne : Fragment {

    constructor() : super() {} // /必须有无参构造方法
    constructor(p: String) : this() {
        
}

也就是回收恢复时创建FragmentOne对象调用的是FragmentOne(),而不是FragmentOne("param"),那么"param" 参数没有被传进去就可能导致一些错误。这时候,也就是setArguments(args)来起作用了.

Fragment被回收时,会保存Fragment状态---FragmentState,意味着Fragment中通过setArguments(args)方法之后的args对象也会被保存下来!那么就恢复的fragment实例就可以通过getArguments()来获取到该值了

所以在我们在创建 FragmentOne("param")传递参数时记得调用setArguments(args),把param保存下来。

也可以写成这样(Android Studio 模板代码):

class BlankFragment: Fragment() {

    private var param1: String? = null
    private var param2: String? = null
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            BlankFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }

关于Fragment系统回收,多说两句:

  1. 可能你注意到了,上面贴的代码中,两个PagerAdapter都有对Fragment的复用进行了处理,但二者之间获取恢复的Fragment的手段并不一致,

FragmentPagerAdapter直接通过tag,从FragmentManager中的所有Fragment中寻找。

而FragmentStatePagerAdapter,在ViewPager.setAdapter()时,从ViewPager的saveState中的获取原先所有处于active的Fragment的key值(被回收时保存进去的),然后再从FragmentManger中的activeMap中获取Fragment。

  1. 当Activity恢复时,Fragment中的子Fragment也是会恢复的。也就是childFragmentManager中也会有Fragment的。

所以 若有自己复写PagerAdapter的,记得重写instantiateItem()时,要判断下,FragmentManger或childFragmentManager中是否有缓存,避免内存泄露或其他意外事件。

  1. 上面那个问题,也就是下面这段代码真正的问题出在哪?又应该如何改善,留给读者思考了:
val fragmentList = mutableListOf<Fragment>(
    FragmentOne(),
    FragmentTwo(),
    FragmentThree()
)

    val adapter = object : FragmentPagerAdapter(childFragmentManager) {
        override fun getItem(position: Int): Fragment {
            return fragmentList[position]
        }
        override fun getCount(): Int {
            return fragmentList.size
        }

    }

参看问答

参考:
Android解惑 - 为什么要用Fragment.setArguments(Bundle bundle)来传递参数

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