IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> RecycleView滑动、缓存、复用源码分析与优化 -> 正文阅读

[移动开发]RecycleView滑动、缓存、复用源码分析与优化

一、RecycleView基础介绍

RecycleView继承自ViewGroup,是一种通过在滑动过程中不断回收复用进而实现流畅滑动的控件,RecycleView回收、缓存、复用的对象都是ViewHolder.itemView,也可以说是ViewHolder(后面回详细说明)

// 直接继承关系
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
	// some other code
}

二、滑动过程分析

用户滑动RecycleView,首先时间会被Recycle.onTouchEvent()获取,我们直接看MotionEvent.ACTION_MOVE:

1. MotionEvent.ACTION_MOVE

case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];

                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

代码前面主要是对滑动参数(水平滑动距离、垂直滑动距离)的合法性进行一些判定,并设定一些状态值(START_SCROLL等),我们直接看最后面的关键函数scrollByInternal(),最终的滑动操作是由此函数完成;

2. scrollByInternal()

boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        /**
        * some other code
        */
    }

这里只展示了scrollByInternal()部分源码, 可以看到,首先判定了一下mAdapter不为空,这里的mAdapter就是RecycleView.Adapter的一个实例,之后执行scrollStep(),OK,我们继续来看scrollStep()做了什么:

3. scrollStep()

/**
     * Scrolls the RV by 'dx' and 'dy' via calls to
     * {@link LayoutManager#scrollHorizontallyBy(int, Recycler, State)} and
     * {@link LayoutManager#scrollVerticallyBy(int, Recycler, State)}.
     *
     * Also sets how much of the scroll was actually consumed in 'consumed' parameter (indexes 0 and
     * 1 for the x axis and y axis, respectively).
     *
     * This method should only be called in the context of an existing scroll operation such that
     * any other necessary operations (such as a call to {@link #consumePendingUpdateOperations()})
     * is already handled.
     */
    void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();

        TraceCompat.beginSection(TRACE_SCROLL_TAG);
        fillRemainingScrollValues(mState);

        int consumedX = 0;
        int consumedY = 0;
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

        TraceCompat.endSection();
        repositionShadowingViews();

        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);

        if (consumed != null) {
            consumed[0] = consumedX;
            consumed[1] = consumedY;
        }
    }

这里其实注释已经说的很明白了,scrollStep()会将RecycleView水平滑动dx,垂直滑动dy,并同时会统计实际消耗的滑动事件数。当dx或者dy不为0(即有效滑动)时,会分别通过mLayout.scrollHorizontallyBy()mLayout.scrollVerticallyBy()实现水平和垂直滑动。这里的mLayout,其类型是RecycleView.LayoutManager,我们继续看mLayout.scrollHorzontallyBy():

4. mLayout.scrollHorzontallyBy()

/**
     * {@inheritDoc}
     */
    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == VERTICAL) {
            return 0;
        }
        return scrollBy(dx, recycler, state);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }

可以看到,scrollHorizontallyBy()scrollVerticallyBy()逻辑是一样的,都是通过scrollBy()实现最终的滑动操作

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || delta == 0) {
            return 0;
        }
        ensureLayoutState();
        mLayoutState.mRecycle = true;
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }

三、缓存回收过程分析

我们知道,RecycleView复用的是View,用户滑动过程中,旧的(滑到屏幕外的)Item(里面通常包含若干个View,回收的单位是ViewHolder)会被回收,新进入屏幕的会复用之前的View并重新绑定数据,那么我们首先需要关注,滑动过程中,新进入的View到底是如何创建的:

1. RecycleView的四级缓存

1.1. mAttachedScrap,mChangeScrap

scrap是用来保存被rv移除掉但最近又马上要使用的缓存,比如rv自带item的动画效果,本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画,如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中

1.2. mCacheViews

也是一个非常重要的缓存,就LinearLayoutManager来说,cached缓存默认大小为2,他的容量非常小,所起到的作用就是rv滑动时候刚被移出屏幕的viewholder的收容所,因为rv会认为刚被移出屏幕的viewholder可能马上就会使用到,所以不会立即设置为无效viewholder,会将他们保存到cached中,但又不能将所有移除屏幕的viewholder视为有效viewholder,所以他的默认容量只有两个,可以通过setViewCacheSize(int viewCount)方法修改

1.3. mViewCacheExtension

第三级缓存,这是一个自定义缓存,rv可以自定义缓存行为,在这里你可以决定缓存的保存逻辑,但是这么个自定义缓存一般都没有见过具体的使用场景,而且自定义缓存需要你对rv中的源码非常熟悉才行,否则在rv执行item动画,或者执行notify的一系列方法后你的自定义缓存是否还能有效就是一个值得考虑的问题,所以一般不太推荐使用该缓存,更多的我觉得这可能是google自已留着方便扩展来使用的,目前来说这还只是个空实现而已,从这点来看其实rv所说的四级缓存本质上还只是三级缓存。

1.4. mRecycleViewPool

又一个重要的缓存,这也是唯一一个我们开发者可以方便设置的一个(虽然extension也能设置,但是难度大),而且设置方式非常简单,new一个pool传进去就可以了,其他的都不用我们来处理,google已经给我们料理好后事了,这个缓存保存的对象就是那些无效的viewholder,虽然说无效的viewholder上的数据是无效的,但是他的rootview还是可以拿来使用的,这也是为什么最早listview有一个convertView的原因,当然这种机制也被rv很好的继承下来了,pool一般会和cached配合使用,这么来说cached存不下的就会被保存到pool中,毕竟cached的默认容量大小只有2,但是pool容量也是有限的当保存满之后再有viewholder到来的话就只能会无情抛弃掉,它也有一个默认的容量大小
private static final int DEFAULT_MAX_SCRAP = 5;
int mMaxScrap = DEFAULT_MAX_SCRAP;

2 缓存复用过程

2.1. layoutChunk()

在layoutChunk()方法中,我们可以看到如下代码:

		View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        }

滑动过程中的新加入的View是通过addView()直接加到当前页面View树的,而这个View是通过layoutState.next()方法获取,这里传入的参数类型时RecycleView.Recycle,也就是RecycleView的回收池

2.2. layoutState.next()
		View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
2.3. recycle.getViewForPosition()
2.4. tryGetViewHolderForPositionByDeadline()

四、RecycleView的优化

五、一些问题

  1. RecycleView适配器及理解:
    实现业务逻辑和UI解耦
  2. RecycleView如何判读一屏有多少个View?
    每加载一个Item进入RecycleView中,就将当前Item高度加到RecycleView中,直到当前bottom大于等于屏幕高度;
  3. RecycleView数据更新
    在这里插入图片描述
    当用户开始向上滑动RecycleView时,底部会出现空缺,此时RecycleView会去回收池中检查是否有被回收的View,如果有符合条件的,会通过适配器的onBindViewHolder(View view)为新的View绑定数据并添加到页面View树中进行显示。
    在这里插入图片描述
    回收池本质上是一个集合,准确的说是若干个栈,每个栈存储一种类型的View(如ImageView,Textview等),在不断滑动刷新过程中,实际上回收池会通过getItemType()返回的item类型去对应的栈中查找是否存在可复用的view;
    回收池的初始化是放在setAdapter()中进行的,并非放在RecycleView的构造或是初始化中,因为只有适配器被设置成功后,才可以确定有多少种View,进而确定回收池需要初始化多少个栈。

RecycleView默认会使用key, value方式对每一个view打标签

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-09-15 02:07:57  更:2022-09-15 02:09:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 4:58:52-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码