欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

详解Android ViewPager2中的缓存和复用机制,

来源: 开发者 投稿于  被查看 3660 次 评论:247

详解Android ViewPager2中的缓存和复用机制,


目录
  • 1. 前言
  • 2. 回顾RecyclerView缓存机制
  • 3. offscreenPageLimit原理
  • 4. FragmentStateAdapter原理以及缓存机制
    • 4.1 简单使用
    • 4.2 原理
  • 5. 案例讲解回收机制
    • 5.1 默认情况
    • 5.2 offscreenPageLimit=1
  • 总结

    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缓存机制

    本章节,简单回顾下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]表示右边的大小。

    假设设置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<>();
    
    

    mFragments:是position与Fragment的映射表。随着position的增长,Fragment是会不断的新建出来的。 Fragment可以被缓存起来,当它被回收后无法重复使用。

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

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

    接下来我们讲解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对应的信息从两张表中删除

    最后讲解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。

    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

    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中删除掉

    总结

    到此这篇关于Android ViewPager2中缓存和复用机制的文章就介绍到这了,更多相关ViewPager2缓存和复用机制内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

    您可能感兴趣的文章:
    • Android实现缓存大图到SD卡
    • 关于Android的 DiskLruCache磁盘缓存机制原理
    • android H5本地缓存加载优化的实战
    • Android清除应用缓存的两种方法
    • 使用SharedPreferences在Android存储对象详细代码
    • Android 文件存储与SharedPreferences存储方式详解用法
    • 在android中使用缓存和脱机存储

    用户评论