百度一面之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删除...
- 一周热门
- 最近发表
-
- Docker篇(二):Docker实战,命令解析
- docker学习笔记——安装和基本操作
- 不可错过的Docker完整笔记(dockerhib)
- 扔掉运营商的 IPTV 机顶盒,全屋全设备畅看 IPTV!
- 第五节 Docker 入门实践:从 Hello World 到容器操作
- 替代Docker build的Buildah简单介绍
- Docker 命令大全(docker命令大全记录表)
- docker常用指令及安装rabbitMQ(docker安装rabbitmq配置环境)
- 使用Docker快速部署Storm环境(docker部署confluence)
- Docker Desktop安装使用指南:零基础教程
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- logstashinput (65)
- hadoop端口 (65)
- vue阻止冒泡 (67)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)