RecyclerView源码浅析
1. RecyclerView简单使用流程
- 创建RecyclerView
new RecyclerView(context)
; - 设置LayoutManager
recyclerView.setLayoutManager(new LinearLayoutManager(context));
- 添加分割线ItemDecoration
recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
- 设置Adapter
recyclerView.setAdapter(new MyAdapter());
- 刷新
adapter.notifyDataSetChanged();
adapter.notifyItemChanged(position);
2. 初始化RecyclerView阶段
初始化源码如下:
java
// RecyclerView 构造函数,接受 Context、AttributeSet 和 defStyle 参数
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
// 调用父类 ViewGroup 的构造函数
super(context, attrs, defStyle);
// 处理 XML 属性中定义的 clipToPadding 属性
if (attrs != null) {
// 获取 TypedArray 用于读取 CLIP_TO_PADDING_ATTR 属性
TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
// 读取属性值,默认 true(裁剪到 padding)
mClipToPadding = a.getBoolean(0, true);
a.recycle(); // 回收 TypedArray
} else {
mClipToPadding = true; // 无属性时默认 true
}
// 启用滚动容器特性(影响窗口焦点行为)
setScrollContainer(true);
// 允许在触摸模式下获取焦点(用于处理键盘导航)
setFocusableInTouchMode(true);
// (1)获取 ViewConfiguration 对象(包含系统级 UI 参数)
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop(); // 触摸事件识别为滚动的最小距离
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); // 最小抛掷速度
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); // 最大抛掷速度
// 如果 OVER_SCROLL_NEVER,则优化绘制性能(跳过 onDraw 调用)
setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
// (2)设置 ItemAnimator 的监听器(用于动画生命周期回调)
mItemAnimator.setListener(mItemAnimatorListener);
// (3)初始化 Adapter 管理相关组件
initAdapterManager();
// (4)初始化子视图管理辅助类
initChildrenHelper();
// 处理可访问性相关配置
if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
// 如果未明确指定,默认启用可访问性
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
// 获取 AccessibilityManager 服务
mAccessibilityManager = (AccessibilityManager) getContext()
.getSystemService(Context.ACCESSIBILITY_SERVICE);
// 设置自定义的可访问性委托
setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this));
// 处理嵌套滚动和布局管理器初始化
boolean nestedScrollingEnabled = true; // 默认启用嵌套滚动
if (attrs != null) { // 再次处理 XML 属性(布局管理器相关)
int defStyleRes = 0;
// 获取 RecyclerView 自定义属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
defStyle, defStyleRes);
// 读取布局管理器类名(例如 LinearLayoutManager)
String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
// 处理子控件焦点优先级
int descendantFocusability = a.getInt(
R.styleable.RecyclerView_descendantFocusability, -1);
if (descendantFocusability == -1) {
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); // 默认行为
}
a.recycle(); // 回收 TypedArray
// (5)根据属性创建 LayoutManager, 如果layoutManagerName是null, 则说明没有在布局中设置, 后续需要代码调用setLayoutManager
createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);
// 处理嵌套滚动属性(API 21+)
if (Build.VERSION.SDK_INT >= 21) {
a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
defStyle, defStyleRes);
nestedScrollingEnabled = a.getBoolean(0, true); // 读取嵌套滚动使能状态
a.recycle();
}
} else {
setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); // 默认无属性时
}
// 处理 EdgeEffect 相关属性(虽然立即回收,但可能用于默认样式继承)
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.EdgeEffect);
a.recycle(); // 快速回收,可能后续由 LayoutManager 处理
// (6) sdk>21支持动态配置, 最终设置嵌套滚动是否启用(兼容所有 API 级别); 如果sdk<21, 默认启用); 如果sdk>=21, 读取属性配置,不设置默认启用;
setNestedScrollingEnabled(nestedScrollingEnabled);
}
- (1)处: 获取
ViewConfiguration
对象,读取系统级参数配置 - (2)处: 设置 ItemAnimator 的监听器(用于动画生命周期回调),如无配置自定义Animator则默认使用
DefaultItemAnimator
- (3)处: 初始化 Adapter管理相关组件
AdapterHelper
- (4)处: 初始化子视图管理辅助类
ChildHelper
- (5)处: 根据属性创建 LayoutManager, 如果layoutManagerName是null, 则说明没有在布局中设置, 后续需要代码调用setLayoutManager
- (6)处: sdk>21支持动态配置, 最终设置嵌套滚动是否启用(兼容所有 API 级别); 如果sdk<21, 默认启用); 如果sdk>=21, 读取属性配置,不设置默认启用;
3. 设置布局管理器LayoutManager
java
// 成员变量视图回收复用机制的核心控制器
final Recycler mRecycler = new Recycler();
/**
* 成员变量
* 保存用于动画的视图数据
*/
final ViewInfoStore mViewInfoStore = new ViewInfoStore();
/**
* 设置 RecyclerView 使用的布局管理器
*
* @param layout 要应用的布局管理器实例(如 LinearLayoutManager、GridLayoutManager)
*/
public void setLayoutManager(LayoutManager layout) {
// 如果新布局与当前布局相同则直接返回(优化重复设置)
if (layout == mLayout) {
return;
}
// 停止所有正在进行的滚动(防止布局切换时的滚动冲突)
stopScroll();
/* 处理旧布局的清理工作 */
if (mLayout != null) {
// 结束所有正在进行的项目动画
if (mItemAnimator != null) {
mItemAnimator.endAnimations(); // 强制终止所有动画
}
// (1)回收所有已附加的视图和临时缓存视图
// (2)主视图回收
mLayout.removeAndRecycleAllViews(mRecycler);
// (3)临时碎片视图回收
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear(); // (4)清空回收站
// (5)如果已附加到窗口,通知布局管理器分离事件
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
// 解绑旧布局与 RecyclerView 的关联
mLayout.setRecyclerView(null);
mLayout = null; // 释放旧布局引用
} else {
// 无旧布局时仍清空回收站(确保状态干净)
mRecycler.clear();
}
/* 防御性清理:确保所有子视图被移除 */
mChildHelper.removeAllViewsUnfiltered(); // 通过 ChildHelper 彻底移除所有视图
/* 设置新布局管理器 */
mLayout = layout; // 更新布局引用
if (layout != null) {
// 安全性检查:布局不能重复绑定
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("布局管理器 " + layout
+ " 已绑定到其他 RecyclerView: " + layout.mRecyclerView);
}
// 建立双向绑定
mLayout.setRecyclerView(this); // 让布局layoutManager持有 RecyclerView 引用
// 如果已附加到窗口,通知布局管理器附加事件
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
/* (6) 后续处理 */
mRecycler.updateViewCacheSize(); // 根据新布局调整视图缓存策略
requestLayout(); // 触发重新测量和布局(关键!使新布局生效)
}
-
一般情况下setLayoutManager()方法只会在RecyclerView初始化时调用一次,所以不会出现重复绑定的情况。但是,在某些情况下,比如在RecyclerView的onCreateViewHolder()方法中调用setLayoutManager()方法,可能会出现重复绑定的情况。
-
动态setLayoutManager时: 异步化布局计算(API 26+), 限制: 部分布局操作仍需主线程完成
java// 使用AsyncLayoutInflater异步加载 new AsyncLayoutInflater(context).inflate( R.layout.grid_item, recycler, (view, resid, parent) -> { // 在子线程计算布局参数 post(() -> recycler.setLayoutManager(gridLayout)); } );
-
在(1)-(6)处 均使用到了mRecycler, recycler详解在3.1中,
Recycler
是视图回收复用机制的核心控制器
3.1 Recycler
视图回收复用机制的核心控制器
3.1.1 根据Recycler成员变量跟踪部分核心代码
java
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2; //默认mAttachedScrap最大缓存数量
...
// 设置缓存池的扩展
void setViewCacheExtension(ViewCacheExtension extension) {
mViewCacheExtension = extension;
}
// 外部调用设置recyclerView的缓存池
// 如果您想在多个 RecyclerView 之间回收视图,请创建一个 RecycledViewPool 实例,并使用RecyclerView#setRecycledViewPool
void setRecycledViewPool(RecycledViewPool pool) {
if (mRecyclerPool != null) {
mRecyclerPool.detach();
}
mRecyclerPool = pool;
if (pool != null) {
mRecyclerPool.attach(getAdapter());
}
}
// 获取mRecyclerPool,可以使用此
RecycledViewPool getRecycledViewPool() {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
}
return mRecyclerPool;
}
// Android RecyclerView 中处理 ViewHolder 回收逻辑的核心部分
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool.");
}
// 标记为添加到mAttachedScrap容器中, false表示无需重新绑定
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
//初始化mChangedScrap容器
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
// 将 ViewHolder 标记为附加到 mChangedScrap(true 表示需要处理更新)。
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
// mCachedViews 和 RecycledViewPool
void recycleViewHolderInternal(ViewHolder holder) {
// 检查1:确保 ViewHolder 未被临时废弃(scrapped)或仍附着在父视图上
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw new IllegalArgumentException(
"Scrapped or attached views may not be recycled. isScrap:"
+ holder.isScrap() + " isAttached:"
+ (holder.itemView.getParent() != null));
}
// 检查2:确保 ViewHolder 未被临时分离(tmpDetached)
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed "
+ "from RecyclerView before it can be recycled: " + holder);
}
// 检查3:确保 ViewHolder 未被标记为忽略状态
if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ " should first call stopIgnoringView(view) before calling recycle.");
}
// 检查 ViewHolder 的临时状态是否阻止回收(如有正在运行的动画)
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
// 如果临时状态阻止回收,尝试通过 Adapter 的钩子强制回收
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false; // 标记是否缓存到 mCachedViews
boolean recycled = false; // 标记是否回收到 RecycledViewPool
// 调试检查:确保不会重复回收已缓存的 ViewHolder
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder);
}
// 决定是否回收:强制回收 或 正常可回收状态
if (forceRecycle || holder.isRecyclable()) {
// 条件1:缓存池未满 (mViewCacheMax > 0)
// 条件2:ViewHolder 无无效/移除/更新/位置未知标志
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// 如果缓存池已满,移除最早缓存的 ViewHolder
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0); // 回收位置0的ViewHolder
cachedViewSize--;
}
// 计算最佳插入位置(考虑预取优化)
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// 跳过最近预取的 ViewHolder,优先保留非预取项
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
// 添加到缓存池
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
// 如果未缓存,则回收到全局 ViewPool
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// 不可回收情况处理(通常由动画引起)
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists");
}
}
// 关键清理:从动画信息存储中移除, 动画如果不清楚 则复用时有可能会错乱,且如果被回收了可能会造成内存泄漏问题
mViewInfoStore.removeViewHolder(holder);
// 特殊场景清理:临时状态阻止回收时的资源释放
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
/**
* Set the maximum number of detached, valid views we should retain for later use.
* 开发者配置的缓存最大值(通过 RecyclerView.setItemViewCacheSize() 设置)
* @param viewCount Number of views to keep before sending views to the shared pool
*/
public void setViewCacheSize(int viewCount) {
mRequestedCacheMax = viewCount;
updateViewCacheSize();
}
// 生成实际生效的最大缓存值 实际上再setLyoutmanager 和 setViewCacheSize的时候会调用这个方法
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
...
}
-
mRequestedCacheMax
和mViewCacheMax
mRequestedCacheMax
: 开发者配置的缓存最大值(通过 RecyclerView.setItemViewCacheSize() 设置)mViewCacheMax
实际生效的缓存最大值(通过GapWoker根据系统资源动态调整mLayout.mPrefetchMaxCountObserved(最大预取数量), 在3.2中有GapWorker的详解)
-
mUnmodifiableAttachedScrap
: 提供对mAttachedScrap
的只读访问接口,防止外部意外修改内部状态 -
mChangedScrap
: 已更新且需要重新处理,专门存储数据已变更的 ViewHolder(如被 notifyItemChanged() 标记的项,在GapWorker
执行预取时最先取出的缓存 一个 ViewHolder 会被添加到 mChangedScrap 中,当同时满足以下条件时:- 它没有被标记为 REMOVED 或 INVALID。
- 它对应的数据项被更新了(isUpdated() 返回 true)。
- 且当前不允许直接重用被更新的 ViewHolder(即 canReuseUpdatedViewHolder(holder) 返回 false)。
-
mAttachedScrap
: 临时存储与当前布局关联的 ViewHolder,这些视图仍在 RecyclerView 可见范围内但需要暂时分离以重新布局。 从scrapView
方法中可以看出viewholder被标记为FLAG_REMOVED
或FLAG_INVALID
或者 holder未update,或者允许复用已更新的 ViewHolder(canReuseUpdatedViewHolder
返回true)- 触发场景
- 布局更新(如 notifyDataSetChanged() 触发onLayout(),onMessure())
- 插入/删除项动画前的视图分离
- 特点
- 不触发 onViewRecycled 回调
- 同一布局周期内快速复用,不需要重新绑定数据
- 存活周期:仅在单次布局过程中有效
- 触发场景
-
mCachedViews
(一级缓存) 离开屏幕,存储最近被移除但未被回收的视图, 完整数据保留 内存占用高 复用成本低(无需绑定)- 移除特点:
- 线性列表:存储最近被移除的 ViewHolder
- 先进先出:移除最早缓存的项
- 降级存储:淘汰的 ViewHolder 转入 RecycledViewPool
- 允许重复类型:可以包含多个相同 viewType 的 ViewHolder
- 优势:无需重新绑定数据(数据未失效)
- 容量限制:mViewCacheMax(默认=2),预取GapWorker动态配置,例:
recyclerView.setItemViewCacheSize(4); // 增大 mCachedViews 大小
- 插入优化:避开预取位置(lastPrefetchIncludedPosition)
- 移除特点:
-
mViewCacheExtension
: 开发者自定义的二级缓存扩展(介于 mCachedViews(一级缓存) 和 mRecyclerPool 之间)java/** * 自定义二级缓存扩展,用于开发者自定义二级缓存, 介于 mCachedViews(一级缓存) 和 mRecyclerPool 之间 */ public abstract static class ViewCacheExtension { /** * 返回一个可以绑定到给定适配器位置(Adapter position)的View。 * 该方法**不应**创建一个新的View。它期望返回一个已经创建好的View,该View可以被重新用于给定的类型(type)和位置 * 如果该View被标记为忽略(ignored),那么在返回该View之前,应该先调用{@link LayoutManager#stopIgnoring * 如有必要,RecyclerView会将返回的View重新绑定到对应位置。 * @param recycler 可用于绑定View的Recycler * @param position 适配器位置(Adapter position) * @param type View的类型,由适配器定义 * @return 一个绑定到给定位置的View,如果没有可复用的View,则返回NULL */ public abstract View getViewForPositionAndType(Recycler recycler, int position, int type); }
- 单向操作:RecyclerView只从该扩展中获取视图,但不会自动将视图存入该扩展。开发者需要自己管理视图的存储和回收。
- 该方法不允许创建新视图,只能返回已有的视图。
- 使用场景:
- 固定位置视图缓存:如常驻顶部的HeaderView,可以预加载并始终通过此扩展返回。
- 按需保留视图:对某些特定位置的视图(如高优先项)保留引用以避免被回收到Pool。
- 跨列表复用视图:在不同RecyclerView之间共享特定视图(需注意生命周期)。
- 典型实现案例:
javapublic class CustomViewCache extends ViewCacheExtension { // 自定义缓存容器 (SparseArray<View> 按位置缓存) private final SparseArray<View> positionCache = new SparseArray<>(); private final SparseArray<View> typeCache = new SparseArray<>(); // 开发者需主动缓存视图 (通常在onViewRecycled时) public void cacheView(int position, View view) { positionCache.put(position, view); } public void cacheTypeView(int type,View view){ typeCache.put(type,view); } @Override public View getViewForPositionAndType(Recycler recycler, int pos, int type) { if (isFixedPosition(pos)) { // 固定位置视图:直接返回(假设已预绑定) return positionCache.get(pos); } else { // 普通视图:从类型缓存获取并重新绑定 View view = typeCache.get(type).poll(); if (view != null) { recycler.bindViewToPosition(view, pos); // 动态绑定新位置 } return view; } } }
- 生命周期管理: 需要在适当时机清理缓存
java// 在RecyclerView销毁时 recyclerView.addOnDetachListener(() -> { cache.clear(); customCache = null; // 防止内存泄漏 }); // 数据失效时 (如刷新数据源) adapter.registerAdapterDataObserver(new AdapterDataObserver() { @Override public void onChanged() { cache.clear(); } });
-
mRecyclerPool
RecycledViewPool(长期缓存), 无数据保留 内存占用低 复用成本高(需重新绑定)-
- 共享特点:全局共享缓存池, 默认是不共享,如需共享需要设置
setRecycledViewPool(RecycledViewPool pool)
- 共享特点:全局共享缓存池, 默认是不共享,如需共享需要设置
场景 推荐方案 原因 单个 RecyclerView 默认私有池 简单高效,无需额外管理 同屏多个相同类型列表 共享池 提高复用率,减少内存占用 分页/分块加载数据 共享池 跨页复用视图,平滑滚动 不同 Activity 的列表 独立池 生命周期不同,难以管理 -
- 类型分桶存储
- 分桶机制:每个 viewType 对应一个独立的 ViewHolder 列表
- 动态创建:遇到新 viewType 时自动创建新桶
javapublic static class RecycledViewPool { // 按 viewType 分桶 private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<>(); }
-
- 容量控制
java// RecycledViewPool private static final int DEFAULT_MAX_SCRAP = 5; private SparseIntArray mMaxScrap = new SparseIntArray(); public void setMaxRecycledViews(int viewType, int max) { mMaxScrap.put(viewType, max); }
- 默认容量:每类型最多存储 5 个 ViewHolder
- 独立配置:可为不同 viewType 设置不同容量上限
- 溢出处理:当超过上限时,移除最早添加的 ViewHolder
-
- 状态重置机制
javapublic void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } // 关键重置操作 // 包括: // - 清除绑定数据 // - 重置标志位 (FLAG_BOUND, FLAG_UPDATE等) // - 清除临时状态 scrap.resetInternal(); scrapHeap.add(scrap); }
- 数据剥离:清除所有与特定位置相关的数据
- 状态归零:重置为"空白画布"状态
- 复用要求:下次使用时必须重新绑定数据
-
- 复用顺序策略
- LIFO(后进先出):优先复用最近添加的 ViewHolder
- 性能优化:最近使用的 ViewHolder 更可能在缓存中
-
- 线程安全
-
- 生命周期特点: 自动清理:当所有关联的 RecyclerView 被销毁时,池可被 GC 回收
javapublic void onDetachedFromWindow() { // RecyclerView 被移除时 mRecycler.detach(); } void detach() { mRecycledViewPool.detach(); // 通知池解耦 }
-
总结: mChangedScrap
和 mAttachedScrap
使用场景:
场景 | 条件组合 | 缓存位置 |
---|---|---|
普通更新(无payload) | isUpdated=true + canReuse=false |
✅ mChangedScrap |
带payload的局部更新 | isUpdated=true + canReuse=true |
❌ mAttachedScrap |
删除操作 | FLAG_REMOVED 被标记 |
❌ mAttachedScrap |
数据失效 | FLAG_INVALID 被标记 |
❌ mAttachedScrap |
缓存使用顺序:
mChangeScrap
mAttachedScrap
mCachedViews
mViewCacheExtension
mRecyclerPool
3.2 GapWorker
预取
在 RecyclerView
中,GapWorker
实现了Runnable 在sdk_int>=21时在onAttachedToWindow初始化,用于预取数据。是Handler得looper一样,使用ThreadLoacal绑定当前线程和GapWorker,一个线程只存在一个GapWorker,每个GapWorker对象可以存多个RecyclerView,所以 GapWorker
是线程安全的。
java
// RecyclerView.java 成员变量 如果sdk_int>=21 ALLOW_THREAD_GAP_WORK=true, 默认开启预抓取
GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry =
ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null;
- 在RecyclerView初始化时,当sdk_int>=21时,预抓取策略
mPrefetchRegistry
被初始化为GapWorker.LayoutPrefetchRegistryImpl
对象,否则为null
java
// RecyclerView.java
// GapWorker的run()执行条件1
onTouchEvent(e)
{ ...
case MotionEvent.ACTION_MOVE: {... mGapWorker.postFromTraversal(this, dx, dy);}
...}
// RecyclerView.java 内部类ViewFlinger处理惯性飞滑事件逻辑
// GapWorker的run()执行条件2
class ViewFlinger implements Runnable {
...
public void run() {
...
mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
...
}
...
}
// GapWorker.java
/**
* Schedule a prefetch immediately after the current traversal.
*/
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
// 执行GapWorker的run方法
recyclerView.post(this);
}
}
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
// GapWorker.java
@Override
public void run() {
try {
Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query last vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
mRecyclerViews.get(0).getDrawingTime());
if (lastFrameVsyncNs == 0) {
// abort - couldn't get last vsync for estimating next
return;
}
// TODO: consider rebasing deadline if frame was already dropped due to long UI work.
// Next frame will still wait for VSYNC, so we can still use the gap if it exists.
long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
Trace.endSection();
}
}
预抓取的执行入口
- RecyclerView滑动时,手指未离开屏幕, 也就是action_move状态
- RecyclerView滑动时,手指离开屏幕, 也就是飞滑惯性状态
- 调用
postFromTraversal
方法 执行run()方法内, 进一步执行预抓取prefetch
方法
抓取策略 prefetch(nextFrameNs);
参数nextFrameNs:
- long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs; 含义:下一帧的VSync时间 = 上一帧VSync时间 + 帧间隔
- VSync 信号的基础原理
- VSync(垂直同步)是 Android 显示系统的核心机制,每帧画面渲染前都会触发一次。
- 屏幕刷新率决定了 VSync 信号的间隔时间(如 60Hz 屏幕 ≈ 16.6ms/帧,90Hz ≈ 11.1ms/帧)。
- mFrameIntervalNs 就是当前屏幕刷新周期的纳秒值(通过 Display.getRefreshRate() 计算得出)。
java
// GapWork.java
void prefetch(long deadlineNs) {
// 1
buildTaskList();
// 2
flushTasksWithDeadline(deadlineNs);
}
- 步骤1:构建预取任务队列
- 步骤2:在截止时间内执行任务,避免主线程卡顿
- 确保预取操作在 deadlineNs 前完成,防止影响下一帧渲染
- 若任务未能在截止时间内完成,剩余任务会被延迟到下一帧处理
构建预取任务列表
java
private void buildTaskList() {
// 1. 更新所有RecyclerView的预取注册信息(一般都是1, 存在嵌套可能)
final int viewCount = mRecyclerViews.size();
int totalTaskCount = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
// (4)
view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
totalTaskCount += view.mPrefetchRegistry.mCount;
}
// 2. 初始化任务列表
mTasks.ensureCapacity(totalTaskCount);
// 3. 填充任务列表
int totalTaskIndex = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+ Math.abs(prefetchRegistry.mPrefetchDy);
for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
final Task task;
if (totalTaskIndex >= mTasks.size()) {
task = new Task();
mTasks.add(task);
} else {
// 复用Task对象
task = mTasks.get(totalTaskIndex);
}
// 从预取数组中读取数据
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
// immediate:标记该任务是否需要立即执行。判断条件为:distanceToItem <= viewVelocity。
// 若成立,表示该item可能在下一帧就会进入屏幕,需要高优先级处理。
// 若不成立,表示有更多时间缓冲,可稍后处理。
task.immediate = distanceToItem <= viewVelocity;
// 当前视图滚动速度, 计算视图速度:viewVelocity = |dx| + |dy|,用于估算滚动速度(逻辑单位/帧)
task.viewVelocity = viewVelocity;
task.distanceToItem = distanceToItem;
task.view = view;
task.position = prefetchRegistry.mPrefetchArray[j];
totalTaskIndex++;
}
}
// ... and priority sort
Collections.sort(mTasks, sTaskComparator);
}
// GapWorker.LayoutPrefetchRegistryImpl implementation RecyclerView.LayoutManager.LayoutPrefetchRegistry
LayoutPrefetchRegistryImpl implementation RecyclerView.LayoutManager.LayoutPrefetchRegistry {
// (5)
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
mCount = 0;
if (mPrefetchArray != null) {
Arrays.fill(mPrefetchArray, -1);
}
final RecyclerView.LayoutManager layout = view.mLayout;
if (view.mAdapter != null
&& layout != null
&& layout.isItemPrefetchEnabled()) {
if (nested) {
// nested prefetch, only if no adapter updates pending. Note: we don't query
// view.hasPendingAdapterUpdates(), as first layout may not have occurred
if (!view.mAdapterHelper.hasPendingUpdates()) {
// 具体实现在LayoutManager的子类中, 并在实现子类中调用addPosttion方法
layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
}
} else {
// momentum based prefetch, only if we trust current child/adapter state
if (!view.hasPendingAdapterUpdates()) {
layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
view.mState, this);
}
}
// 更新预取数量, 如果本次收集的预取数量mCount超过历史最大值,则更新最大值记录,并调整RecyclerView的缓存大小(updateViewCacheSize()会根据最大值优化缓存池大小)
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
view.mRecycler.updateViewCacheSize();
}
}
}
// (6) 将需要预取的Item位置和像素距离添加到预取数组中
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
if (pixelDistance < 0) {
throw new IllegalArgumentException("Pixel distance must be non-negative");
}
// allocate or expand array as needed, doubling when needed
final int storagePosition = mCount * 2;
if (mPrefetchArray == null) {
mPrefetchArray = new int[4];
Arrays.fill(mPrefetchArray, -1);
} else if (storagePosition >= mPrefetchArray.length) {
final int[] oldArray = mPrefetchArray;
mPrefetchArray = new int[storagePosition * 2];
System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
}
// add position
mPrefetchArray[storagePosition] = layoutPosition;
mPrefetchArray[storagePosition + 1] = pixelDistance;
mCount++;
}
}
- 第一步: 遍历所有RecyclerView:mRecyclerViews保存了当前所有注册了预取的RecyclerView(通常一个页面只有一个,但嵌套时可能有多个)。
- 收集预取位置:对每个RecyclerView,调用其mPrefetchRegistry.collectPrefetchPositionsFromView()方法。该方法的作用是:
- 根据当前滚动方向(mPrefetchDx/mPrefetchDy)计算需要预取的item位置(即将进入屏幕的item)。
- 结果存储在mPrefetchRegistry的两个数组中:
- mPrefetchArray:偶数索引存position,奇数索引存该item与当前可视区域的距离(逻辑距离,如item索引差)。
- mCount:有效预取项的数量。
- 统计总任务数:累加所有RecyclerView的预取项数量到totalTaskCount。
- 第二步: 初始化任务列表
mTasks.ensureCapacity(totalTaskCount);
- 确保任务列表mTasks(ArrayList
<Task>
)有足够的容量存放所有任务,避免多次扩容带来的性能开销。
- 确保任务列表mTasks(ArrayList
- 填充任务列表,并根据是否在下一帧执行任务进行排序
- (4)(5)(6) 之间的协作关系
collectPrefetchPositionsFromView
作为入口方法,根据情况调用LayoutManager的预取位置收集方法。LayoutManager
的collectInitialPrefetchPositions
或collectAdjacentPrefetchPositions
方法会通过回调addPosition
添加多个预取项。addPosition
方法负责将位置和距离信息存储到数组中。并得出最大预取数量mPrefetchMaxCountObserved
任务执行流程: flushTasksWithDeadline(deadlineNs)
java
// GapWorker.java
private void flushTasksWithDeadline(long deadlineNs) {
for (int i = 0; i < mTasks.size(); i++) {
final Task task = mTasks.get(i);
if (task.view == null) {
break; // done with populated tasks
}
flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
private void flushTaskWithDeadline(Task task, long deadlineNs) {
// 确定任务执行时限
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
// 执行核心预取操作
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
// 处理嵌套 RecyclerView
if (holder != null && holder.mNestedRecyclerView != null) {
prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
}
}
...
// prefetchPositionWithDeadline调用链核心预取操作
// 尝试在给定的截止时间(deadlineNs)之前获取一个ViewHolder
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// If there is a changed scrap, try to find from there
// 1. 预布局阶段的Changed Scrap
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// Find by position from scrap/hidden list/cache
// Scrap、Hidden列表或缓存
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 3.通过稳定ID从Scrap或缓存中获取
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// 4. ViewCacheExtension(自定义缓存)
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder");
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view.");
}
}
}
// 5. RecycledViewPool(回收池)
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
// 6. 如果都没有,则创建新的ViewHolde
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 如果ViewHolder还没有绑定数据,或者需要更新,或者无效,则尝试绑定
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 确保ViewHolder的itemView有正确的LayoutParams(RecyclerView.LayoutParams类型)。如果不存在或不匹配,则生成新的LayoutParams。
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
// 将ViewHolder与LayoutParams关联。
rvLayoutParams.mViewHolder = holder;
// mPendingInvalidate:如果从缓存中获取并且刚刚绑定了数据,则为true,表示需要刷新。
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
急任务 (immediate = true):
- 使用 FOREVER_NS (Long.MAX_VALUE) 表示无时间限制
- 原因:这些任务对应的 item 可能下一帧就进入屏幕,必须完成
非紧急任务:
- 使用统一的 deadlineNs(通常是下一帧 VSync 时间)
- 可能在时间耗尽时部分执行
tryGetViewHolderForPositionByDeadline
整体流程 方法按顺序尝试从不同的来源获取ViewHolder,直到成功或超时。来源包括:
- 预布局阶段的Changed Scrap
- Scrap、Hidden列表或缓存
- 通过稳定ID从Scrap或mCachedViews中获取
- ViewCacheExtension(自定义缓存)
- RecycledViewPool(回收池)
- 最后,如果都没有,则创建新的ViewHolder
3.3 LayoutManager
布局管理器
LayoutManager 是 RecyclerView 架构中的核心控制器,负责决定列表项的布局方式、滚动行为和复用策略。
3.3.1 核心职责概览
3.3.2 布局管理(核心功能)
决定列表项的排列方式,实现多样化布局:
布局类型 | 实现类 | 特点 |
---|---|---|
线性布局 | LinearLayoutManager | 单行/列排列,支持横向/纵向 |
网格布局 | GridLayoutManager | 等间距网格,支持跨列 |
瀑布流 | StaggeredGridLayoutManager | 错位网格,动态高度 |
自定义布局 | 继承 LayoutManager | 任意复杂布局(如环形、螺旋) |
关键方法:
java
// 布局入口
void onLayoutChildren(Recycler recycler, State state);
// 布局单个项
void layoutDecorated(View child, int left, int top, int right, int bottom);
3.3.3 滚动控制(核心功能)
管理滚动行为和位置:
java
// 滚动实现
int scrollVerticallyBy(int dy, Recycler recycler, State state);
int scrollHorizontallyBy(int dx, Recycler recycler, State state);
// 滚动到指定位置
void scrollToPosition(int position);
void smoothScrollToPosition(RecyclerView recyclerView, State state, int position);
滚动特性:
- 支持物理模拟滚动(fling)
- 精确控制滚动偏移量
- 处理边界效果(overscroll)
3.3.4 回收复用协调(核心功能)
与 Recycler 协同工作:
java
// 获取视图
View getViewForPosition(int position);
// 回收视图
void recycleView(View child);
复用流程:
3.3.5 动画支持(核心功能)
为增删改动画提供布局信息:
java
// 记录动画前位置
void onLayoutChildren(Recycler recycler, State state) {
// 保存预布局信息
layoutForPredictiveAnimations();
}
// 计算动画偏移
void calculateItemDecorationsForChild(View child, Rect outRect);
动画工作流:
- 记录预布局状态
- 执行数据变更
- 计算最终布局
- 生成动画差值
3.3.6 测量与布局(核心功能)
控制RecyclerView自身尺寸:
java
// 测量RecyclerView
void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec);
// 自动处理wrap_content
boolean isAutoMeasureEnabled();
尺寸计算模式:
模式 | 行为 |
---|---|
固定尺寸 | 直接使用给定尺寸 |
AT_MOST | 计算内容尺寸 |
UNSPECIFIED | 计算最小可行尺寸 |
3.3.7 状态保存与恢复(核心功能)
保持UI状态一致性:
java
// 保存布局状态
Parcelable onSaveInstanceState();
// 恢复状态
void onRestoreInstanceState(Parcelable state);
保存内容:
- 滚动位置
- 锚点信息
- 自定义布局状态
3.3.8 预取优化
实现性能关键优化:
java
// 收集预取位置
void collectAdjacentPrefetchPositions(...);
void collectInitialPrefetchPositions(...);
预取流程:
- 预测滚动方向
- 识别即将进入的项
- 提前绑定数据
3.3.9 设计哲学
LayoutManager 是 RecyclerView 架构中的核心控制器,负责决定列表项的布局方式、滚动行为和复用策略。
- 关注点分离
- Adapter:数据与视图绑定
- Recycler:视图缓存管理
- LayoutManager:布局决策核心
- GapWork 预取
- 模板方法模式
java
public abstract class LayoutManager {
// 基本方法(子类实现)
abstract void onLayoutChildren(Recycler recycler, State state);
// 带默认实现的方法
public boolean supportsPredictiveItemAnimations() {
return false;
}
}
- 状态机管理 布局状态流转:
css
[初始] → [预布局] → [实际布局] → [动画] → [完成]
↑ ↓
└── 增量更新 ←─┘
4.ItemDecoration详解
RecyclerView.ItemDecoration 详解
ItemDecoration
是 RecyclerView 中用于绘制装饰视图(如分割线、间隔、标记等)的核心组件,可在项目周围添加视觉装饰而无需修改适配器或 ViewHolder。
核心方法
getItemOffsets()
• 作用:设置项目周围的偏移量(内边距)
• 参数:
• outRect
: 设置上下左右偏移量
• view
: 当前项目视图
• parent
: RecyclerView
• state
: RecyclerView 状态
• 使用场景:添加项目间距或分割线预留空间
java
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
int position = parent.getChildAdapterPosition(view);
if (position == RecyclerView.NO_POSITION) return;
// 排除最后一项的底部间距
if (position != parent.getAdapter().getItemCount() - 1) {
outRect.bottom = 20; // 设置底部间距(分割线高度)
}
}
onDraw()
• 作用:在 RecyclerView 画布上绘制装饰(在项目下方绘制)
• 调用时机:在项目绘制之前执行
• 使用场景:绘制背景型装饰(如分割线)
java
@Override
public void onDraw(Canvas c, RecyclerView parent, State state) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int pos = parent.getChildAdapterPosition(child);
// 非最后一项时绘制分割线
if (pos != parent.getAdapter().getItemCount() - 1) {
int top = child.getBottom(); // 分割线起点Y
int bottom = top + 20; // 分割线高度
c.drawRect(0, top, parent.getWidth(), bottom, paint);
}
}
}
onDrawOver()
• 作用:在 RecyclerView 画布上绘制装饰(在项目上方绘制)
• 调用时机:在项目绘制之后执行
• 使用场景:绘制覆盖型装饰(如悬浮标记)
java
@Override
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
// 示例:在第一个项目顶部绘制红色标记
View firstChild = parent.getChildAt(0);
if (firstChild != null) {
c.drawCircle(50, firstChild.getTop(), 10, redPaint);
}
}
完整自定义分割线实现
java
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Paint paint;
private int height;
public DividerItemDecoration(Context context) {
paint = new Paint();
paint.setColor(ContextCompat.getColor(context, R.color.divider));
height = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
if (parent.getChildAdapterPosition(view) != parent.getAdapter().getItemCount() - 1) {
outRect.bottom = height;
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, State state) {
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
if (parent.getChildAdapterPosition(child) == parent.getAdapter().getItemCount() - 1) {
continue;
}
c.drawRect(0, child.getBottom(), parent.getWidth(), child.getBottom() + height, paint);
}
}
}
// 使用方式
recyclerView.addItemDecoration(new DividerItemDecoration(context));
进阶技巧
-
动态颜色分割线
javapaint.setColor(isSpecialPosition(position) ? specialColor : normalColor);
-
分组悬浮标题 • 在
onDrawOver()
中判断分组变化• 绘制固定标题栏并跟随滚动
-
不对称间距
javaif (isFirstRow(position)) outRect.top = largeSpace; if (isEdgeColumn(position)) outRect.right = largeSpace;
-
圆形指示器 • 在
onDraw()
中为特定状态的项目绘制角标
常见问题解决方案
-
分割线不显示 • 确认
getItemOffsets()
设置了正确的偏移量• 检查
onDraw()
中坐标计算是否正确 -
滚动时重复绘制 • 使用
RecyclerView.ItemDecoration
而非直接修改适配器• 避免在
onDraw()
中创建新对象 -
最后一项多余分割线
javaif (position == adapter.getItemCount() - 1) return;
-
网格布局特殊处理
java// 判断是否为最后一行 if ((position + 1) % spanCount == 0) { outRect.bottom = spacing; }
性能优化 • Paint 对象复用:在构造函数中初始化并复用 Paint
• 避免 onDraw() 中分配内存:提前计算坐标
• 使用 Canvas.clipRect() 限制绘制区域
• 关闭硬件加速(必要时):
xml
android:layerType="software"
通过合理使用 ItemDecoration
,可实现丰富视觉效果而无需侵入业务逻辑代码,是 RecyclerView 高级定制化的核心技巧。
5 Adapter详解
在 Android 开发中,RecyclerView.Adapter
是 RecyclerView
组件的核心桥梁,负责将数据集绑定到视图上。下面从多个维度详解 Adapter
的设计原理、实现步骤和优化技巧:
5.1. Adapter 核心职责
- 视图创建 :通过
onCreateViewHolder()
将 XML 布局转换为ViewHolder
对象 - 数据绑定 :通过
onBindViewHolder()
将数据映射到视图元素 - 数据管理:处理数据更新通知机制(增/删/改)
5.2 Adapter 关键方法解析
5.2.1. 必需实现的方法
java
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
// 返回 ViewHolder 实例
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_layout, parent, false);
return new ViewHolder(view);
}
// 绑定数据到 ViewHolder
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
DataItem item = dataList.get(position);
holder.bind(item); // 将数据映射到视图
}
// 返回数据总量
@Override
public int getItemCount() {
return dataList.size();
}
}
5.2.2. 进阶方法
java
// 多布局类型支持(需要重写 getItemViewType)
@Override
public int getItemViewType(int position) {
return dataList.get(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
}
// 局部更新(高效刷新)
public void updateItem(int position) {
notifyItemChanged(position);
// 其他局部更新方法:
// notifyItemInserted(), notifyItemRemoved(), notifyItemRangeChanged()
}
5.3 ViewHolder 设计模式
java
class ViewHolder extends RecyclerView.ViewHolder {
private final TextView titleView;
private final ImageView iconView;
public ViewHolder(View itemView) {
super(itemView);
titleView = itemView.findViewById(R.id.tv_title);
iconView = itemView.findViewById(R.id.iv_icon);
}
// 数据绑定入口
void bind(DataItem item) {
titleView.setText(item.getName());
Glide.with(itemView).load(item.getImageUrl()).into(iconView);
// 点击事件
itemView.setOnClickListener(v -> {
int pos = getAdapterPosition();
if(pos != RecyclerView.NO_POSITION) {
onItemClick(pos);
}
});
}
}
设计优势:
- 避免重复调用
findViewById()
- 封装视图组件访问逻辑
- 便于添加交互动画
5.4 数据更新最佳实践
5.4.1 DiffUtil 高效刷新
java
public class DataDiffCallback extends DiffUtil.Callback {
private final List<DataItem> oldList, newList;
// 比较逻辑实现...
@Override
public boolean areItemsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).getId() == newList.get(newPos).getId();
}
@Override
public boolean areContentsTheSame(int oldPos, int newPos) {
return oldList.get(oldPos).equals(newList.get(newPos));
}
}
// 使用 DiffUtil 更新
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DataDiffCallback(oldList, newList));
dataList = newList;
result.dispatchUpdatesTo(adapter); // 智能执行局部更新
5.4.2 数据更新注意事项
java
// 错误方式(直接替换引用)
dataList = newDataList;
notifyDataSetChanged(); // 全量刷新,丢弃所有缓存
// 正确方式(保持引用不变)
dataList.clear();
dataList.addAll(newDataList);
notifyItemRangeInserted(0, newDataList.size());
5.5 Adapter 高级技巧
5.5.1 多类型布局实现
java
@Override
public int getItemViewType(int position) {
DataItem item = dataList.get(position);
if(item instanceof HeaderItem) return TYPE_HEADER;
if(item instanceof AdItem) return TYPE_AD;
return TYPE_NORMAL;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch(viewType) {
case TYPE_HEADER:
return new HeaderViewHolder(...);
case TYPE_AD:
return new AdViewHolder(...);
default:
return new NormalViewHolder(...);
}
}
5.5.2 尾部加载更多实现
java
// Adapter 属性
private boolean showLoadingItem = false;
@Override
public int getItemCount() {
return dataList.size() + (showLoadingItem ? 1 : 0);
}
// 监听滚动
recyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if(!layoutManager.canScrollVertically(1)) { // 到达底部
showLoadingItem = true;
notifyItemInserted(getItemCount());
loadMoreData(); // 触发加载
}
}
});
5.6 常见问题解决
-
图片闪烁问题:
java// 在 ViewHolder 中重置图片 @Override public void onViewRecycled(ViewHolder holder) { holder.iconView.setImageDrawable(null); }
-
位置错乱处理:
java// 使用稳定的 ID 机制 @Override public long getItemId(int position) { return dataList.get(position).getId(); } // Adapter 构造函数中设置 setHasStableIds(true);
-
视图回收冲突:
java// 取消异步任务 @Override public void onViewDetachedFromWindow(ViewHolder holder) { holder.cancelPendingRequests(); }
5.7 性能优化清单
-
使用
DiffUtil
替代notifyDataSetChanged()
-
避免在
onBindViewHolder()
中创建对象 -
复杂布局采用组合视图(merge/include)
-
设置合理的
RecyclerView
缓存策略:xml<RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" app:recycledViewPoolSize="10" app:itemViewCacheSize="20"/>
5.8 架构演进方向
-
ViewHolder 泛型化:
javaabstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder<T>> { public abstract void onBindData(BaseViewHolder<T> holder, T item); }
-
结合 Data Binding:
xml<!-- item_layout.xml --> <layout> <data> <variable name="item" type="com.example.DataItem"/> </data> <TextView android:text="@{item.title}"/> </layout>
-
Paging 3 集成:
javaclass PagingAdapter extends PagingDataAdapter<DataItem, ViewHolder> { // 自动处理分页加载逻辑 }
通过以上完整实现,RecyclerView Adapter 能高效处理 10,000+ 项 的数据集,同时保持 60fps 的流畅滚动体验。关键在于合理运用局部更新、视图复用和异步加载技术。