ViewPager + Fragment 生命周期简单验证



ViewPager + Fragment 是一种简单好用的结构。使用 ViewPager 控件可以自动管理多个 Fragment 的生命周期,同时只会加载使页面切换平滑的必要数量的 Fragment。

我们先来做一个生命周期调用的实验,再对其中容易理解错误的地方进行说明。

实验代码及说明

完整代码

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final List<Fragment> fragmentList = new ArrayList<>();
        fragmentList.add(BlankFragment1.newInstance());
        fragmentList.add(BlankFragment2.newInstance());
        fragmentList.add(BlankFragment3.newInstance());

        ViewPager viewPager = findViewById(R.id.view_pager);
        viewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {

            @Override
            public Fragment getItem(int position) {
                return fragmentList.get(position);
            }

            @Override
            public int getCount() {
                return fragmentList.size();
            }
        });
        viewPager.setCurrentItem(0); // 当前选择的 Item
        viewPager.setOffscreenPageLimit(1); // 预加载范围,最低为1
    }
}

setOffscreenPageLimit(int) 该方法设定了预加载的范围,指的是与当前 Item 距离(无论“左侧”“右侧”)为该设定值以内的 Item 都会被预加载。这样可以保证相邻页面切换时平滑。

public class BlankFragment1 extends Fragment {

    private final String mName = "F1";

    public BlankFragment1() {

    }

    public static BlankFragment1 newInstance() {
        return new BlankFragment1();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.d("MyDebug", mName + " onCreate");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_blank, container, false);

        TextView textView = root.findViewById(R.id.tv_name);
        textView.setText(mName);

        Log.d("MyDebug", mName + " onCreateView");

        return root;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d("MyDebug", mName + " setUserVisibleHint" + ":" + isVisibleToUser);
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d("MyDebug", mName + " onResume");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d("MyDebug", mName + " onPause");
    }
}

这里请注意 setUserVisibleHint(boolean isVisibleToUser) 这个看上去有些陌生的方法。由于 ViewPager 预加载机制,Fragment 的 onResumeonPause 并不一定会在 Fragment 显示时调用。所以一些显示时调用的逻辑改为写在 setUserVisibleHint 中。这是网上搜索出得比较流行的方式。

实验结果

// 启动应用
F1 setUserVisibleHint:false
F2 setUserVisibleHint:false
F1 setUserVisibleHint:true
F1 onCreate
F2 onCreate
F1 onCreateView
F1 onResume
F2 onCreateView
F2 onResume
// 向右滑动
F3 setUserVisibleHint:false
F1 setUserVisibleHint:false
F2 setUserVisibleHint:true
F3 onCreate
F3 onCreateView
F3 onResume
// 向右滑动
F2 setUserVisibleHint:false
F3 setUserVisibleHint:true
F1 onPause

几点结论:

  • ViewPager 的加载流程为
    1. 执行 setUserVisibleHint
      • 先调用所有未 Create 的 Fragment setUserVisibleHint(false)
      • 再调用已 Create 被隐藏的 Fragment setUserVisibleHint(false)
      • 最后调用将被显示的 Fragment setUserVisibleHint(true) (无论被显示的 Fragment 是否已经 Create)
    2. 未 Create 的 Fragment 调用 onCreate
    3. 上一步调用过 onCreate 的 Fragment 分别调用 onCreateViewonResume
  • Fragment 的 onResumeonPause 的调用与预加载范围一致,所有在范围内的 Fragment 都处于 active(活动)的状态。
  • 如流程分析所述,首次加载 Fragment 时 setUserVisibleHint(false) 的调用早于 onCreate。所以,如果在 setUserVisibleHint 方法中操作 View 可能会引起空指针异常——因为 View 还没有被创建。

其他

因为最近在 Coding 时对 ViewPager + Fragment 的使用出现不少困惑,所以在这里来亲自验证。ViewPager 在“滑动切换”显示的表现上,很容易让人认为多个 Fragment 是被一个个的执行 onResumeonPause 生命周期的。同理,当查到用 setUserVisibleHint 做替代的方法时,我也认为如此,所以导致了出现 View 为空的问题(“为什么有的时候会 CreateView 而有的时候又没有!”)。

问题总出现在思维的死角。❖