ViewPager+Fragment 更新问题


先说一下我的问题场景:

我的主Activity使用ViewPager+4个fragment的方式实现,自定义适配器MyMainFragmentPagerAdapter继承自FragmentPagerAdapter,MainActivity继承自FragmentActivity。

实际使用的时候,当MainActivity执行完一些必要数据后,开始为ViewPager设置适配器资源,各个Fragment的实例化是在FragmentPagerAdapter的getItem方法中执行的。程序启动的时候首先会加载第一个和第二个Fragment(即调用getItem实例化)。

目前操作是,按home键,整个APP进入后台,去系统设置页面切换语言,语言切换完成后,再次进入我的APP。

结果MainActivity执行了onDestroy方法,第一个Fragment依次执行了onDestroyView-->onDestroy-->onDetach,紧接着执行了Fragment的onCreate方法,然后才去执行了MainAct的onCreate方法,这点很奇怪,不明白为什么会直接就执行Fragment的onCreate方法,按照我的理解,不应该是MainActivity和里面的Fragment都已经销毁了吗?Fragment怎么优先于宿主MainActivity执行呢?(这个问题暂时搁置,希望有大神看到能指导下)

这样一来,之前所有的正常逻辑都打乱了,因为我的fragment创建的时候需要带一些参数,而像刚才这种执行顺序的话,我的参数就无法传递到Fragment了。那么我是不是可以来自己控制这个Fragment的创建呢,把已经加载的Fragment清除掉,加入一个新的Fragment?该怎么做呢?如何像ViewPager一样控制Fragment呢?

在使用中我们会发现,连接ViewPager和Fragment的适配器PagerAdapter做了很多工作,那么相应的控制应该就是在这个类里面做了。

首先来看看PagerAdapter:

【PagerAdapter】
 
PageAdapter 是 ViewPager 的支持者,ViewPager 将调用它来取得所需显示的页,而 PageAdapter 也会在数据变化时,通知 ViewPager。这个类也是FragmentPagerAdapter 以及 FragmentStatePagerAdapter 的基类。如果继承自该类,至少需要实现 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
 
getItemPosition()
该函数用以返回给定对象的位置,给定对象是由 instantiateItem() 的返回值。
在 ViewPager.dataSetChanged() 中将对该函数的返回值进行判断,以决定是否最终触发 PagerAdapter.instantiateItem() 函数。
在 PagerAdapter 中的实现是直接传回 POSITION_UNCHANGED。如果该函数不被重载,则会一直返回 POSITION_UNCHANGED,从而导致 ViewPager.dataSetChanged() 被调用时,认为不必触发 PagerAdapter.instantiateItem()。很多人因为没有重载该函数,而导致调用
PagerAdapter.notifyDataSetChanged() 后,什么都没有发生。
instantiateItem()
在每次 ViewPager 需要一个用以显示的 Object 的时候,该函数都会被 ViewPager.addNewItem() 调用。
notifyDataSetChanged()
在数据集发生变化的时候,一般 Activity 会调用 PagerAdapter.notifyDataSetChanged(),以通知 PagerAdapter,而 PagerAdapter 则会通知在自己这里注册过的所有 DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中注册过的 PageObserver。PageObserver 则进而调用 ViewPager.dataSetChanged(),从而导致 ViewPager 开始触发更新其内含 View 的操作。

 


实际使用过程中,我们不太会用到这个基类,而是使用如下两个派生类:


【FragmentPagerAdapter】
 
FragmentPagerAdapter 继承自 PagerAdapter。相比通用的 PagerAdapter,该类更专注于每一页均为 Fragment 的情况。如文档所述,该类内的每一个生成的 Fragment 都将保存在内存之中,因此适用于那些相对静态的页,数量也比较少的那种;如果需要处理有很多页,并且数据动态性较大、占用内存较多的情况,应该使用FragmentStatePagerAdapter。FragmentPagerAdapter 重载实现了几个必须的函数,因此来自 PagerAdapter 的函数,我们只需要实现 getCount(),即可。且,由于 FragmentPagerAdapter.instantiateItem() 的实现中,调用了一个新增的虚函数 getItem(),因此,我们还至少需要实现一个 getItem()。因此,总体上来说,相对于继承自 PagerAdapter,更方便一些。
 
getItem()
该类中新增的一个虚函数。函数的目的为生成新的 Fragment 对象。重载该函数时需要注意这一点。在需要时,该函数将被 instantiateItem() 所调用。
如果需要向 Fragment 对象传递相对静态的数据时,我们一般通过 Fragment.setArguments() 来进行,这部分代码应当放到 getItem()。它们只会在新生成 Fragment 对象时执行一遍。
如果需要在生成 Fragment 对象后,将数据集里面一些动态的数据传递给该 Fragment,那么,这部分代码不适合放到 getItem() 中。因为当数据集发生变化时,往往对应的 Fragment 已经生成,如果传递数据部分代码放到了 getItem() 中,这部分代码将不会被调用。这也是为什么很多人发现调用 PagerAdapter.notifyDataSetChanged() 后,getItem() 没有被调用的一个原因。
instantiateItem()
函数中判断一下要生成的 Fragment 是否已经生成过了,如果生成过了,就使用旧的,旧的将被 Fragment.attach();如果没有,就调用 getItem() 生成一个新的,新的对象将被 FragmentTransation.add()。
FragmentPagerAdapter 会将所有生成的 Fragment 对象通过 FragmentManager 保存起来备用,以后需要该 Fragment 时,都会从 FragmentManager 读取,而不会再次调用 getItem() 方法。
如果需要在生成 Fragment 对象后,将数据集中的一些数据传递给该 Fragment,这部分代码应该放到这个函数的重载里。在我们继承的子类中,重载该函数,并调用 FragmentPagerAdapter.instantiateItem() 取得该函数返回 Fragment 对象,然后,我们该 Fragment 对象中对应的方法,将数据传递过去,然后返回该对象。
否则,如果将这部分传递数据的代码放到 getItem()中,在 PagerAdapter.notifyDataSetChanged() 后,这部分数据设置代码将不会被调用。
destroyItem()
该函数被调用后,会对 Fragment 进行 FragmentTransaction.detach()。这里不是 remove(),只是 detach(),因此 Fragment 还在 FragmentManager 管理中,Fragment 所占用的资源不会被释放。
 
 
【FragmentStatePagerAdapter】
 
 
FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 一样,是继承子 PagerAdapter。但是,和 FragmentPagerAdapter 不一样的是,正如其类名中的 'State' 所表明的含义一样,该 PagerAdapter 的实现将只保留当前页面,当页面离开视线后,就会被消除,释放其资源;而在页面需要显示时,生成新的页面(就像 ListView 的实现一样)。这么实现的好处就是当拥有大量的页面时,不必在内存中占用大量的内存。
 
getItem()
一个该类中新增的虚函数。
函数的目的为生成新的 Fragment 对象。
Fragment.setArguments() 这种只会在新建 Fragment 时执行一次的参数传递代码,可以放在这里。
由于 FragmentStatePagerAdapter.instantiateItem() 在大多数情况下,都将调用 getItem() 来生成新的对象,因此如果在该函数中放置与数据集相关的 setter 代码,基本上都可以在 instantiateItem() 被调用时执行,但这和设计意图不符。毕竟还有部分可能是不会调用 getItem() 的。因此这部分代码应该放到 instantiateItem() 中。
instantiateItem()
除非碰到 FragmentManager 刚好从 SavedState 中恢复了对应的 Fragment 的情况外,该函数将会调用 getItem() 函数,生成新的 Fragment 对象。新的对象将被 FragmentTransaction.add()。
FragmentStatePagerAdapter 就是通过这种方式,每次都创建一个新的 Fragment,而在不用后就立刻释放其资源,来达到节省内存占用的目的的。
destroyItem()
将 Fragment 移除,即调用 FragmentTransaction.remove(),并释放其资源。

 

 

结合实际情况,我这里就使用了FragmentPagerAdapter。
从实际项目中我们可以知道,ViewPager一般会缓存当前Fragment和两边相邻的Fragment。即当我程序启动的时候,会默认加载了第一个fragment和右边的fragment(即第二个),而当我切换到第二个fragment的时候,又会自动加载了第三个fragment,而当我继续切换到第三个fragment的时候,会自动加载第四个fragment,并且此时,第一个

fragment执行了onDestroyView。这些fragment的生命周期函数是由FragmentPagerAdapter来控制,具体就是在相应的时候调用了instantiateItem和destroyItem方法。在切换不同 的fragment的时候,会反复执行这两个方法,但是getItem却很少执行。

我们进入FragmentPagerAdapter源码看下它在instantiateItem执行了什么:

 

@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;
    }


从源码中可以看到,getItem方法是由instantiateItem调用的,而fragment对象是由FragmentManager管理的,一旦fragment对象创建了,对应我的APP就是四个fragment创建了,无论怎么切换都不会再执行getItem方法了,这样也就无法重新创建fragment对象了。这样也就可以解释了为什么我的activity再次执行onCreate的时候,没有去重新执行创建fragment的过程。在重新启动activity的过程中,很可能当前应用的FragmentManager并没有销毁,这个FragmentManager把新的activity和已经存在的fragment又关联起来了(但这又是为什么?是什么原理的?不懂!)。


经过上面的分析,似乎弄明白了重启activity的时候没有重新创建fragment对象的原因,那么我现在想把这个已经存在的fragment清除掉,让我来控制重新创建一个新的fragment关联到这个activity。
其实我们可以仿照instantiateItem源码执行的过程,这个源码在fragment为空的时候,执行了创建和关联的动作。那么我在这里强制执行创建和关联动作:

首先复写MyMainFragmentPagerAdapter的instantiateItem方法:

@Override
    public Object instantiateItem(ViewGroup container, int position) {
        Log.i(TAG, "instantiateItem:"+position);
        Fragment fragment = (Fragment) super.instantiateItem(container,
                position);//得到缓存的fragment
            
        
        if(update && position==0){//我这里只需要重新更新第一个fragment
            Log.w(TAG, "update new fragment");
                //得到tag,这点很重要
                String fragmentTag = fragment.getTag();
 
                FragmentTransaction ft = fm.beginTransaction();
                //移除旧的fragment
                ft.remove(fragment);
                //换成新的fragment
                fragment = getItem(position);
                //添加新fragment时必须用前面获得的tag,这点很重要
                ft.add(container.getId(), fragment, fragmentTag);
                ft.attach(fragment);
                ft.commit();
                update = false;//清除更新标记(只有重新启动的时候需要去创建新的fragment对象),防止正常情况下频繁创建对象
        }
        
        
        return fragment;
        
    }


然后,当我在需要更新fragment的时候,就调用一下PagerAdapter的notifyDataSetChanged()方法,这个方法会去执行instantiateItem方法

这样我就可以实现在重启Activity的时候,在需要的时候手动重新创建一个新的fragment了。

当然了,这里还遗留了一些问题,希望以后能搞明白,本质原因还是需要进一步探究啊:

当应用进入后台的时候,切换了系统语言,为什么会导致再次进入应用的时候重新创建Activity呢?

宿主activity已经执行了onDestroy,而fragment也相应的执行到了生命末尾,为什么还没等宿主activity执行onCreate的时候,fragment就已经开始执行onCreate了?

FragmentManager不会随着宿主activity的销毁而消亡吗,它是怎么关联activity和fragment的?

文章写的有点乱,只是为了记录这个疑难问题,方便以后可以查阅深究。有大神能给点指导就更好了。

最后贴上两篇阅读到的文章,问题得以解决还是这两篇文章的功劳,3Q:

android FragmentPagerAdapter getItem方法没有执行

FragmentPagerAdapter刷新fragment最完美解决方案

补充:

鉴于我的应用场景,如果我的应用不需要跟随系统语言重新加载资源的话,可以在Manifest.xml中的MainActivity节点添加属性:

android:configChanges="locale|layoutDirection"

这样设置后,当系统语言发生了改变,在进入Activity时会触发onConfigurationChanged方法,而不会重新执行onCreate方法。相反如果不设置这个属性则会重新执行onCreate方法。(这样做,上面那些所做的修改也就没必要了,一个配置就搞定了~)

注意,在4.2以上的版本中需要添加layoutDirection,解释:android:configChanges locale 改语言后,该配置不起作用的原因


4.2增加了一个layoutDirection属性,当改变语言设置后,该属性也会成newConfig中的一个mask位。所以ActivityManagerService(实际在ActivityStack)在决定是否重启Activity的时候总是判断为重启。
需要在android:configChanges 中同时添加locale和layoutDirection。


当然如果应用支持多语言,想跟随系统语言的话,重启还是很有必要的。

尊重作者劳动成果:https://blog.csdn.net/anhenzhufeng/article/details/48468167

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