Android中ViewPager懒加载的优化详解

  目录

  01.ViewPager简单介绍

  ViewPager使用一个键对象来关联每一页,而不是管理View。这个键用于追踪和唯一标识在adapter中独立位置中的一页。调用方法startUpdate(ViewGroup)表明ViewPager中的内容需要更改。

  通过调用一次或多次调用instantiateItem(ViewGroup, int)来构造页面视图。

  调用destroyItem(ViewGroup, int, Object)来取消ViewPager关联的页面视图。

  最后,当一次更新(添加和/或移除)完成之后将会调用finishUpdate(ViewGroup)来通知adapter, 提交关联和/或取消关联的操作。这三个方法就是用于ViewPager使用回调的方式来通知PagerAdapter来管理其中的页面。

  一个非常简单的方式就是使用每页视图作为key来关联它们自己,在方法instantiateItem(ViewGroup, int)中创建和添加它们到ViewGroup之后,返回该页视图。与之相匹配的方法destroyItem(ViewGroup, int, Object)实现从ViewGroup中移除视图。当然必须在isViewFromObject(View, Object)中这样实现:return view == object;.

  PagerAdapter支持数据改变时刷新界面,数据改变必须在主线程中调用,并在数据改变完成后调用方法notifyDataSetChanged(), 和AdapterView中派生自BaseAdapter相似。一次数据的改变可能关联着页面的添加、移除、或改变位置。ViewPager将根据adapter中实现getItemPosition(Object)方法返回的结果,来判断是否保留当前已经构造的活动页面(即重用,而不完全自行构造)。

  02.ViewPager弊端分析

  普通的viewpager如果你不使用setoffscreenpagelimit(int limit)这个方法去设置默认加载数的话是会默认加载页面的左右两页的,也就是说当你进入viewpager第一页的时候第二页和第一页是会被一起加载的,这样同时加载就会造成一些问题,试想我们如果设置了setoffscreenpagelimit为3的话,那么进入viewpager以后就会同时加载4个fragment,像我们平时的项目中在这些fragment中一般都是会发送网络请求的,也就是说我们有4个fragment同时发送网络请求去获取数据,这样的结果显而易见给用户的体验是不好的(如:浪费用户流量,造成卡顿等等)。

  懒加载的实现弊端

  03.ViewPager预加载

  ViewPager的预加载机制。那么,我们可不可以设置ViewPager的预加载为0,不就解决问题了吗?也就是代码这样操作:

  vp.setOffscreenPageLimit(0);

  然后看一下源码

  public void setOffscreenPageLimit(int limit) {

  if (limit < 1) {

  Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1);

  limit = 1;

  }

  if (limit != this.mOffscreenPageLimit) {

  this.mOffscreenPageLimit = limit;

  this.populate();

  }

  }

  即使你设置为0,那么还是会在里面判断后设为默认值1。所以这个方法是行不通的。

  ViewPager默认情况下的加载,当切换到当前页面时,会默认预加载左右两侧的布局到ViewPager中,尽管两侧的View并不可见的,我们称这种情况叫预加载;由于ViewPager对offscreenPageLimit设置了限制,页面的预加载是不可避免……

  初始化缓存(mOffscreenPageLimit == 1)

  当初始化时,当前显示页面是第0页;mOffscreenPageLimit为1,所以预加载页面为第1页,再往后的页面就不需要加载了(这里的2, 3, 4页)

  中间页面缓存(mOffscreenPageLimit == 1)

  当向右滑动到第2页时,左右分别需要缓存一页,第0页就需要销毁掉,第3页需要预加载,第4页不需要加载

  04.ViewPager部分源码

  ViewPager.setAdapter方法

  ViewPager.populate(int newCurrentItem)

  ViewPager.dataSetChanged()

  ViewPager.scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected)

  滑动到指定页面,内部会触发OnPageChangeListener

  ViewPager.calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)

  05.懒加载出现问题

  发现Fragment中有一个setUserVisibleHint(boolean isVisibleToUser)方法,这个方法就是告诉用户,UI对用户是否可见,可以做懒加载初始化操作。

  因为ViewPager会加载好多Fragment,为了节省内容等会在Fragment不可见的某个时候调用onDestroyView()将用户界面销毁掉但是Fragment的实例还在,所以可能第一次加载没有问题,但是再次回到第一个Fragment再去加载的时候就会出现UI对用户可见但是视图还没有初始化。

  懒加载需要处理的几个问题

  预加载,虽然没有显示在界面上,但是当前页面的上一页和下一页的Fragment已经执行了一个Fragment能够显示在界面上的所有生命周期方法,但是我们想在跳转到该页时才真正构造数据视图和请求数据。那么我们可以使用一个占位视图,那么可以想到使用ViewStub,当真正跳转到该页时,执行ViewStub.inflate()方法,加载真正的数据视图和请求数据。

  视图保存

  当某一页超出可视范围和预加载范围,那么它将会被销毁,FragmentStatePagerAdapter销毁整个Fragment, 我们可以自己保存该Fragment,或使用FragmentPagerAdapter让FragmentTransition来保留Fragment的引用。虽然这样,但是它的周期方法已经走完,那么我们只能手动的保存Fragment根View的引用,当再次重新进入新的声明周期方法时,返回原来的View

  是否已经被用户所看到

  其实本身而言,FragmentManager并没有提供为Fragment被用户所看到的回调方法,而是在FragmentPagerAdapter和FragmentStatePagerAdapter中,调用了Fragment.setUserVisibleHint(boolean)来表明Fragment是否已经被作为primaryFragment. 所以这个方法可以被认为是一个回调方法。

  06.如何实现预加载机制

  主要的方法是Fragment中的setUserVisibleHint(),此方法会在onCreateView()之前执行,当viewPager中fragment改变可见状态时也会调用,当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法,使用getUserVisibleHint() 可以返回fragment是否可见状态。

  在BaseLazyFragment中需要在onActivityCreated()及setUserVisibleHint()方法中都调了一次lazyLoad() 方法。如果仅仅在setUserVisibleHint()调用lazyLoad(),当默认首页首先加载时会导致viewPager的首页第一次展示时没有数据显示,切换一下才会有数据。因为首页fragment的setUserVisible()在onActivityCreated() 之前调用,此时isPrepared为false 导致首页fragment 没能调用onLazyLoad()方法加载数据。

  /**

  *

  * @author yangchong

  * blog : https://github.com/yangchong211

  * time : 2017/7/22

  * desc : 懒加载

  * revise: 懒加载时机:onCreateView()方法执行完毕 + setUserVisibleHint()方法返回true

  *

  */

  public abstract class BaseLazyFragment extends BaseFragment {

  /*

  * 预加载页面回调的生命周期流程:

  * setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()-->

  * onActivityCreate() --> onStart() --> onResume()

  */

  /**

  * 懒加载过

  */

  protected boolean isLazyLoaded = false;

  /**

  * Fragment的View加载完毕的标记

  */

  private boolean isPrepared = false;

  /**

  * 第一步,改变isPrepared标记

  * 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isPrepared标记为true,并调用lazyLoad()方法

  */

  @Override

  public void onActivityCreated(@Nullable Bundle savedInstanceState) {

  super.onActivityCreated(savedInstanceState);

  isPrepared = true;

  //只有Fragment onCreateView好了

  //另外这里调用一次lazyLoad()

  lazyLoad();

  }

  /**

  * 第二步

  * 此方法会在onCreateView()之前执行

  * 当viewPager中fragment改变可见状态时也会调用

  * 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法

  * true表示当前页面可见,false表示不可见

  */

  @Override

  public void setUserVisibleHint(boolean isVisibleToUser) {

  super.setUserVisibleHint(isVisibleToUser);

  LogUtil.d("setUserVisibleHint---"+isVisibleToUser);

  //只有当fragment可见时,才进行加载数据

  if (isVisibleToUser){

  lazyLoad();

  }

  }

  /**

  * 调用懒加载

  * 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载

  */

  private void lazyLoad() {

  if (getUserVisibleHint() && isPrepared && !isLazyLoaded) {

  showFirstLoading();

  onLazyLoad();

  isLazyLoaded = true;

  } else {

  //当视图已经对用户不可见并且加载过数据,如果需要在切换到其他页面时停止加载数据,可以覆写此方法

  if (isLazyLoaded) {

  stopLoad();

  }

  }

  }

  /**

  * 视图销毁的时候讲Fragment是否初始化的状态变为false

  */

  @Override

  public void onDestroyView() {

  super.onDestroyView();

  isLazyLoaded = false;

  isPrepared = false;

  }

  /**

  * 第一次可见时,操作该方法,可以用于showLoading操作,注意这个是全局加载loading

  */

  protected void showFirstLoading() {

  LogUtil.i("第一次可见时show全局loading");

  }

  /**

  * 停止加载

  * 当视图已经对用户不可见并且加载过数据,但是没有加载完,而只是加载loading。

  * 如果需要在切换到其他页面时停止加载数据,可以覆写此方法。

  * 存在问题,如何停止加载网络

  */

  protected void stopLoad(){

  }

  /**

  * 第四步:定义抽象方法onLazyLoad(),具体加载数据的工作,交给子类去完成

  */

  @UiThread

  protected abstract void onLazyLoad();

  }

  onLazyLoad()加载数据条件

  还有几个细节需要优化一下

  07.懒加载配合状态管理器

  什么是状态管理器?

  如何降低偶性和入侵性

  让View状态的切换和Activity彻底分离开,必须把这些状态View都封装到一个管理类中,然后暴露出几个方法来实现View之间的切换。 在不同的项目中可以需要的View也不一样,所以考虑把管理类设计成builder模式来自由的添加需要的状态View。

  那么如何降低耦合性,让代码入侵性低。方便维护和修改,且移植性强呢?大概具备这样的条件……

  那么具体怎么操作呢?

  可以自由切换内容,空数据,异常错误,加载,网络错误等5种状态。父类BaseFragment直接暴露5中状态,方便子类统一管理状态切换,这里fragment的封装和activity差不多。

  /**

  *

  * @author yangchong

  * blog : https://github.com/yangchong211

  * time : 2017/7/20

  * desc : fragment的父类

  * revise: 注意,该类具有懒加载

  *

  */

  public abstract class BaseStateFragment extends BaseLazyFragment {

  protected StateLayoutManager statusLayoutManager;

  private View view;

  @Nullable

  @Override

  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,

  @Nullable Bundle savedInstanceState) {

  if(view==null){

  view = inflater.inflate(R.layout.base_state_view, container , false);

  initStatusLayout();

  initBaseView(view);

  }

  return view;

  }

  @Override

  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {

  super.onViewCreated(view, savedInstanceState);

  initView(view);

  initListener();

  }

  @Override

  public void onActivityCreated(@Nullable Bundle savedInstanceState) {

  super.onActivityCreated(savedInstanceState);

  }

  /**

  * 获取到子布局

  * @param view view

  */

  private void initBaseView(View view) {

  LinearLayout llStateView = view.findViewById(R.id.ll_state_view);

  llStateView.addView(statusLayoutManager.getRootLayout());

  }

  /**

  * 初始化状态管理器相关操作

  */

  protected abstract void initStatusLayout();

  /**

  * 初始化View的代码写在这个方法中

  * @param view view

  */

  public abstract void initView(View view);

  /**

  * 初始化监听器的代码写在这个方法中

  */

  public abstract void initListener();

  /**

  * 第一次可见状态时,showLoading操作,注意下拉刷新操作时不要用该全局loading

  */

  @Override

  protected void showFirstLoading() {

  super.showFirstLoading();

  showLoading();

  }

  /*protected void initStatusLayout() {

  statusLayoutManager = StateLayoutManager.newBuilder(activity)

  .contentView(R.layout.common_fragment_list)

  .emptyDataView(R.layout.view_custom_empty_data)

  .errorView(R.layout.view_custom_data_error)

  .loadingView(R.layout.view_custom_loading_data)

  .netWorkErrorView(R.layout.view_custom_network_error)

  .build();

  }*/

  /*---------------------------------下面是状态切换方法-----------------------------------------*/

  /**

  * 加载成功

  */

  protected void showContent() {

  if (statusLayoutManager!=null){

  statusLayoutManager.showContent();

  }

  }

  /**

  * 加载无数据

  */

  protected void showEmptyData() {

  if (statusLayoutManager!=null){

  statusLayoutManager.showEmptyData();

  }

  }

  /**

  * 加载异常

  */

  protected void showError() {

  if (statusLayoutManager!=null){

  statusLayoutManager.showError();

  }

  }

  /**

  * 加载网络异常

  */

  protected void showNetWorkError() {

  if (statusLayoutManager!=null){

  statusLayoutManager.showNetWorkError();

  }

  }

  /**

  * 加载loading

  */

  protected void showLoading() {

  if (statusLayoutManager!=null){

  statusLayoutManager.showLoading();

  }

  }

  }

  //如何切换状态呢?

  showContent();

  showEmptyData();

  showError();

  showLoading();

  showNetWorkError();

  //或者这样操作也可以

  statusLayoutManager.showLoading();

  statusLayoutManager.showContent();

  状态管理器的设计思路

  StateFrameLayout是继承FrameLayout自定义布局,主要是存放不同的视图,以及隐藏和展示视图操作

  StateLayoutManager是状态管理器,主要是让开发者设置不同状态视图的view,以及切换视图状态操作

  几种异常状态要用ViewStub,因为在界面状态切换中loading和内容View都是一直需要加载显示的,但是其他的3个只有在没数据或者网络异常的情况下才会加载显示,所以用ViewStub来加载他们可以提高性能。

  OnRetryListener,为接口,主要是重试作用。比如加载失败了,点击视图需要重新刷新接口,则可以用到这个。开发者也可以自己设置点击事件

  关于状态视图切换方案,目前市场有多种做法,具体可以看我的这篇博客:Android中不同状态页面管理优化

  到此这篇关于Android中ViewPager懒加载的优化详解的文章就介绍到这了,更多相关Android ViewPager懒加载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

  您可能感兴趣的文章: