聊聊ViewPager2中的缓存和复用机制

简介: 聊聊ViewPager2中的缓存和复用机制

1. 前言


众所周知ViewPager2是ViewPager的替代版本。它解决了ViewPager的一些痛点,包括支持right-to-left布局,支持垂直方向滑动,支持可修改的Fragment集合等。ViewPager2内部是使用RecyclerView来实现的。


所以它继承了RecyclerView的优势,包含但不限于以下


  1. 支持横向和垂直方向布局
  2. 支持嵌套滑动
  3. 支持ItemPrefetch(预加载)功能
  4. 支持三级缓存


ViewPager2相对于RecyclerView,它又扩展出了以下功能


  1. 支持屏蔽用户触摸功能setUserInputEnabled
  2. 支持模拟拖拽功能fakeDragBy
  3. 支持离屏显示功能setOffscreenPageLimit
  4. 支持显示Fragment的适配器FragmentStateAdapter


如果熟悉RecyclerView,那么上手ViewPager2将会非常简单。可以简单把ViewPager2想象成每个ItemView都是全屏的RecyclerView。本文将重点讲解ViewPager2的离屏显示功能和基于FragmentStateAdapter的缓存机制。

2. 回顾RecyclerView缓存机制


640.png

本章节,简单回顾下RecyclerView缓存机制。RecyclerView有三级缓存,简单起见,这里只介绍mViewCaches和mRecyclerPool两种缓存池。更多关于RecyclerView的缓存原理,请移步公众号相关文章。


  1. mViewCaches:该缓存离UI更近,效率更高,它的特点是只要position能对应上,就可以直接复用ViewHolder,无需重新绑定,该缓存池是用队列实现的,先进先出,默认大小为2,如果RecyclerView开启了预抓取功能,则缓存池大小为2+预抓取个数,默认预抓取个数为1。所以默认开启预抓取缓存池大小为3。
  2. mRecyclerPool:该缓存池离UI最远,效率比mViewCaches低,回收到该缓存池的ViewHolder会将数据解绑,当复用该ViewHolder时,需要重新绑定数据。它的数据结构是类似HashMap。key为itemType,value是数组,value存储ViewHolder,数组默认大小为5,最多每种itemType的ViewHolder可以存储5个。


3. offscreenPageLimit原理


//androidx.viewpager2:ViewPager2:1.0.0@aar
//ViewPager2.java
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
    if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        throw new IllegalArgumentException(
                "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
    }
    mOffscreenPageLimit = limit;
    mRecyclerView.requestLayout();
  }

调用setOffscreenPageLimit方法就可以为ViewPager2设置离屏显示的个数,默认值为-1。如果设置不当,会抛异常。我们看到该方法,只是给mOffscreenPageLimit赋值。为什么就能实现离屏显示功能呢?如下代码

//androidx.viewpager2:ViewPager2:1.0.0@aar
//ViewPager2$LinearLayoutManagerImpl
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
        @NonNull int[] extraLayoutSpace) {
    int pageLimit = getOffscreenPageLimit();
    if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        super.calculateExtraLayoutSpace(state, extraLayoutSpace);
        return;
    }
    final int offscreenSpace = getPageSize() * pageLimit;
    extraLayoutSpace[0] = offscreenSpace;
    extraLayoutSpace[1] = offscreenSpace;
}

以水平滑动ViewPager2为例:getPageSize()表示ViewPager2的宽度,离屏的空间大小为getPageSize() * pageLimit。extraLayoutSpace[0]表示左边的大小,extraLayoutSpace[1]表示右边的大小。

640.png

640.png

假设设置offscreenPageLimit为1,简单讲,Android系统会默认把画布宽度增加到3倍。左右两边各有一个离屏ViewPager2的宽度。


4. FragmentStateAdapter原理以及缓存机制


4.1 简单使用


FragmentStateAdapter继承自RecyclerView.Adapter。它有一个抽象方法,createFragment()。它能将Fragment与ViewPager2完美结合。

public abstract class FragmentStateAdapter extends
        RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
    public abstract Fragment createFragment(int position);
}

使用FragmentStateAdapter非常简单,Demo如下

class ViewPager2WithFragmentsActivity : AppCompatActivity() {
    private lateinit var mViewPager2: ViewPager2
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view_view_pager2)
        mViewPager2 = findViewById(R.id.viewPager2)
        (mViewPager2.getChildAt(0) as RecyclerView).layoutManager?.apply {
//            isItemPrefetchEnabled = false
        }
        mViewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
        mViewPager2.adapter = MyAdapter(this)
//        mViewPager2.offscreenPageLimit = 1
    }
    inner class MyAdapter(fragmentActivity: FragmentActivity) :
        FragmentStateAdapter(fragmentActivity) {
        override fun getItemCount(): Int {
            return 100
        }
        override fun createFragment(position: Int): Fragment {
            return MyFragment("Item $position")
        }
    }
    class MyFragment(val text: String) : Fragment() {
        init {
            println("MyFragment $text")
        }
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            var view = layoutInflater.inflate(R.layout.view_item_view_pager_snap, container)
            view.findViewById<TextView>(R.id.text_view).text = text
            return view;
        }
    }
}


4.2 原理


首先FragmentStateAdapter对应的ViewHolder定义如下,它只是返回一个简单的带有id的FrameLayout。由此可以看出,FragmentStateAdapter并不复用Fragment,它仅仅是复用FrameLayout而已。

public final class FragmentViewHolder extends ViewHolder {
    private FragmentViewHolder(@NonNull FrameLayout container) {
        super(container);
    }
    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
        FrameLayout container = new FrameLayout(parent.getContext());
        container.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT));
        container.setId(ViewCompat.generateViewId());
        container.setSaveEnabled(false);
        return new FragmentViewHolder(container);
    }
    @NonNull FrameLayout getContainer() {
        return (FrameLayout) itemView;
    }
}

然后介绍FragmentStateAdapter中两个非常重要的数据结构:

final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
  1. mFragments:是position与Fragment的映射表。随着position的增长,Fragment是会不断的新建出来的。Fragment可以被缓存起来,当它被回收后无法重复使用。

Fragment什么时候会被回收掉呢?


640.pngmItemIdToViewHolder:是position与ViewHolder的Id的映射表。由于ViewHolder是RecyclerView缓存机制的载体。所以随着position的增长,ViewHolder并不会像Fragment那样不断的新建出来,而是会充分利用RecyclerView的复用机制。所以如下图,position 4处打上了一个大大的问号,具体的值是不确定的,它由缓存的大小以及离屏个数共同决定的。

640.png

接下来我们讲解onViewRecycled()。当ViewHolder从mViewCaches缓存中移出到mRecyclerPool缓存中时会调用该方法

@Override
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
}


该方法的作用是,当ViewHolder回收到RecyclerPool中时,将ViewHolder相关的信息从上面两张表中移除。


举例 当ViewHolder1发生回收时,position 0对应的信息从两张表中删除

640.png

640.png

最后讲解onBindViewHolder方法


@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    final long itemId = holder.getItemId();
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null && boundItemId != itemId) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
    mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
    ensureFragment(position);
    /** Special case when {@link RecyclerView} decides to keep the {@link container}
     * attached to the window, but not to the view hierarchy (i.e. parent is null) */
    final FrameLayout container = holder.getContainer();
    if (ViewCompat.isAttachedToWindow(container)) {
        if (container.getParent() != null) {
            throw new IllegalStateException("Design assumption violated.");
        }
        container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if (container.getParent() != null) {
                    container.removeOnLayoutChangeListener(this);
                    placeFragmentInViewHolder(holder);
                }
            }
        });
    }
    gcFragments();
}

该方法可以分成3个部分:


  1. 检查该复用的ViewHolder在两张表中是否还有残留的数据,如果有,将它从两张表中移除掉。
  2. 新建Fragment,并将ViewHolder与Fragment和position的信息注册到两张表中
  3. 在合适的时机把Fragment展示在ViewPager2上。


大概的脉络就是这样,为了避免文章冗余,其它的细支且也蛮重要的方法就没有列出来


5. 案例讲解回收机制


5.1 默认情况


默认情况:offscreenPageLimit = -1,开启预抓取功能

因为开启了预抓取,所以mViewCaches大小为3。

640.jpg

  1. 刚开始进入ViewPager2,没有触发Touch事件,不会触发预抓取,所以只有Fragment1
  2. 滑动到Fragment2,会触发Fragment3预抓取,由于offscreenPageLimit = -1,所以只有Fragment2会展示在ViewPager2上,1和3进入mViewCaches缓存中
  3. 滑动到Fragment3。1、2、4进入mViewCaches缓存中
  4. 滑动到Fragment4。2、3、5进入mViewCaches缓存中,由于缓存数量为3,所以1被挤出到mRecyclerPool缓存中,同时把Fragment1从mFragments中移除掉
  5. 滑动到Fragment5。Fragment6会复用Fragment1对应的ViewHolder。3、4、6进入mViewCaches缓存中,2被挤出到mRecyclerPool缓存中


5.2 offscreenPageLimit=1

640.jpg



offscreenPageLimit=1,所以ViewPager2一下子能展示3屏Fragment,左右各显示一屏


  1. Fragment1左边没有数据,所以屏幕只有1和2
  2. 滑动到fragment2,1、2、3显示在屏幕上(1和3肉眼不可见,下同),同时预抓取4放入mViewCaches
  3. 滑动到fragment3,2、3、4显示在屏幕上,1和5放入mViewCaches
  4. 滑动到fragment4,3、4、5显示在屏幕上,1、2、6放入mViewCaches
  5. 滑动到fragment5,4、5、6显示在屏幕上,2、3、7放入mViewCaches,1被回收到mRecyclerPool缓存中。Fragment1同时从mFragments中删除掉


相关文章
|
6月前
|
存储 缓存 安全
第二章 HTTP请求方法、状态码详解与缓存机制解析
第二章 HTTP请求方法、状态码详解与缓存机制解析
107 0
|
2月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
3月前
|
缓存 应用服务中间件 nginx
Web服务器的缓存机制与内容分发网络(CDN)
【8月更文第28天】随着互联网应用的发展,用户对网站响应速度的要求越来越高。为了提升用户体验,Web服务器通常会采用多种技术手段来优化页面加载速度,其中最重要的两种技术就是缓存机制和内容分发网络(CDN)。本文将深入探讨这两种技术的工作原理及其实现方法,并通过具体的代码示例加以说明。
290 1
|
1月前
|
存储 缓存 负载均衡
Nginx代理缓存机制
【10月更文挑战第2天】
62 4
|
1月前
|
存储 缓存 NoSQL
深入理解后端缓存机制的重要性与实践
本文将探讨在后端开发中缓存机制的应用及其重要性。缓存,作为提高系统性能和用户体验的关键技术,对于后端开发来说至关重要。通过减少数据库访问次数和缩短响应时间,缓存可以显著提升应用程序的性能。本文将从缓存的基本概念入手,介绍常见的缓存策略和实现方式,并通过实例展示如何在后端开发中有效应用缓存技术。最后,我们将讨论缓存带来的一些挑战及其解决方案,帮助您在实际项目中更好地利用缓存机制。
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
64 8
|
6月前
|
缓存 安全 Java
7张图带你轻松理解Java 线程安全,java缓存机制面试
7张图带你轻松理解Java 线程安全,java缓存机制面试
|
2月前
|
缓存 Java Python
python垃圾回收&缓存机制
python垃圾回收&缓存机制
|
3月前
|
存储 缓存 JavaScript
深入理解后端开发中的缓存机制
【8月更文挑战第31天】本文将通过一个实际的后端开发案例,介绍如何有效地使用缓存来提高应用性能。我们将从基础概念开始,逐步深入到缓存策略的实施,最后通过代码示例展示如何在Node.js环境中实现一个简单的缓存系统。无论你是缓存新手还是希望优化现有系统的开发者,这篇文章都将为你提供实用的指导和启示。