百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

百度一面之RecyclerView的四级缓存详解

bigegpt 2024-10-22 10:04 8 浏览

本文通过在字节面试遇到的问题总结而出,如有不对地方,请及时批评指正。篇幅较长,请耐心阅读。

简介

RecyclerView作为Android开发广泛使用的框架之一,它的出现取代了ListView,支持多种布局样式,功能强大。如下图所示:

使用步骤

添加RecyclerView布局控件

定义适配器GridAdapter继承RecyclerView.Adapter抽象类

RecyclerView设置适配器


缓存详解

RecyclerView拥有高效的四级缓存:分别是屏幕内缓存(mChangeScrap),屏幕外缓存(mCacheViews),自定义缓存(mViewCacheExtension),缓存池(RecycledViewPool )。

RecyclerView的缓存都是从滑动事件开始。

缓存过程

1 .RecyclerView->onTouchEvent()

 public boolean onTouchEvent(MotionEvent e) {
       ......省略部分代码.......

       //获取当前RecyclerView的滑动方向
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();
       ......省略部分代码.......
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                //记录手指按下点的值
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
              ......省略部分代码.......
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                mScrollPointerId = e.getPointerId(actionIndex);
                //记录手指按下点的值
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
            } break;

            case MotionEvent.ACTION_MOVE: {
                //获取当前屏幕的手指触摸点个数
                final int index = e.findPointerIndex(mScrollPointerId);
               //记录滑动值
                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) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // 更新滑动偏移量
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // 设置禁止父view拦截触摸事件
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //滑动判断->缓存入口!!!
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                           // 设置禁止父view拦截触摸事件
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            case MotionEvent.ACTION_POINTER_UP: {
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {
                ........省略部分代码.........
                //重置滑动操作
                resetScroll();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                //取消滑动
                cancelScroll();
            } break;
        }

        if (!eventAddedToVelocityTracker) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();

        return true;
    }

在onTouchEvent事件中处理RecyclerView的滑动事件进入到scrollByInternal( )方法。

2 .RecyclerView->onTouchEvent()->scrollByInternal( )

 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;
   ........省略部分代码.........
         //判断适配器已经添加
        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;
        }
     ........省略部分代码.........
         //滑动事件分发
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;
      
        // 更新最后一次触摸偏移量
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
          ........省略部分代码.........
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

scrollByInternal方法中记录手指在屏幕上的滑动偏移量,然后调用scrollStep()进行增量滑动RecyclerView。

3 .RecyclerView->onTouchEvent()->scrollByInternal( )->scrollStep()

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    //滑动期间禁止响应对requestLayout()的额外调用
    startInterceptRequestLayout();
    //记录滑动事件+1
    onEnterLayoutOrScroll();
    ........省略部分代码.........
    int consumedX = 0;
    int consumedY = 0;
    if (dx != 0) {
        //调用LinearLayoutManager的横向滑动->缓存入口!!!
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
          //调用LinearLayout的纵向滑动
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

     ........省略部分代码.........
    //退出滑动事件
    onExitLayoutOrScroll();
    //允许调用requestLayout()
    stopInterceptRequestLayout(false);

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

scrollStep()方法中通过调用mLayout.scrollHorizontallyBy()方法处理滑动事件,在RecyclerView使用中设置所有的LayoutManager都继承于LayoutManager这个抽象类,这里只分析LinearLayoutManager的方法即可。

4 .RecyclerView->onTouchEvent()->scrollByInternal( )->scrollStep()->LinearLayoutManager->scrollHorizontallyBy()

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
    RecyclerView.State state) {
    //校验滑动方向
    if (mOrientation == VERTICAL) {
        return 0;
    }
    return scrollBy(dx, recycler, state);
}

scrollHorizontallyBy()先校验滑动方向是否是水平方向,然后调用scrollBy()方法。

5 .RecyclerView->onTouchEvent()->scrollByInternal( )->scrollStep()->LinearLayoutManager->scrollHorizontallyBy()->scrollBy( )。

 int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
     //判断RecyclerView的View个数   
     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);

         ........省略部分代码.........
        return scrolled;
    }

scrollBy()中调用fill()方法,计算滑动偏移量。

6 .RecyclerView->onTouchEvent()->scrollByInternal( )->scrollStep()->LinearLayoutManager->scrollHorizontallyBy()->scrollBy( )->fill( )

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // 记录开始偏移量
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // 异常处理
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        //滑动偏移量
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            //检查布局->缓存入口!!!
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ........省略部分代码.........
        return start - layoutState.mAvailable;
    }

从fill()方法中的layoutChunk( )开始,才开始进入布局缓存的使用。

7 .RecyclerView->onTouchEvent()->scrollByInternal( )->scrollStep()->LinearLayoutManager->scrollHorizontallyBy()->scrollBy( )->fill( )->layoutChunk()

  void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        //通过Recycler获取下一个即将显示的view ->缓存入口!!!
        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;
        }
          ........省略部分代码.........
        result.mFocusable = view.hasFocusable();
    }
  View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
   public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }

     View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
 }

layoutChunk()中主要通过recycler获取下一个需要显示的View。

8 .RecyclerView->onTouchEvent()->scrollByInternal( )->scrollStep()->LinearLayoutManager->scrollHorizontallyBy()->scrollBy( )->fill( )->layoutChunk()->layoutState.next()->getViewForPosition()->tryGetViewHolderForPositionByDeadline()

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                                                 boolean dryRun, long deadlineNs) {
    //判断下一个需要显示的view的下标是否越界
    if (position < 0 || position >= mState.getItemCount()) {
        throw new IndexOutOfBoundsException("Invalid item position " + position
                                            + "(" + position + "). Item count:" + mState.getItemCount()
                                            + exceptionLabel());
    }
    //默认不从缓存中获取
    boolean fromScrapOrHiddenOrCache = false;
    //定义holder
    ViewHolder holder = null;
    // 1) 如果屏幕内有改变的scrap, 直接从scrap中获取
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 2)从/隐藏列表/缓存中按位置查找
    if (holder == null) {
        //先从屏幕内缓存查找(后面分析)
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                // 如果不再使用了先回收
                if (!dryRun) {
                    //确保不再使用
                    //回收处理
                     ........省略部分代码.........
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        //通过下一个view的位置获取偏移position
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
         ........省略部分代码.........
        final int type = mAdapter.getItemViewType(offsetPosition);
    
        if (mAdapter.hasStableIds()) {
            //从屏幕内或屏幕外缓存中查找缓存
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                               type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        if (holder == null && mViewCacheExtension != null) {
            //从自定义缓存中查找,getViewForPositionAndType是个抽象方法,需要我们自己实现。
            final View view = mViewCacheExtension
        
                .getViewForPositionAndType(this, position, type);
            endingInvalidate = fromScrapOrHiddenOrCache && bound;
                    return holder;
                    }
         if (holder == null) { 
                    //从缓存池中查找。
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

四级缓存

1 . getChangedScrapViewForPosition->从屏幕内缓存中查找

ViewHolder getChangedScrapViewForPosition(int position) {
 .............
// 先从屏幕内缓存查看可改变的holder
for (int i = 0; i < changedScrapSize; i++) {
    final ViewHolder holder = mChangedScrap.get(i);
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        return holder;
    }
}
// 通过id从屏幕中查看holder
if (mAdapter.hasStableIds()) {
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
        final long id = mAdapter.getItemId(offsetPosition);
        for (int i = 0; i < changedScrapSize; i++) {
            final ViewHolder holder = mChangedScrap.get(i);
            if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                return holder;
            }
        }
    }
}
return null;
}

2 .getScrapOrHiddenOrCachedHolderForPosition->先从屏幕内查找缓存,如果没有再从屏幕外查找缓存

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();

    // 从屏幕内查找可复用的holder.mAttachedScrap最大长度为 5
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
            && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    .....省略部分代码.....

    // 从屏幕外缓存中查找mCachedViews 最大长度为2
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
         
        if (!holder.isInvalid() && holder.getLayoutPosition() == position
            && !holder.isAttachedToTransitionOverlay()) {
            if (!dryRun) {
                //使用完从缓存中移除
                mCachedViews.remove(i);
            }
            .................
            return holder;
        }
    }
    return null;
}

3 .getViewForPositionAndType ->自定义缓存,需要自己实现

public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                                               int type);

4 .getRecycledViewPool().getRecycledView(type)->从缓存池中获取缓存

public ViewHolder getRecycledView(int viewType) {
    //先根据布局类型获取ScrapData
    final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
    //再从ScrapData的mScrapHeap中查缓存holder,同时从缓存池中移除
    final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; //最大长度为20
    for (int i = scrapHeap.size() - 1; i >= 0; i--) {
        if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
            return scrapHeap.remove(i);
        }
    }
}
return null;
}

先从缓存池RecycledViewPool中根据布局类型获取对应的ViewHolder集合(最大长度为5),然后再从集合中查找缓存是否存在。

RecyclerViewPool类似于HashMap的数据结构,根据不同的布局类型viewType获取缓存Holder。


以上就是百度面试后总结的几个要点,还不会的同学赶紧学起来吧,感谢您的阅读,创造不易,如果您觉得本篇文章对您有帮助,请点击关注小编,您的支持就是小编创作的最大动力!

相关推荐

Docker篇(二):Docker实战,命令解析

大家好,我是杰哥上周我们通过几个问题,让大家对于Docker有了一个全局的认识。然而,说跟练往往是两个概念。从学习的角度来说,理论知识的学习,往往只是第一步,只有经过实战,才能真正掌握一门技术所以,本...

docker学习笔记——安装和基本操作

今天学习了docker的基本知识,记录一下docker的安装步骤和基本命令(以CentOS7.x为例)一、安装docker的步骤:1.yuminstall-yyum-utils2.yum-con...

不可错过的Docker完整笔记(dockerhib)

简介一、Docker简介Docker是一个开源的应用容器引擎,基于Go语言并遵从Apache2.0协议开源。Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,...

扔掉运营商的 IPTV 机顶盒,全屋全设备畅看 IPTV!

其实现在看电视节目的需求确实大大降低了,折腾也只是为了单纯的让它实现,享受这个过程带来的快乐而已,哈哈!预期构想家里所有设备直接接入网络随时接收并播放IPTV直播(电信点播的节目不是太多,但好在非常稳...

第五节 Docker 入门实践:从 Hello World 到容器操作

一、Docker容器基础运行(一)单次命令执行通过dockerrun命令可以直接在容器中执行指定命令,这是体验Docker最快捷的方式:#在ubuntu:15.10容器中执行ech...

替代Docker build的Buildah简单介绍

Buildah是用于通过较低级别的coreutils接口构建OCI兼容镜像的工具。与Podman相似,Buildah不依赖于Docker或CRI-O之类的守护程序,并且不需要root特权。Builda...

Docker 命令大全(docker命令大全记录表)

容器生命周期管理run-创建并启动一个新的容器。start/stop/restart-这些命令主要用于启动、停止和重启容器。kill-立即终止一个或多个正在运行的容器rm-于删除一个或...

docker常用指令及安装rabbitMQ(docker安装rabbitmq配置环境)

一、docker常用指令启动docker:systemctlstartdocker停止docker:systemctlstopdocker重启docker:systemctlrestart...

使用Docker快速部署Storm环境(docker部署confluence)

Storm的部署虽然不是特别麻烦,但是在生产环境中,为了提高部署效率,方便管理维护,使用Docker来统一管理部署是一个不错的选择。下面是我开源的一个新的项目,一个配置好了storm与mono环境的D...

Docker Desktop安装使用指南:零基础教程

在之前的文章中,我多次提到使用Docker来安装各类软件,尤其是开源软件应用。鉴于不少读者对此有需求,我决定专门制作一期关于Docker安装与使用的详细教程。我主要以Macbook(Mac平台)为例进...

Linux如何成功地离线安装docker(linux离线安装httpd)

系统环境:Redhat7.2和Centos7.4实测成功近期因项目需要用docker,所以记录一些相关知识,由于生产环境是不能直接连接互联网,尝试在linux中离线安装docker。步骤1.下载...

Docker 类面试题(常见问题)(docker面试题目)

Docker常见问题汇总镜像相关1、如何批量清理临时镜像文件?可以使用sudodockerrmi$(sudodockerimages-q-fdanging=true)命令2、如何查看...

面试官:你知道Dubbo怎么优雅上下线的吗?你:优雅上下线是啥?

最近无论是校招还是社招,都进行的如火如荼,我也承担了很多的面试工作,在一次面试过程中,和候选人聊了一些关于Dubbo的知识。Dubbo是一个比较著名的RPC框架,很多人对于他的一些网络通信、通信协议、...

【Docker 新手入门指南】第五章:Hello Word

适合人群:完全零基础新手|学习目标:30分钟掌握Docker核心操作一、准备工作:先确认是否安装成功打开终端(Windows用户用PowerShell或GitBash),输入:docker--...

松勤软件测试:详解Docker,如何用portainer管理Docker容器

镜像管理搜索镜像dockersearch镜像名称拉取镜像dockerpullname[:tag]列出镜像dockerimages删除镜像dockerrmiimage名称或id删除...