ViewPager 的预加载与懒加载

11 篇文章 0 订阅

1、缓存与预加载

ViewPager 为了提升页面的展示速度,默认提供了缓存与预加载机制,具体是指:

  • 缓存:ViewPager 会缓存处于缓存范围内的页面,将它们存储在内存中。当用户滑动到缓存的页面时,无须创建页面就可以快速的展示给用户
  • 预加载:ViewPager 在展示当前页面的同时,会提前加载下一个页面,在下一个页面还无须展示时就创建他并开始其生命周期,以减少页面的加载时间

简单说,缓存会节省创建页面的时间,预加载会节省加载页面的时间。至于缓存范围与预加载哪些页面,是通过 ViewPager 的 setOffscreenPageLimit() 设置的,下面我们通过一个简单的例子来看二者在实际项目中的表现。

1.1 实例演示

Demo 实际就是 ViewPager 与 BottomNavigationView 结合的简单使用,如下图:

请添加图片描述

BottomNavigationView 中有 5 个 Item 对应 ViewPager 管理的 5 个 Fragment,创建 Fragment 时给它们相应位置的索引 1 ~ 5,然后在其所有生命周期方法以及 setUserVisibleHint() 中打印 log:

	override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        // 标号在 setUserVisibleHint() 赋值,因为它在所有生命周期方法之前调用
        tabIndex = arguments!!.getInt(POSITION)
        super.setUserVisibleHint(isVisibleToUser)
        Log.d(TAG, "$tabIndex fragment setUserVisibleHint: $isVisibleToUser")
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        Log.d(TAG, "$tabIndex fragment onAttach: ")
    }
    ...

此外,设置 ViewPager 的 offscreenPageLimit 为 1:

		binding.viewPager.apply {
            offscreenPageLimit = 1
            ...
        }

接下来,启动程序,默认展示 T1 页面,输出如下 log:

请添加图片描述

fragment 1 走 onAttach() 到 onResume() 这个生命周期倒不难理解,但是 fragment 2 在未显示的情况下也走了同样的生命周期,这其实就是对 fragment 2 执行了预加载。

然后我们点击 T4:

请添加图片描述

与之前类似:

  • 先执行要显示页面的 onAttach()、onCreate()、onCreateView()、onStart()
  • 由于 offscreenPageLimit = 1,还会缓存 T4 左右各 1 个页面,即 T3、T5,执行这两个 fragment 的 onAttach()、onCreate()、onCreateView()
  • T4 走 onResume() 对用户可见之后,再走 T3、T5 的 onStart()、onResume()
  • 最后销毁不在缓存范围内的 Fragment —— 执行 T2、T1 的 onPause()、onStop()、onDestroyView(),但是并没有执行 onDestroy()

从以上两种情况的 log 中我们能看出:

  • 假如 offscreenPageLimit = n,那么 ViewPager 会缓存当前展示页面左侧和右侧各 n 个 fragment(如果某一侧的 fragment 数量不足 n 个,那么有几个就缓存几个),最多缓存2n + 1个 fragment
  • 不在缓存范围内的 fragment 实例并没有被真正的销毁(没走 onDestroy()),因此 fragment 被实例化后会一直占用内存
  • Fragment 所有的生命周期方法都是在 setUserVisibleHint() 之后执行的
  • 被缓存的那些 Fragment,也会被预加载,也可以说,缓存的目的就是为了预加载

1.2 关于 setUserVisibleHint()

该方法的作用是设置一个提示,告诉系统 ViewPager 内当前的 Fragment 是否可见:

	/**
	* 此提示默认为 true,并在 Fragment 实例状态保存和恢复时保持不变。我们可以将此
	* 值设置为 false 以表示 Fragment UI 已经不可见,系统可根据此信息处理 Fragment 
	* 生命周期。注意,此方法可以在生命周期之外调用,因此它与生命周期方法没有固定的顺序
	*/
	public void setUserVisibleHint(boolean isVisibleToUser) {
        FragmentStrictMode.onSetUserVisibleHint(this, isVisibleToUser);
        if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
                && mFragmentManager != null && isAdded() && mIsCreated) {
            mFragmentManager.performPendingDeferredStart(
                    mFragmentManager.createOrGetFragmentStateManager(this));
        }
        mUserVisibleHint = isVisibleToUser;
        mDeferStart = mState < STARTED && !isVisibleToUser;
        if (mSavedFragmentState != null) {
            // Ensure that if the user visible hint is set before the Fragment has
            // restored its state that we don't lose the new value
            mSavedUserVisibleHint = isVisibleToUser;
        }
    }

看完整个源码后,个人理解,这个方法只是提示系统,要显示哪一个页面,而不是该页面的 UI 是否可见。

看上面的 log,在走 Fragment 的生命周期之前,会对要显示的页面调用 setUserVisibleHint(true),对被缓存的页面和移出缓存范围内的页面调用 setUserVisibleHint(false),而 Fragment 生命周期开始运行之后,不论是走到 onResume() 还是 onPause(),都不会再调用 setUserVisibleHint(),也就是说,setUserVisibleHint() 的调用与 Fragment 生命周期无关,该可见性不是指 Fragment 的 UI 是否可见,仅仅是 ViewPager 作为是否要显示该 Fragment 的标记罢了。记住这一点,后续对实现懒加载有帮助。

通过 log 我们还能发现,setUserVisibleHint() 会在 Fragment 的生命周期运行之前被调用,它的调用时机有两个:

  1. 创建要显示的 Fragment 实例以及缓存 Fragment 时,需要先让这些 Fragment 的可见性为 false。比如第二张 log 截图中,要显示 T4,还要缓存 T3、T5,那么就先设置 fragment 3、4、5 的可见性为 false
  2. 要显示指定的 Fragment 时,设置该 Fragment 可见性为 true,同时检查不在缓存区的 Fragment,设置其可见性为 false。还是第二张 log 截图,由于设置 fragment 4 可见后,原本在缓存区内的 fragment 1 不在缓存区了,因此在设置 fragment 4 可见性为 true 之前,先要设置 fragment 1 不可见

在最新版本中,该方法已经被弃用,需改用 FragmentTransaction.setMaxLifecycle(Fragment, Lifecycle.State) 方法。如果仍手动调用以使用 setUserVisibleHint(),需要将传递 true 时实现的行为移动到 onResume() 中,传递 false 时实现的行为移到 onPause() 中。

虽然该方法已被弃用,但是作为实现 ViewPager 懒加载最原始的方式,我们还是需要了解他,并且知道它的调用时机,这部分的具体源码分析会在 2.3 节中集中体现。

2、populate()

如果预加载的 Fragment 中有很复杂的布局或者执行了很复杂的操作,那么它会占用很大的内存,在还不需要它显示的情况下就开始走生命周期运行,轻则影响性能,重则可能会造成 OOM,所以我们需要通过懒加载避免这种情况。

在写懒加载的代码之前,我们要对 ViewPager 的源码有一个充分的了解。ViewPager 独具特色的一点,就是需要设置缓存页面的数量,通过 setOffscreenPageLimit() 实现:

	/**
	* 设置空闲状态下当前页面两侧应保留的页面数,超出 limit 范围的页面会在需要时
	* 通过适配器重新创建。
	* 该方法被当作优化方法提供。如果事先知道需要支持的页面数量或者页面支持懒加载,
	* 调整此设置可以提高分页动画和交互的感知平滑度。如果只需保留少量(3~4)页面
	* 处于活动状态,那么用户在翻页时,为新视图布局所花费的时间会更少。
	* 应将此设置保持较低,特别是页面包含复杂布局的时候。此设置默认为 1
	*/
	public void setOffscreenPageLimit(int limit) {
		// DEFAULT_OFFSCREEN_PAGES = 1,如果 limit 小于 1 会被强制置为 1
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

而 setOffscreenPageLimit() 中的 populate() 才是核心方法,负责调用适配器,管理缓存。该方法的内容大致可以分为如下几步:

请添加图片描述

下面按照适配步骤介绍 populate() 的内容。

2.1 准备适配

	// mCurItem 表示当前正在显示的 Item 的 index
    void populate() {
        populate(mCurItem);
    }

	void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }

		// 绘制顺序的相关处理,省略……

        // 调用 PagerAdapter 的 startUpdate()
        mAdapter.startUpdate(this);
        ...
    }

当 populate() 参数的 newCurrentItem 与当前正在显示的 Item 索引不同时,说明 ViewPager 要显示的 Item 要发生变更,此时要记录下被替换掉的 Item 信息到 oldCurInfo 中,infoForPosition() 就是从 mItems 中按位置获取 ItemInfo:

	ItemInfo infoForPosition(int position) {
        for (int i = 0; i < mItems.size(); i++) {
            ItemInfo ii = mItems.get(i);
            if (ii.position == position) {
                return ii;
            }
        }
        return null;
    }

接下来调用 PagerAdapter.startUpdate(),基类是一个空方法,而在两个实现类 FragmentPagerAdapter 和 FragmentStatePagerAdapter 中,都是做了参数检查这样的初始化工作:

	@Override
    public void startUpdate(@NonNull ViewGroup container) {
        if (container.getId() == View.NO_ID) {
            throw new IllegalStateException("ViewPager with adapter " + this
                    + " requires a view id");
        }
    }

2.2 创建 Item 数据

	void populate(int newCurrentItem) {
		// 1.记录被替换掉的 ItemInfo,调用 PagerAdapter.startUpdate()...
		
		// 根据缓存页面数量计算开始缓存的位置和结束缓存的位置索引
        final int pageLimit = mOffscreenPageLimit;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);

        // 查找当前正在显示的 Item 在 mItems 中的位置,如果没有,则添加
        int curIndex = -1;
        ItemInfo curItem = null;
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

		// 如没有在 mItems 找到 curItem,则创建一个 ItemInfo 对象并
		// 将其插入到 mItems 中的合适位置
        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }
        ...
	}

实际上我们说的缓存,就是 mItems 这个 ArrayList<ItemInfo>,ItemInfo 封装了 Fragment 以及位置等信息:

	static class ItemInfo {
		// 这个 object 通常会被赋值为 Fragment
        Object object;
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;
    }

因此上面这段代码,就是常见的创建对象的套路:先在缓存中找有没有需要的对象,如果没有,才创建新对象并加入缓存中。创建与加入缓存的操作在 addNewItem() 中:

	// position 是该 ItemInfo 在 ViewPager 中的位置,而 index
	// 是 ItemInfo 在 mItems 中应该被插入的位置索引
	ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        // 调用 PagerAdapter.instantiateItem() 创建一个 Item
        ii.object = mAdapter.instantiateItem(this, position);
        ii.widthFactor = mAdapter.getPageWidth(position);
        // 加入缓存
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }

PagerAdapter 的 instantiateItem() 会抛出异常让子类重写该方法,这里我们看 FragmentPagerAdapter 的实现:

	@NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

		// getItemId() 默认会返回 position
        final long itemId = getItemId(position);

        // 如果通过 name 能找到对应的 Fragment 就 attach(),否则要通过
        // getItem() 获取到 Fragment 并 add()
        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));
        }
        // 如果 fragment 不是要显示的 Item,需要先设置其 UI 不可见
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            // 如果在构造 ViewPager 时传入 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 参数,
            // 那么就不用再通过 setUserVisibleHint() 来维护可见性了,直接用 Lifecycle
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
            } else {
            	// 设置提示,告诉系统当前 UI 对用户不可见
                fragment.setUserVisibleHint(false);
            }
        }

        return fragment;
    }

这里要解释下最后的 if (fragment != mCurrentPrimaryItem) 判断,mCurrentPrimaryItem 是要显示的 Fragment,现在我们只是将这个 Fragment 对象创建了出来,还没有将它设置为 mCurrentPrimaryItem(在 2.3 节中会讲到通过调用 PagerAdapter.setPrimaryItem() 设置),因此当前这个 if 判断是成立的,也就会调用 fragment.setUserVisibleHint(false),这也就解释了两张 log 截图中的第一句 log,要显示的 Fragment 的 setUserVisibleHint() 输出 false 的现象。

2.3 销毁 Item 数据,设置显示 Item

	void populate(int newCurrentItem) {
		// 1.记录被替换掉的 ItemInfo,调用 PagerAdapter.startUpdate()...
		// 2.如需显示的 fragment 的 ItemInfo 没有被缓存,则需调用
		// PagerAdapter.instantiateItem() 实例化一个 fragment 保存在
		// ItemInfo 的 object 字段中,同时管理其可见性...

		// ViewPager 进行滑动时,会有 3 个页面受到影响:当前页面、滑出缓存
        // 的页面、滑入缓存的页面。如果 curItem 不存在,那么就没有工作可做,
        // 就可以直接进入下一步 PageAdapter.finishUpdate() 了
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            // itemIndex 是当前显示页面左侧的那个页面(如果有的话)
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            // clientWidth 是 ViewPager 测量的宽度减去左右 padding
            final int clientWidth = getClientWidth();
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            // 从当前位置减 1 开始到 0,也就是当前显示页面左侧的页面
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                // 左侧缓存区以外的 Item 要从 mItems 中 remove 掉,并且要调用
                // PagerAdapter 的 destroyItem() 销毁
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                    	// 从 mItems 移除并通过 mAdapter 销毁 Item
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                	// 在缓存中
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    // itemIndex 如果 Item 没出现过则 addNewItem()
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }

            // 当前页面右侧,与左侧类似,省略...
            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ...
            }

			// 计算 Item 的偏移量,相关信息会保存到 ItemInfo 中
            calculatePageOffsets(curItem, curIndex, oldCurInfo);

            // 设置当前 PrimaryItem,使当前显示的 Item 可见,让之前显示的 Item 不可见
            mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
        }
        ...
	}

这部分先关注 PagerAdapter.destroyItem(),基类仍是会抛出异常让子类实现,子类 FragmentPagerAdapter 内会对 fragment 做 detach 操作,并且如果 fragment 是 PrimaryItem(正在显示的 Item)的话还会将其置空:

	@Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.detach(fragment);
        if (fragment.equals(mCurrentPrimaryItem)) {
            mCurrentPrimaryItem = null;
        }
    }

注意在这个“销毁”操作中,并没有真正地销毁 Fragment 对象,只是将其 detach 了。

然后关注 for 循环中的 else,实际就是检查在缓存区内有哪些位置的 ItemInfo 还没被缓存,如果有的话就通过 addItem() 将它们创建并缓存。注意该操作在要显示的页面的左右两侧都做了,addItem() -> instantiateItem() -> fragment.setUserVisibleHint(false),这也和 log 截图中,显示的左右两侧各 1 个 Fragment 的可见性被设置为 false 吻合。

最后关注 PagerAdapter.setPrimaryItem(),还是看 FragmentPagerAdapter:

	@Override
    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    	// 要显示的 fragment
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
        	// 修改原来处于显示状态的 Item 为不可见状态,这套操作在 2.2 节中见过
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                    if (mCurTransaction == null) {
                        mCurTransaction = mFragmentManager.beginTransaction();
                    }
                    mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
                } else {
                    mCurrentPrimaryItem.setUserVisibleHint(false);
                }
            }
            // 修改 fragment 为可见状态
            fragment.setMenuVisibility(true);
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                if (mCurTransaction == null) {
                    mCurTransaction = mFragmentManager.beginTransaction();
                }
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
            } else {
                fragment.setUserVisibleHint(true);
            }

			// 将 mCurrentPrimaryItem 设置为当前显示的 fragment
            mCurrentPrimaryItem = fragment;
        }
    }

先设置原本的 mCurrentPrimaryItem 不可见,再设置新的 mCurrentPrimaryItem 可见,这与 log 截图中最后两句 setUserVisibleHint 的 log 吻合。

2.4 完成适配

    void populate(int newCurrentItem) {
        // 1.记录被替换掉的 ItemInfo,调用 PagerAdapter.startUpdate()...
        // 2.如需显示的 fragment 的 ItemInfo 没有被缓存,则需调用
        // PagerAdapter.instantiateItem() 实例化一个 fragment 保存在
        // ItemInfo 的 object 字段中,同时管理其可见性...
        // 3.“销毁” Item 数据,设置要显示的 Item

        // 完成更新,调用 PagerAdapter 的 finishUpdate() 后会触发 Fragment 生命周期
        mAdapter.finishUpdate(this);

        // 一些其他操作,省略...
    }

在 FragmentPagerAdapter.finishUpdate() 中,会提交之前一系列的 Fragment 事务:

	@Override
    public void finishUpdate(@NonNull ViewGroup container) {
        if (mCurTransaction != null) {
            if (!mExecutingFinishUpdate) {
                try {
                    mExecutingFinishUpdate = true;
                    // 提交 Fragment 事务并开始 Fragment 生命周期
                    mCurTransaction.commitNowAllowingStateLoss();
                } finally {
                    mExecutingFinishUpdate = false;
                }
            }
            mCurTransaction = null;
        }
    }

也就是说,Fragment 的生命周期是在执行了 FragmentPagerAdapter.finishUpdate() 之后才开始执行的,这也解释了为什么 log 会先输出 setUserVisibleHint 的 log,然后再输出生命周期的 log。

此外,mCurTransaction 包含的是要显示的 Fragment 以及被缓存的 Fragment 的事务,向其添加事务的操作在 2.2 节创建 Item 时调用 instantiateItem() 内。可以回看一下。

3、实现懒加载

对 setUserVisibleHint() 与 populate() 的运行机制有了一定了解后,就可以开始着手实现懒加载了,我们一步一步来。

3.1 重写 setUserVisibleHint()

新建一个抽象的 BaseFragment,重写 setUserVisibleHint(),如果当前 Fragment 可见就加载数据,否则就停止加载:

	override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)

        if (isViewCreated) {
            dispatchUserVisibleHint(isVisibleToUser)
        }
    }

	private fun dispatchUserVisibleHint(visibleToUser: Boolean) {
        if (visibleToUser) {
            onFragmentLoadStart()
        } else {
            onFragmentLoadStop()
        }
    }

	// 由于子类使用懒加载并不是强制的,因此这两个方法没有设置为 abstract
	open fun onFragmentLoadStart() {
    }

    open fun onFragmentLoadStop() {
    }

注意调用 dispatchUserVisibleHint() 之前要先判断 Fragment 的 View 是否已经创建,因为子类在重写 onFragmentLoadStart()、onFragmentLoadStop() 处理数据时可能伴随着对 Fragment UI 的更新,而 setUserVisibleHint() 是在 Fragment 的所有生命周期之前调用,使用 isViewCreated 可以避免 NPE:

	override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        Log.d(TAG, "onCreateView: ")
        _binding = FragmentNormalBinding.inflate(inflater, container, false)
        isViewCreated = true

        // 主动进行一次判断,如果此时页面已经可见,就手动分发一次,
        // 以解决 Fragment 首次显示不会加载数据、更新 UI 的问题
        if (userVisibleHint) {
            dispatchUserVisibleHint(userVisibleHint)
        }

        return binding.root
    }

在 onCreateView() 中当 userVisibleHint 为 true 时要主动分发一次可见性,这是因为 Fragment 首次显示时,setUserVisibleHint() 在 Fragment 生命周期之前执行,因此在 onCreateView() 内才被赋值为 true 的 isViewCreated 还是 false,不会调用 dispatchUserVisibleHint()。因此需要主动调用一次 dispatchUserVisibleHint() 避免 Fragment 首次显示时不加载数据。

3.2 解决数据重复加载问题

运行程序,初始在 T1 页面,我先点击 T3,然后再点击 T1,观察回到 T1 的 log:

请添加图片描述

很明显的能看到 fragment 1 加载了两次数据,并且从临近的 log 中能分析出来,第一次是在 setUserVisibleHint() 中,第二次是在 onCreateView() 中。正常情况下,T1 先跳到 T3,由于缓存数量我们设置的是 1,那么 T1 就不在缓存中,它就要走到 onDestroyView() 这个生命周期,因此在重新调回到 T1 后,先执行 setUserVisibleHint() 时,判断视图是否被创建的标记位 isViewCreated 应该为 false 才对,但是从结果上看,onFragmentLoadStart() 通过 setUserVisibleHint() 执行了,说明出问题时,isViewCreated = true。也就是说,fragment 1 在走 onDestroyView() 时没有重置 isViewCreated:

	override fun onDestroyView() {
        super.onDestroyView()
        isViewCreated = false
    }

经过修改再看 log,重复加载数据/重复停止加载数据的现象就没有了。

3.3 跳转到其他页面的处理

我们在 T5 页面加一个按钮,点击按钮后跳转到另一个 Activity,在跳出 T5 时并没有停止数据的加载,因此我们需要在 onPause() 时分发页面不可见状态,在回到 T5 走 onResume() 时分发页面可见状态:

	override fun onResume() {
        super.onResume()
        // userVisibleHint 是为了判断 Fragment 是正在显示的,
        // !lastVisibleState 表示之前页面生命周期不可见,但是现在
        // 回到 onResume() 处于可见状态了,就要修改该状态
        if (userVisibleHint && !lastVisibleState) {
            dispatchUserVisibleHint(true)
        }
    }

    override fun onPause() {
        super.onPause()
        if (userVisibleHint && lastVisibleState) {
            dispatchUserVisibleHint(false)
        }
    }

	/**
     * 页面是否可见,onResume 之前为 true,onPause 开始之后为 false
     */
    private var lastVisibleState = false

	private fun dispatchUserVisibleHint(visibleToUser: Boolean) {
        lastVisibleState = visibleToUser

        if (visibleToUser) {
            onFragmentLoadStart()
        } else {
            onFragmentLoadStop()
        }
    }

添加以上代码后,显示的 Fragment 在由可见状态进入到 onPause() 时会停止加载,在由不可见状态走到 onResume() 时会重新加载。

这么做实际上是因为 setUserVisibleHint() 不会跟随生命周期的变化而被调用以保持可见/不可见状态,换句话说,它只负责页面初始是否可见,当 Fragment 生命周期变化,比如走到 onPause() 和后续的生命周期变得不可见时,setUserVisibleHint() 不会实时的更新该状态,所以需要我们自己创建一个标记位来维护该状态。

3.4 ViewPager 嵌套

现在很多应用中都有 ViewPager 双层嵌套的结构,比如音乐应用中:

请添加图片描述

像这种结构的 ViewPager 嵌套非常常见,那么我们也在 Demo 中加入这种结构,将外层 ViewPager 的 T2 页面的 Fragment 内放一个包含 4 个 Fragment 的内层 ViewPager:

请添加图片描述

加好之后我们启动应用,来到 T1 页面,观察 log:

请添加图片描述

发现 T2 内层 ViewPager 包含的 Fragment 会执行数据加载的方法,原因是 BaseFragment 在做可见性分发时没有考虑嵌套的逻辑,应该考虑内层 Fragment 的可见但外层 Fragment 不可见的情况:

	private fun dispatchUserVisibleHint(visibleToUser: Boolean) {
        lastVisibleState = visibleToUser

        // 如果外层 Fragment 不可见,但是 visibleToUser 为 true 则不操作
        if (visibleToUser && isParentFragmentInvisible()) {
            return
        }

        if (visibleToUser) {
            onFragmentLoadStart()
        } else {
            onFragmentLoadStop()
        }
    }

    // 返回嵌套的外层 Fragment 是否不可见
    private fun isParentFragmentInvisible(): Boolean {
        if (parentFragment is BaseFragment5) {
            return !(parentFragment as BaseFragment5).lastVisibleState
        }
        return false
    }

运行程序,这次进入 T1 时,T2 的内层 Fragment 不会再加载数据了,但是点击进入 T2,你会发现 T2 的内层 Fragment 不会加载数据了,这是因为外层 Fragment 的可见状态没有同步给内层 Fragment,需要添加相关逻辑:

	private fun dispatchUserVisibleHint(visibleToUser: Boolean) {
        lastVisibleState = visibleToUser

        // 如果外层 Fragment 不可见,但是 visibleToUser 为 true 则不操作
        if (visibleToUser && isParentFragmentInvisible()) {
            return
        }

        if (visibleToUser) {
            onFragmentLoadStart()
        } else {
            onFragmentLoadStop()
        }
        // 将外层 Fragment 可见状态分发给内层 Fragment
        dispatchChildVisibleHint(visibleToUser)
    }
    
    private fun dispatchChildVisibleHint(visibleToUser: Boolean) {
        val childFragments = childFragmentManager.fragments
        childFragments.forEach {
        	// 嵌套内层的 Fragment 被显示才向其分发可见性
            if (it is BaseFragment5 && it.userVisibleHint && !it.isHidden) {
                it.dispatchUserVisibleHint(visibleToUser)
            }
        }
    }

这样在显示嵌套内层的 Fragment 时才会在显示时进行数据加载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值