通常我们 ViewPager 在使用的是一般都是结合 Fragment 一起使用,我们先来搭一个简单的使用界面,最终搭建出来的效果如下:
简单的 ViewPager +? Fragment 的实现,比较简单,就不贴代码的实现了,我们接下来内容的讲解;
mViewPager.setOffscreenPageLimit(2);
传递几就缓存几个页面?;那么如果我们传递 0,就不会进行缓存了吗?以及它到底是怎么缓存的呢?又是缓存了几个呢?假设我们传递进去的是 2,当我们滑动到 F3 的时候
那么缓存的就是 左边 2 个,右边 2 个,加上当前的 F3 的缓存,一共就是缓存了 5 个;
如果停留在 F2,则左边只能缓存 1 个,右边缓存 2 个,加上当前的 F2 的缓存,一共是 4 个;
如果停留在 F1,则左边没有能缓存的,右边缓存 2 个,加上当前的 F1 的缓存,一共是 3 个;
同理,停留在 F4 和 F5 一样的缓存个数;
如果?setOffscreenPageLimit?设置的是 1,当前停留在 F3 则左右两边各缓存 1 个,加上当前的 F3 的缓存,一共是缓存了 3 个;
其他的都会被销毁;
如果?setOffscreenPageLimit?设置的是 0,则无效,会改成 1;
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
如果传递进去的是 0,则无效,ViewPager 会强制更改成 1;
mViewPager.setOffscreenPageLimit(2);
也是通过这个方法,进行页面的预加载,但是和缓存有一些区别
如果当前切换到了 F1 则预加载 F2、F3,如果从 F1 切换到了 F3,则预加载 F4、F5;
预加载只会预加载滚动方向的前 x 个,如果是从F1 -> F5,那么预加载的就是右边的 x 个,如果是从 F5 -> F1,就是预加载左边的 x 个;
因为?setOffscreenPageLimit 设置 0 无效,会强制更改成 1,如果我们从 F2 切换到 F3 的时候,会预加载 F4,但是此时 F4 还不可见,但是系统给我们进行了预加载,如果 F4 上做了大量的网络请求,就会造成资源的浪费,影响性能;
预加载的越多,浪费的性能就越多;如果一个 Fragment 占用 1M,后面预加载的个数比较多的情况下,容易OOM;预加载的 Fragment 在进行网络请求,会浪费流量,还会卡顿;
为了规避预加载的问题,我们需要使用懒加载机制来规避这个问题,减少性能消耗;
页面可见的时候才进行加载;依托这个 setUserVisibleHint(isVisibleToUser: Boolean) 来实现懒加载;
ViewPager渲染原理
onMeasure/setAdapter/setOffscreenPageLimit 等入口最终都会走到 populate 这个方法,我们进入这个方法看下,这个方法比较长,我们只需要找其中的关键点即可,因为 ViewPager 依赖 Adapter 才能渲染 View,所以我们只需要在这个方法中找到 mAdapter 的相关调用:
mAdapter.startUpdate(this);
mAdapter.destroyItem(this, pos, ii.object);
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
mAdapter.finishUpdate(this);
mAdapter.instantiateItem(this, position);
这几个方法对应的功能如下:
startUpdate
startUpdate 默认空实现,开放给开发者一些初始化的准备工作,在开发者自己的 Adapter 中复写实现;
public void startUpdate(@NonNull View container) {}
instantiateItem
创建适配的 item 数据,这里进行的操作就是把我们的 Fragment 加到事务管理器中;
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?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
// 如果 Fragment 已经存在,直接调用 attach 方法,最终调用到 onAttach 方法
mCurTransaction.attach(fragment);
} else {
// 从我们自定义的 Adapter 中获取实例化的 Fragment;
fragment = getItem(position);
// 获取到实例化的 Fragment 后并添加到事务管理器中
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
// 设置 Fragment 不可见
fragment.setUserVisibleHint(false);
}
return fragment;
}
setPrimaryItem
设置当前显示的 item 的数据,这里就只是进行了 setUserVisibleHint 的操作;
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
// 非当前 Fragment 设置成 false
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
fragment.setMenuVisibility(true);
// 设置当前 item 可见
fragment.setUserVisibleHint(true);
mCurrentPrimaryItem = fragment;
}
}
finishUpdate
public void finishUpdate(@NonNull ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
这里进行了事务管理器的激活,也就是当调用了 comitNowAllowingStateLoss 方法之后,Fragment 的生命周期就开始执行了;
通过 Adapter 的调用顺序(setPrimaryItem - finishUpdate)我们可以看到,setUserVisibleHint 的执行顺序是早于 Fragment 的生命周期的;
我们通过下面这张图可以看下 Fragment 的状态
当我们从 tab1 切换到 tab2 的时候,tab3 执行的是 setUserVisibleHint(false),tab1 执行的也是?setUserVisibleHint(false),只有 tab2 执行的是?setUserVisibleHint(true)
Fragment 的生命周期打印如下:
所以,我们就可以根据 setUserVisibleHint 的可见与不可见,来管理 Fragment 是否要懒加载;
为了方便事件的分发和生命周期管理,我们这里实现了一个 BaseLazyFragment
public abstract class BaseLazyFragment extends Fragment {
private View rootView = null;
private boolean isViewCreated = false;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e("","onCreateView: ");
if (rootView == null) {
rootView = inflater.inflate(getLayoutRes(),container, false);
}
isViewCreated = true;
initView(rootView);
return rootView;
}
// 让子类完成,初始化布局,初始化控件
protected abstract void initView(View rootView);
protected abstract int getLayoutRes();
@Override
public void onResume() {
super.onResume();
Log.e("","onResume");
}
@Override
public void onPause() {
super.onPause();
Log.e("","onPause");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.e("","onDestroyView");
}
}
根据 setUserVisibleHint 判断 Framgent 是否可见来进行懒加载;
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e("","setUserVisibleHint: ");
if (isVisibleToUser) {
// 加载数据
onFragmentLoadStart();
} else {
// 停掉数据
onFragmentLoadStop();
}
}
抽取了 BaseLazyFragment,我们需要一个子 Fragment 继承这个 BaseLazyFragment 实现相关方法
public class MyFragment extends BaseLazyFragment {
private static final String TAG = "MyFragment";
public static final String POSITION = "Position";
private ImageView imageView;
private TextView textView;
private int tabIndex;
public static MyFragment newInstance(int position) {
Bundle bundle = new Bundle();
bundle.putInt("Position", position);
MyFragment fragment = new MyFragment();
fragment.setArguments(bundle);
return fragment;
}
@Override
protected void initView(View view) {
Log.d(TAG, tabIndex + " fragment " + "initView: " );
tabIndex = getArguments().getInt(POSITION);
imageView = view.findViewById(R.id.iv_content);
textView = view.findViewById(R.id.tv_loading);
}
@Override
protected int getLayoutRes() {
return R.layout.fragment_vp;
}
@Override
public void onFragmentLoad() {
super.onFragmentLoad();
textView.setText("startLoad");
}
@Override
public void onFragmentLoadStop() {
super.onFragmentLoadStop();
textView.setText("stopLoad");
}
}
一个简单实现的子类,然后我们编译运行看下效果,结果程序运行,我们直接发生了崩溃:
textView 的空指针异常,我们明明 findViewById 了,但是还是报了空指针异常,我们来看看怎么回事?
根据事件分发我们直到,setUserVisibleHint 早于 Fragment 的生命周期,也就是说分发 onFragmentLoadStop 的 Fragment 还没有创建以及初始化相关 View;所以我们在分发 onFragmentLoadStop 的时候,需要判断下 View 是不是已经创建了,创建了才执行 View 的更新操作;
private boolean isViewCreated = false;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e("","setUserVisibleHint: ");
if (isViewCreated) {
if (isVisibleToUser) {
// 加载数据
onFragmentLoad();
} else {
// 停掉数据
onFragmentLoadStop();
}
}
}
我们在运行看下效果:
可以看到,运行不崩溃了,并且每次 Fragment 可见的时候,才会执行 startLoad 逻辑;但是,我们的第一个 Fragment 却一直都是 loading 态
这是为什么呢?还是事件分发的问题,setUserVisibleHint 早于 Fragment 的生命周期,当分发为 true 之后,Fragment 的生命周期才执行,也就是说第一个 Fragment 可见的时候,isViewCreated 还是 false,所以导致?setUserVisibleHint 为 true 的时候无法执行 onFragmentLoad 方法,导致 UI 无法更新,一直显示 loading 中;
解决第一个 Fragment 状态不更新的问题,所以需要我们在 onCreateView 中根据当前 Fragment 可见的时候,手动分发下状态,修改如下:
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e("","onCreateView: ");
if (rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false);
}
initView(rootView);
isViewCreated = true;
if (getUserVisibleHint()) {
setUserVisibleHint(true);
}
return rootView;
}
我们运行看下效果:
可以看到,第一次进来的时候,不再是 loading 了,更新成 startLoad 了;但是如果这个时候我们来看下生命周期的调用会发现,存在重复加载更新和重复暂停更新的问题
也就说 从 不可见 到 可见 才算 可见 (变化的过程)
从 可见 到 不可见 才算 不可见(变化的过程),那么这个问题我们应该如何处理呢?
我们需要记录上一次状态和本地状态,做对比,如果一样则不处理,不一样才处理;
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(TAG,"setUserVisibleHint: ");
if (isViewCreated) {
if (isVisibleToUser && !isVisibleStateUp) {
// 当前可见,但是上一次不可见
dispatchVisibleHint(true);
} else if (!isVisibleToUser && isVisibleStateUp) {
// 当前不可见,但是上一次可见
dispatchVisibleHint(false);
}
}
}
//
private void dispatchVisibleHint(boolean isVisibleToUser) {
isVisibleStateUp = isVisibleToUser;
if (isVisibleToUser) {
// 加载数据
onFragmentLoad();
} else {
// 停掉数据
onFragmentLoadStop();
}
}
我们运行看下效果,可以看到,我们的调用栈都只调用了一次,似乎已经很完美了;
但是如果我们在 Fragment 上执行二跳的时候呢?假设我们从 Fragment 跳转到一个新的 Activity;
setUserVisibleHint 看起来并没有执行;那么它为什么没有执行呢?因为它的执行是在 ViewPager 中通过 adapter 调用的时候才执行,也就是我们在二跳的时候并没有触发 adapter 的调用;
populate -> mAdapter.setPrimaryItem -> setUserVisibleHint 调用的,跟生命周期没有任何关系;
也就是我们想分发这个状态,就要和生命周期管理起来,那么我们就需要在 Fragment 的 onResume 和 onPasue 增加逻辑处理
@Override
public void onResume() {
super.onResume();
Log.e(TAG,"onResume");
if (getUserVisibleHint() && !isVisibleStateUp) {
// 当前可见,并且上一次不可见的时候 分发
dispatchVisibleHint(true);
}
}
@Override
public void onPause() {
super.onPause();
Log.e(TAG,"onPause");
if (!getUserVisibleHint() && isVisibleStateUp) {
dispatchVisibleHint(false);
}
}
我们运行看下效果:
可以看到,二跳出去的时候,fragment 5 停止了加载,返回的时候
又重新进行了加载,看起来也似乎很完美了;
如果我们把 ViewPager + Fragment 在嵌套到一个 Fragment 中的时候,我们的状态还能正常吗?就是下面这种效果:
嵌套层架如下:
类似这样的一个层级嵌套,我们参照ViewPager + Fragment 修改我们的其中一个 Fragment 让它内部在套一个 ViewPager + Fragment;我们运行看下效果:
可以看到,Fragment1 正常加载的时候,Fragment2 下的子 Framgent 也执行了加载逻辑,看起来不符合我们期望的结果;
这是因为 ViewPager? 的预加载,Fragment1 展示的时候,预加载了 Fragment2,Fragment2 因为是 ViewPager + Fragment 的结构,导致 ViewPager 中的第一个 Framgent 执行了相关声明周期逻辑;
那么我们需要让 Fragment2 的子 Fragment 不执行相关生命周期逻辑;也就是说我们需要判断下 Fragment2 是不是真正的可见,修改如下,增加父 Framgent 是否可见,同时当我们切换到 Fragment2 的时候 要分发相关状态给 Framgent2 的子 Fragment;
protected boolean isParentVisible() {
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof BaseLazyFragment) {
BaseLazyFragment baseLazyFragment = (BaseLazyFragment) parentFragment;
return !baseLazyFragment.isVisibleStateUp;
}
return false;
}
private void dispatchVisibleHint(boolean isVisibleToUser) {
isVisibleStateUp = isVisibleToUser;
if (isVisibleStateUp && isParentVisible()) {
return;
}
if (isVisibleToUser) {
// 加载数据
onFragmentLoad();
// 手动 分发嵌套执行
dispatchChildVisibleState(true);
} else {
// 停掉数据
onFragmentLoadStop();
dispatchChildVisibleState(false);
}
}
protected void dispatchChildVisibleState(boolean isVisibleToUser) {
FragmentManager childFragmentManager = getChildFragmentManager();
List<Fragment> fragments = childFragmentManager.getFragments();
if (fragments.size() > 0) {
for (Fragment fragment : fragments) {
if (fragment instanceof BaseLazyFragment
&& !fragment.isHidden()
&& fragment.getUserVisibleHint()) {
((BaseLazyFragment) fragment).dispatchVisibleHint(isVisibleToUser);
}
}
}
}
我们运行看下打印结果:
当我们切换到 Fragment2 的时候,内部的事件才开始分发;好了,完美,我们针对 ViewPager的懒加载优化到这里就完成了,我们的 BaseLazyFragment 的完整实现如下:
public abstract class BaseLazyFragment extends Fragment {
private static final String TAG = "MyFragment";
private View rootView = null;
private boolean isViewCreated = false;
private boolean isVisibleStateUp = false;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e(TAG,"onCreateView: ");
if (rootView == null) {
rootView = inflater.inflate(getLayoutRes(), container, false);
}
initView(rootView);
isViewCreated = true;
if (getUserVisibleHint()) {
setUserVisibleHint(true);
}
return rootView;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(TAG,"setUserVisibleHint: ");
if (isViewCreated) {
if (isVisibleToUser && !isVisibleStateUp) {
// 当前可见,但是上一次不可见
dispatchVisibleHint(true);
} else if (!isVisibleToUser && isVisibleStateUp) {
// 当前不可见,但是上一次可见
dispatchVisibleHint(false);
}
}
}
private void dispatchVisibleHint(boolean isVisibleToUser) {
isVisibleStateUp = isVisibleToUser;
if (isVisibleStateUp && isParentVisible()) {
return;
}
if (isVisibleToUser) {
// 加载数据
onFragmentLoad();
// 手动 分发嵌套执行
dispatchChildVisibleState(true);
} else {
// 停掉数据
onFragmentLoadStop();
dispatchChildVisibleState(false);
}
}
protected boolean isParentVisible() {
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof BaseLazyFragment) {
BaseLazyFragment baseLazyFragment = (BaseLazyFragment) parentFragment;
return !baseLazyFragment.isVisibleStateUp;
}
return false;
}
protected void dispatchChildVisibleState(boolean isVisibleToUser) {
FragmentManager childFragmentManager = getChildFragmentManager();
List<Fragment> fragments = childFragmentManager.getFragments();
if (fragments.size() > 0) {
for (Fragment fragment : fragments) {
if (fragment instanceof BaseLazyFragment
&& !fragment.isHidden()
&& fragment.getUserVisibleHint()) {
((BaseLazyFragment) fragment).dispatchVisibleHint(isVisibleToUser);
}
}
}
}
@Override
public void onResume() {
super.onResume();
Log.e(TAG,"onResume");
if (getUserVisibleHint() && !isVisibleStateUp) {
// 当前可见,并且上一次不可见的时候 分发
dispatchVisibleHint(true);
}
}
@Override
public void onPause() {
super.onPause();
Log.e(TAG,"onPause");
if (!getUserVisibleHint() && isVisibleStateUp) {
dispatchVisibleHint(false);
}
}
// 让子类完成,初始化布局,初始化控件
protected abstract void initView(View rootView);
protected abstract int getLayoutRes();
public void onFragmentLoadStop() {
Log.e(TAG, "onFragmentLoadStop");
}
public void onFragmentLoad() {
Log.e(TAG,"onFragmentLoad");
}
}
后面我会单独写一章详细分析 ViewPager2 的文章,如果大家的点赞率比较高的话;
简历上可写:深度理解 ViewPager 的 五次 懒加载优化;
带你玩转 NestedScrollView 嵌套滑动机制;
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~