
教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载本篇技术指南以 YCBlogs 仓库中 android/18.recyclerView/03.ViewHolder.md 为骨架系统梳理 ViewHolder 在 RecyclerView 中的核心职责、它与缓存复用体系Recycler、RecycledViewPool之间的配合原理并完整剖析一个基于 SparseArray 的BaseMViewHolder轻量封装实现。读完本文你将能解释为什么 onCreateViewHolder 只调用有限次数而 onBindViewHolder 每次都会执行这一面试高频问题并掌握一套可直接复用的 ViewHolder 封装写法用于减少 findViewById 调用、统一点击事件与数据绑定逻辑。01. ViewHolder 的作用ViewHolder 是 RecyclerView 体系中与 Adapter、LayoutManager、ItemDecoration、ItemAnimator 并列的核心组成。关于 RecyclerView 的整体结构可以参考 01.RecyclerView.md其中明确指出RecyclerView.Adapter处理数据集合并负责绑定视图ViewHolder持有所有的用于绑定数据或者需要操作的 ViewLayoutManager负责摆放视图等相关操作ItemDecoration负责绘制 Item 附近的分割线ItemAnimator为 Item 的一般操作添加动画效果增删条目等。ViewHolder 的作用可以概括为两点Adapter 应当拥有 ViewHolder 的子类并且 ViewHolder 内部应当存储一些子 View避免时间代价很大的findViewById操作。findViewById需要在 View 树中按 id 递归查找在列表滑动频繁创建/复用 Item 的场景下反复调用会带来可感知的性能开销ViewHolder 将子 View 引用焊死在自身字段上一次查找、多次使用。它是 Adapter 与 Item 视图之间的桥梁。RecyclerView 内部定义的 ViewHolder 类包含很多复杂属性如mItemViewType、mPosition、mOwnerRecyclerView、mFlags等内部使用场景很多。而我们日常开发中最常接触的只有两个方法onCreateViewHolder()当 RecyclerView 需要一个新类型的 Item ViewHolder 时调用用于创建 ViewHolderonBindViewHolder()当 RecyclerView 需要在特定位置position的 Item 上展示数据时调用用于绑定数据。02. ViewHolder 与复用机制2.1 必须重写的两个方法在复写RecyclerView.Adapter时需要我们复写两个核心方法Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { // 创建 Item 视图并返回相应的 ViewHolder } Override public void onBindViewHolder(VH holder, int position) { // 绑定数据到正确的 Item 视图上 }从字面上看onCreateViewHolder就是创建 ViewHolder、onBindViewHolder就是绑定 ViewHolder。它们与getItemCount()返回该 Adapter 所持有的 Item 数量、getItemViewType(int position)获取当前项是哪种类型的布局一起构成了 Adapter 最常用的重写方法集合详见 02.Adapter.md。2.2 复用场景模拟为什么 onCreateViewHolder 不会无限调用模拟一个简单场景列表中只有一种 ViewType上下滑动时需要的 ViewHolder种类只有一种但需要的 ViewHolder对象数量并不止一个。实际打印日志时会发现创建了 9 个 ViewHolder 之后需要的数量够用了无论再怎么滑动都不会再调用onCreateViewHolder但onBindViewHolder每次都会调用。第一反应是ViewHolder 对象的数量够用之后就停止调用onCreateViewHolder。这个判断需要到源码层面验证。2.3 查看 createViewHolder 源码RecyclerView 内部有一个createViewHolder方法从源码看这里并没有限制创建次数public final VH createViewHolder(ViewGroup parent, int viewType) { TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG); final VH holder onCreateViewHolder(parent, viewType); holder.mItemViewType viewType; TraceCompat.endSection(); return holder; }也就是说RecyclerView 本身并不拦截创建真正决定不再创建的是底层的缓存命中逻辑——当缓存池里能拿到可复用的 ViewHolder 时就根本走不到createViewHolder这一步。2.4 getViewForPosition 与 tryGetViewHolderForPositionByDeadline对于ViewHolder 数量够用之后就不再调用 onCreateViewHolder的现象需要查看 RecyclerView.Recycler 中获取视图的入口。原文档中给出了如下源码骨架public View getViewForPosition(int position) { return getViewForPosition(position, false); } View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { // 代码省略了有需要的小伙伴可以自己看看这里面逻辑实在太复杂呢 }tryGetViewHolderForPositionByDeadline内部的逻辑相当复杂但它的核心使命是为给定位置获取或创建一个ViewHolder 并返回对应的 itemView。LayoutManager 在布局过程中正是通过Recycler.getViewForPosition(position)向 Recycler 要 ViewHolder 的。2.5 结合缓存层级理解创建少、绑定多要彻底理解上面的现象需要结合 RecyclerView 的缓存机制。在 12.RecyclerView缓存原理.md 中Recycler 内部实现了多级缓存主要包含以下对象缓存对象说明匹配方式mAttachedScrap未与 RecyclerView 分离但已被标记移除的 ViewHolder 集合按 id 和 position 查找mChangedScrap数据已改变notify 系列方法触发的 ViewHolder 集合按 position 和 id 匹配mCachedViews一级缓存解决滑动抖动与 Prefetch默认容量 2按 position/id 匹配mViewCacheExtension开发者自定义缓存Recycler 只读取不写入按 position 和 type 匹配mRecyclerPoolViewHolder 缓存池容量不足时兜底按 type 匹配默认每 type 5 个这三层缓存决定了获取 ViewHolder 时的命中路径一级缓存mCachedViews / mAttachedScrap 等返回布局和内容都有效的 ViewHolder。命中一级缓存时无需调用onCreateViewHolder和onBindViewHolder。二级缓存ViewCacheExtension开发者自绘缓存按 position 和 type 匹配直接返回 View适合位置固定、内容不变的场景如固定的 Header。三级缓存RecycledViewPool返回布局有效、内容无效的 ViewHolder。按 type 匹配每个 type 默认缓存 5 个取出后需要再次调用Adapter.bindViewHolder绑定内容。因此当 ViewHolder 从屏幕滑出、进入mCachedViews数量少、默认 2时由于内容仍然有效滑动回来时直接复用、无需绑定当mCachedViews满了、ViewHolder 被挤入RecycledViewPool后虽然布局有效但内容被标记无效下次取出时必须重新走 onBindViewHolder 绑定数据。这正是创建少、绑定多的底层原因——列表的 Item 视图数量是有限的与屏幕可容纳数量相关一旦每种 type 的 ViewHolder 数量够用后续滑动只会反复命中缓存并重新绑定内容而不会再触发创建。关于 RecycledViewPool 的更多细节setMaxRecycledViews(int viewType, int max)如何从列表末尾丢弃超出的部分、getRecycledView(int viewType)如何从池中移除并返回末尾项、putRecycledView(ViewHolder)在池满时立即丢弃等可以继续阅读 09.RecycledViewPool.md。03. ViewHolder 简单封装理解了 ViewHolder 的作用与复用机制后就可以动手做一套通用封装把 findViewById 缓存、数据绑定入口、点击事件分发等重复代码收敛起来。原文档给出了一份完整且可运行的BaseMViewHolder实现下面逐段展开讲解。3.1 完整封装代码public abstract class BaseMViewHolderM extends RecyclerView.ViewHolder { // SparseArray 比 HashMap 更省内存在某些条件下性能更好只能存储 key 为 int 类型的数据 // 用来存放 View 以减少 findViewById 的次数 private SparseArrayView viewSparseArray; BaseMViewHolder(View itemView) { super(itemView); if (viewSparseArray null) { viewSparseArray new SparseArray(); } } public BaseMViewHolder(ViewGroup parent, LayoutRes int res) { super(LayoutInflater.from(parent.getContext()).inflate(res, parent, false)); if (viewSparseArray null) { viewSparseArray new SparseArray(); } } /** * 子类设置数据方法 * param data */ public void setData(M data) {} /** * 第二种 findViewById 方式 * 根据 ID 来获取 View * param viewId viewID * param T 泛型 * return 将结果强转为 View 或 View 的子类型 */ SuppressWarnings(unchecked) protected T extends View T getView(int viewId) { // 先从缓存中找找到的话则直接返回 // 如果找不到则 findViewById再把结果存入缓存中 View view viewSparseArray.get(viewId); if (view null) { view itemView.findViewById(viewId); viewSparseArray.put(viewId, view); } return (T) view; } /** * 获取上下文 context * return context */ protected Context getContext() { return itemView.getContext(); } /** * 获取数据索引的位置 * return position */ protected int getDataPosition() { RecyclerView.Adapter adapter getOwnerAdapter(); if (adapter ! null adapter instanceof RecyclerArrayAdapter) { return getAdapterPosition() - ((RecyclerArrayAdapter) adapter).getHeaderCount(); } return getAdapterPosition(); } /** * 获取 adapter 对象 * param T * return adapter */ Nullable private T extends RecyclerView.Adapter T getOwnerAdapter() { RecyclerView recyclerView getOwnerRecyclerView(); //noinspection unchecked return recyclerView ! null ? (T) recyclerView.getAdapter() : null; } Nullable private RecyclerView getOwnerRecyclerView() { try { Field field RecyclerView.ViewHolder.class.getDeclaredField(mOwnerRecyclerView); field.setAccessible(true); return (RecyclerView) field.get(this); } catch (NoSuchFieldException ignored) { ignored.printStackTrace(); } catch (IllegalAccessException ignored) { ignored.printStackTrace(); } return null; } /** * 添加子控件的点击事件 * param viewId 控件 id */ protected void addOnClickListener(IdRes final int viewId) { final View view getView(viewId); if (view ! null) { if (!view.isClickable()) { view.setClickable(true); } view.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { if (getOwnerAdapter() ! null) { if (((RecyclerArrayAdapter) getOwnerAdapter()).getOnItemChildClickListener() ! null) { ((RecyclerArrayAdapter) getOwnerAdapter()).getOnItemChildClickListener() .onItemChildClick(v, getDataPosition()); } } } }); } } // 省略部分代码 }3.2 两个构造入口封装类提供了两个构造方法分别对应两种 Item 视图来源BaseMViewHolder(View itemView)直接接收外部传入的 itemView适合在 Adapter 的onCreateViewHolder中自行 inflate 后再构造BaseMViewHolder(ViewGroup parent, LayoutRes int res)内部完成 inflate接收父容器与布局资源 id使用LayoutInflater.from(parent.getContext()).inflate(res, parent, false)创建 itemView。注意第三个参数必须传false避免 inflate 时立即 attach 到 parent 上导致重复添加。两个构造方法都会惰性初始化SparseArrayView保证缓存容器只创建一次。3.3 为什么用 SparseArray 而不是 HashMap封装中使用SparseArrayView存放 View其注释明确指出SparseArray 比 HashMap 更省内存在某些条件下性能更好只能存储 key 为 int 类型的数据。原因在于View 的 idR.id.xxx本身就是 int 类型天然契合 SparseArray 的 key 约束SparseArray 内部基于二分查找与数组存储避免了 HashMap 的 Entry 对象开销与自动装箱Integer内存占用更小对数量少、查找为主的 View 缓存场景二分查找的性能足够而内存收益明显。getView(int viewId)是这套封装的精髓每次获取子 View 时先从缓存中查找命中则直接返回未命中才调用itemView.findViewById(viewId)并把结果存入缓存。这样整个 Item 生命周期内同一个 id 只做一次 findViewById与 ViewHolder 设计的初衷完全一致。3.4 反射获取 mOwnerRecyclerView 的说明getOwnerRecyclerView()通过反射读取RecyclerView.ViewHolder的私有字段mOwnerRecyclerView从而拿到 ViewHolder 所属的 RecyclerView再经getOwnerAdapter()获取当前 Adapter。这是封装库中为了在 ViewHolder 内部拿到 Adapter 回调而使用的一种技巧优点ViewHolder 无需在构造时由外部传入 Adapter 引用解耦更彻底代价依赖 ViewHolder 内部私有字段名如果 Android 版本间该字段被重构或混淆规则覆盖会抛出NoSuchFieldException/IllegalAccessException代码中已对这两种异常做了捕获与打印兜底。getDataPosition()则考虑了带 Header 的封装 Adapter场景当 Adapter 是RecyclerArrayAdapter含 Header 区时真正的数据索引需要减去getHeaderCount()否则直接返回getAdapterPosition()。3.5 addOnClickListener 点击事件设计addOnClickListener(IdRes int viewId)展示了把子 View 点击事件统一收口到 Adapter 层的设计通过getView(viewId)复用缓存拿到目标子 View若 View 不可点击则先setClickable(true)设置OnClickListener回调时从 AdapterRecyclerArrayAdapter中取出对外暴露的getOnItemChildClickListener()并携带getDataPosition()数据索引回调给外部。这样业务层只需关注哪个控件被点击、数据在列表中的真实位置而无需关心 ViewHolder 内部 findViewById 与监听器创建的细节。3.6 封装后的使用方式子类继承BaseMViewHolderM后只需在构造方法中通过getView(R.id.xxx)声明要持有的子 View重写setData(M data)完成数据绑定需要子控件点击时调用addOnClickListener(R.id.xxx)。由于 ViewHolder 内部完成了 findViewById 缓存、context 获取、position 换算与点击事件分发每个 Item 的 ViewHolder 代码量被大幅压缩且多个列表页可以复用同一套 Base 封装这也是仓库中RecyclerArrayAdapter与 Adapter 封装库一脉相承的思路。关于多 type 页面如何借助封装避免instanceof类型检查与类型转换、增强 Adapter 扩展性与可维护性可以参考 20.RecyclerView实现多type页面.md 和 28.Adapter分组封装.md。04. 实战中的几点优化建议在 ViewHolder 封装的基础上结合仓库其他文档还有几个与 ViewHolder 强相关的实践要点4.1 Item 点击事件放置位置关于 RecyclerView 的 item 点击事件常见有在 onCreateViewHolder 中写在 onBindViewHolder 中写在 ViewHolder 中写三种方式。仓库在 21.RecyclerView优化处理.md 中明确给出的结论是onBindViewHolder()中频繁创建新的 OnClickListener 实例没有必要建议实际开发中应该在onCreateViewHolder()中每次为新建的 View 设置一次就行。这与本文封装的思路完全一致监听器只在 ViewHolder 创建时挂载一次而不是在每次绑定数据时重复 new 对象。更进一步可以对每个 ViewHolder 的子 View 统一使用一个全局 Listener通过view.getId()与view.getTag()区分具体控件与数据避免为每个新创建的 ViewHolder 都 new 一个监听器减少对象频繁创建带来的资源消耗。4.2 善用缓存复用池如果 Item 高度固定可以使用RecyclerView.setHasFixedSize(true)避免 requestLayout 浪费资源可以通过RecyclerView.setItemViewCacheSize(size)调大一级缓存容量以空间换时间提升滚动流畅度如果多个 RecyclerView 的 Adapter 相同例如嵌套 RecyclerView可以通过RecyclerView.setRecycledViewPool(pool)共享同一个RecycledViewPool让不同列表复用彼此的 ViewHolder减少重复创建。4.3 避免在绑定方法中做耗时操作onCreateViewHolder与onBindViewHolder对时间都非常敏感。类似 I/O 读写、Bitmap 解码一类的耗时操作最好不要放在这两个方法里否则快速滑动时容易出现卡顿。图片加载建议配合滚动监听暂停/恢复例如滑动时Glide.with(...).pauseRequests()停止后resumeRequests()详细说明同样见 21.RecyclerView优化处理.md。05. 总结ViewHolder 的本质持有子 View 引用、避免频繁 findViewById 的缓存单元是 Adapter 与 Item 视图之间的桥梁其核心交互方法为onCreateViewHolder()与onBindViewHolder()。复用的本质Recycler 通过getViewForPosition()→tryGetViewHolderForPositionByDeadline()逐级命中mCachedViews、ViewCacheExtension、RecycledViewPool三层缓存一级缓存内容有效直接复用三级缓存内容无效需要重新 bind——这就是onCreateViewHolder 次数有限、onBindViewHolder 每次必调的底层原因相关源码见 03.ViewHolder.md 与 12.RecyclerView缓存原理.md。封装的要点用 SparseArray 缓存 findViewById 结果、提供双构造入口、反射获取所属 RecyclerView 以解耦 Adapter 引用、统一收口子 View 点击事件最终得到一个可复用的BaseMViewHolderM基类完整实现见 03.ViewHolder.md。掌握了 ViewHolder 的职责、复用原理与封装套路你就能写出更流畅、更易维护的 RecyclerView 列表代码也能够在面试中把ViewHolder 与缓存复用这条链路讲得清晰透彻。赞分享教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载相关推荐MagenticBrain-8bit vs 原版模型8位量化如何在M2 Pro上实现10倍速推理MagenticBrain 8bit vs 原版模型8位量化如何在M2 Pro上实现10倍速推理 MagenticBrain 8bit是微软Magenticdeveloper-roadmap 之 Android 主题RecyclerView 高性能列表与 ViewHolder 复用机制全指南developer roadmap 之 Android 主题RecyclerView 高性能列表与 ViewHolder 复用机制全指南 本文围绕 devel文档教程知识库RAGatouille索引管理如何高效构建、更新和查询文档库RAGatouille索引管理如何高效构建、更新和查询文档库 RAGatouille是一款基于ColBERT技术的高效检索工具能够帮助用户轻松构建、更新和查人工智能RAGNLP深度学习上一篇RT-Thread 在 LM401-LoRaWAN 开发板上的 BSP 快速上手与进阶配置指南STM32WLE5CB下一篇Desloppify快速上手5分钟安装并运行你的第一次代码库健康扫描创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考