美文网首页Android开发Android技术知识Android进阶之路
Android知识笔记:记录一个至今仍有很多人写错的技术点,避免

Android知识笔记:记录一个至今仍有很多人写错的技术点,避免

作者: Z_萧晓 | 来源:发表于2020-04-08 16:18 被阅读0次

    之前已经分享了两篇我们容易搞错的几个Android知识点,今天继续更新,我们还是要追求极致,把不懂的问题搞懂的~

    今天我要来纠正一个关于 ViewPager 的错误用法。

    这个错误写法其实广为流传,我在早期的博客也有类似的写法。

    下面开始正文:

    我随便在网上搜了个 ViewPager + Fragment用法,类似的代码很常见:

    public class MainActivity extends FragmentActivity {
        private ViewPager m_vp;
        private ArrayList<Fragment> fragmentList;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            m_vp = (ViewPager)findViewById(R.id.viewpager);
    
            mfragment1 = new fragment1();
            mfragment2 = new fragment2();
            mfragment3 = new fragment3();
    
            fragmentList = new ArrayList<Fragment>();
            fragmentList.add(mfragment1);
            fragmentList.add(mfragment2);
            fragmentList.add(mfragment3);
    
            // FragmentPagerAdapter
            m_vp.setAdapter(new MyViewPagerAdapter(getSupportFragmentManager()));
        }
    
        public class MyViewPagerAdapter extends FragmentPagerAdapter{
    
            @Override
            public Fragment getItem(int arg0) {
                return fragmentList.get(arg0);
            }
    
            @Override
            public int getCount() {
                return fragmentList.size();
            }
        }
    }
    

    很多同学都喜欢这么写,然后还经常通过

    adapter.getItem(pos)

    fragmentList.get(pos)

    去获取对应的 fragment。

    其实,这种写法是存在很大的问题的!

    我们引出几个问题来慢慢回答:

    1. 这种写法在什么情况下,会造成什么异常(问题以及对应的场景)?

    2. 造成该问题的原因是(原理)?

    3. 更好的写法应该是什么(提供根据 position 获取对应 Fragment 方法)?

    1. 异常情况

    在Activity 被触发重建行为时会发生异常情况,什么时候会重建呢?

    例如你的 Activity 被用户切换到后台, 此时用户打王者荣耀去了,打完回来,由于内存原因,你的 Activity 很可能被系统干掉,然后用户切回你的app,对应的 Activity 就会尝试重建。

    上面的代码,重建时会产生什么问题呢?

    重建会走 Activity#onCreate,然后就会执行:

    mfragment1 = new fragment1();
    mfragment2 = new fragment2();
    mfragment3 = new fragment3();
    
    fragmentList = new ArrayList<Fragment>();
    
    fragmentList.add(mfragment1);
    fragmentList.add(mfragment2);
    fragmentList.add(mfragment3);
    

    重新创建了 3 个 fragment,然后放到 fragmentList 中。

    但是,问题在于Activity 重建的时候,上一次界面上的Fragment 相关信息会被存储下来用于恢复。

    对应到上例,ViewPager 的 FragmentPagerAdapter 在恢复的时候,会尝试恢复上次的Fragment。

    而你这次新创建的 3 个 Fragment 则完全没有被使用,这就导致后续你在通过 fragmentList 获取的 Fragment 对象其实和界面完全不是一个对象,如果你尝试做一些操作那大概率崩溃了,因为这些二次创建的 Fragment 都没往下走生命周期,里面的 View 都没初始化。

    为什么会这样呢?

    2. 为什么会这样呢?

    需要从 FragmentPagerAdapter 的源码中来寻找答案了。

    先来看看我们定义Adapter时重写的getItem方法是在哪里被调用的:

    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        ......
    
        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) {
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position);
            mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }
        ......
        return fragment;
    }
    

    咦?在instantiateItem方法中,我们重写的getItem方法竟然不是每次都会被调用的!

    它会先判断FragmentManager是否已添加了目标Fragment(findFragmentByTag),如果已经添加了的话,就会把它取出来并重新关联上,而getItem方法就不会被调用了。

    如果从FragmentManager中找不到的话,才会调用getItem获取目标Fragment,然后通过事务来添加进去,注意此时add方法的第三个参数(tag)传的是makeFragmentName方法的返回值,它跟上面查找时传的值是一样的,来看一下:

    private static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
    }
    

    超简单,就是拼接一个字符串。

    看回instantiateItem方法,可以看到makeFragmentName的两个参数分别传的是container的id值和getItemId方法返回的值:

    public long getItemId(int position) {
        return position;
    }
    

    getItemId方法如果不重写的话,返回就是参数值,也就是ViewPager页面的索引值了。

    好,总结一下:

    • 在FragmentPagerAdapter的instantiateItem方法(这个方法会在ViewPager滑动状态变更时调用)中,每个position所对应的Fragment只会添加一次到FragmentManager里面,也就是说,我们在Adapter中重写的getItem方法,它的参数position不会出现两次相同的值。

    • 当Fragment被添加时,会给这个Fragment指定一个根据itemId来区分的tag,而这个itemId就是根据getItemId方法来获取的,默认就是当前页面的索引值。

    怎么避免上面的问题呢?

    3. 如何避免这样的问题 ,方式1

    现在我们已经知道了问题发生的原因,要解决的话,对症下药就行了:

    既然ViewPager在添加新Item时会优先查找FragmentManager中已存在的Fragment,那么我们在Activity重建后,实例Fragment时也可以像它那样,先看看FragmentManager中有没有,如果有的话就直接重用,不用new了。

    比如定义一个instantiateFragment方法:

    private Fragment instantiateFragment(ViewPager viewPager, int position, Fragment defaultResult) {
        String tag = "android:switcher:" + viewPager.getId() + ":" + position;
        Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
        return fragment == null ? defaultResult : fragment;
    }
    

    然后在原来实例化Fragment的地方:

    mfragment1 = new fragment1();
    mfragment2 = new fragment2();
    mfragment3 = new fragment3();
    

    改成:

    mfragment1 = instantiateFragment(m_vp, 0, new fragment1());
    mfragment2 = instantiateFragment(m_vp, 1, new fragment2());
    mfragment3 = instantiateFragment(m_vp, 2, new fragment3());
    

    就OK啦!!!

    这样的话,就算Activity被意外销毁,重新创建时,我们也一样能找回原来已经添加了的Fragment。

    等等,这个方式好像有一丝小问题。

    4. 如何避免这样的问题 ,方式2

    刚才的方案确实可以,相当于每次先通过 FragmentManager去取,能够取到就直接使用,取不到就重新创建。

    但是取 fragment 需要通过 tag或者id。

    上例使用了 tag,但是 FragmentPagerAdapter 的makeFragmentName方法是私有的,也就是说,未来它可能会修改它内部 tag 生成的逻辑。

    一旦 tag 的逻辑修改了,上述代码就要一起修改。

    还有个解决上述问题的思路。

    类似代码如下:

    public class MyPagerAdapter extends FragmentStatePagerAdapter {
        SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
    
        public MyPagerAdapter(FragmentManager fm) {
            super(fm);
        }
    
        @Override
        public int getCount() {
            return ...;
        }
    
        @Override
        public Fragment getItem(int position) {
            return MyFragment.newInstance(...); 
        }
    
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment fragment = (Fragment) super.instantiateItem(container, position);
            registeredFragments.put(position, fragment);
            return fragment;
        }
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            registeredFragments.remove(position);
            super.destroyItem(container, position, object);
        }
    
        public Fragment getRegisteredFragment(int position) {
            return registeredFragments.get(position);
        }
    }
    

    其实关键点就一点,getItem 这个方法不是 get Fragment,其实称之为 create Fragment更为合适,你的 Fragment 创建逻辑可以放这个方法里。

    Google 的 ViewPager2的介绍中,也有类似的话术:

    如果你想保存一份 Fragment 的引用,可以利用 instantiateItem,因为这个方法,在重建也会被回调(参考上述源码)。

    上述的问题stackoverflow也有相关讨论,这个代码就是来自下面这个链接,也可以阅读下:

    https://stackoverflow.com/questions/8785221/retrieve-a-fragment-from-a-viewpager

    好了,如果你发现项目中有少量的跟 Fragment 相关的空指针或者一些Fragment 状态相关的崩溃,很可能就是上述原因引起的,检查下代码吧。

    不要觉得很多文章说的复现步骤是横纵屏切换,以为设置为竖屏就能避免该问题,并不能。

    最后送个思考题:

    上文说的是 Activity 被系统内存不足杀死会造成如上文所述情况,那么如果整个app进程都因为内存不足被干掉呢?会是什么现象?

    最后

    最后我想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

    这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

    相信它会给大家带来很多收获:

    上述【高清技术脑图】以及【配套的架构技术PDF】可以 关注我 【主页简介】 或者【简信】免费获取

    Android学习PDF+架构视频+面试文档+源码笔记

    当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

    相关文章

      网友评论

        本文标题:Android知识笔记:记录一个至今仍有很多人写错的技术点,避免

        本文链接:https://www.haomeiwen.com/subject/jpitmhtx.html