Android ListView 的 Adapter、BaseAdapter 与 RecycleBin 复用机制拆解:从 getView 到 convertView 的完整链路
1. 为什么你的 ListView 越滑越卡从一次真实卡顿说起Android ListView 的 Adapter、BaseAdapter 与 RecycleBin 复用机制是很多中高级开发者面试和实战都绕不开的硬骨头。它决定了列表滑动是否流畅、内存是否稳定、会不会在长列表里突然 OOM。简单说Adapter 是数据与视图之间的翻译官BaseAdapter 是它最常用的抽象底座而 RecycleBin 则是 AbsListView 内部那个默默回收 View 的仓库管理员。适合已经会写getView、但说不清convertView到底从哪来、为什么有时复用失效的开发者。我见过不少项目ListView 只有几十条数据就开始掉帧一查getView里每次都在inflateconvertView永远是 null。表面看是“没写 ViewHolder”根因其实是没理解 RecycleBin 的两级缓存ActiveViews 和 ScrapViews。前者保存布局开始时屏幕上的 View后者保存已经滚出屏幕、等待被复用的 View。只有把这条链路走通你才能解释为什么getView的convertView有时有值、有时为 null以及getItemViewType为什么能决定复用是否安全。这篇会从职责边界讲到可复制代码再给出用日志和 Layout Inspector 验证复用次数的具体步骤。你不需要死记源码只要跟着把 BaseAdapter 骨架、ViewHolder 配置和验证方法跑一遍就能把“复用”从概念变成可观测的行为。2. 前置准备TaoToken 与开发环境怎么配在开始拆 ListView 之前先把模型对话和编码辅助的环境准备好。我平时排查这类源码问题会一边看 AOSP 片段一边让模型帮我解释RecycleBin的方法调用顺序效率比纯翻文档高不少。TaoToken 在这里的角色是提供一个统一的模型调用入口你可以在官网了解它的能力边界再决定用哪种方式接入。官网地址https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content如果你只是想快速问几个getView复用的问题直接用模型对话就行https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你打算长期做 Android 源码阅读和 Agent 辅助编码可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite需要自己管理调用凭证时进控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Key 在 API Keys 页面生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档在这里包含请求格式和参数说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你用 Claude Code 做 Android 工程辅助Anthropic 兼容入口是https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewriteAPI 基础地址统一用https://taotoken.net/api注意上面这些链接只是环境准备真正决定 ListView 性能的还是你自己的 Adapter 实现。模型可以帮你解释源码但不会替你写对getViewTypeCount。本地环境需要 Android Studio 任意较新版本SDK 里带上 Layout Inspector。建一个空 Activity 项目即可不需要额外依赖。下面所有代码都放在一个MainActivity和一个SimpleBaseAdapter里方便你直接复制运行。3. Adapter、BaseAdapter 与 RecycleBin 的职责边界先把三者的关系理清楚不然后面看代码会混。Adapter是接口定义了getCount、getItem、getItemId、getView这些必须实现的方法还定义了getItemViewType、getViewTypeCount、registerDataSetObserver等可选能力。ListAdapter和SpinnerAdapter都继承自Adapter分别给列表和下拉框用。BaseAdapter是抽象类实现了ListAdapter和SpinnerAdapter把getItemViewType默认返回 0、getViewTypeCount默认返回 1、isEmpty默认判断getCount() 0这样你继承它时只需要补上数据相关的方法。RecycleBin 不是 Adapter 的一部分它藏在AbsListView内部ListView 和 GridView 都通过它来缓存 View。它的核心成员有两块mActiveViews保存布局开始时屏幕上的 ViewmScrapViews是一个按 viewType 分组的 ArrayList 数组保存已经滚出屏幕的 View。mCurrentScrap在viewTypeCount 1时直接指向mScrapViews[0]省去一次数组寻址。关键方法调用链是这样的布局开始时如果数据没变layoutChildren调用fillActiveViews把屏幕上的 child 放进mActiveViews然后obtainView里通过getActiveView(position)或getScrapView(position)取复用 View布局结束时scrapActiveViews把mActiveViews里没被用到的 View 移到mScrapViews并调用pruneScrapViews防止缓存无限增长。addScrapView则在 View 滚出屏幕时把它加入 scrap 堆并记录scrappedFromPosition。retrieveFromScrap的查找策略值得单独说它先遍历 scrap 列表找scrappedFromPosition position的 View找到就直接返回找不到就返回列表最后一个列表为空则返回 null。这解释了三种典型场景首次显示时缓存为空convertView为 null向上滚动产生新 item 时从缓存尾部取一个向下滚回已回收的位置时能精确命中之前那个 View。shouldRecycleViewType判断viewType 0才回收ITEM_VIEW_TYPE_IGNORE -1和ITEM_VIEW_TYPE_HEADER_OR_FOOTER -2都不会进 scrap 堆。如果你自定义了不该复用的类型却返回了非负值就会浪费内存甚至 OOM。4. 可复制配置BaseAdapter 骨架与 ViewHolder下面这份SimpleBaseAdapter可以直接放进项目。它包含数据列表、ViewHolder 缓存、getView复用逻辑以及getItemViewType和getViewTypeCount的默认实现。注意getView里只在convertView null时才inflate其余情况直接复用并更新数据。public class SimpleBaseAdapter extends BaseAdapter { private final ListString data; private final LayoutInflater inflater; public SimpleBaseAdapter(Context context, ListString data) { this.data data; this.inflater LayoutInflater.from(context); } Override public int getCount() { return data null ? 0 : data.size(); } Override public Object getItem(int position) { return data.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView inflater.inflate(R.layout.item_simple, parent, false); holder new ViewHolder(); holder.title convertView.findViewById(R.id.tv_title); holder.index convertView.findViewById(R.id.tv_index); convertView.setTag(holder); Log.d(RecycleBin, new view at position position); } else { holder (ViewHolder) convertView.getTag(); Log.d(RecycleBin, reuse view at position position , scrappedFrom position); } holder.title.setText(data.get(position)); holder.index.setText(String.valueOf(position)); return convertView; } static class ViewHolder { TextView title; TextView index; } }对应的item_simple.xml只需要两个 TextView一个显示内容一个显示位置方便你在屏幕上直接看到复用是否把数据更新对了。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding16dp TextView android:idid/tv_index android:layout_width48dp android:layout_heightwrap_content android:textSize16sp / TextView android:idid/tv_title android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:textSize16sp / /LinearLayoutActivity 里初始化 ListView 并设置 Adapter数据给 200 条方便观察滚动时的复用日志。public class MainActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView findViewById(R.id.list_view); ListString data new ArrayList(); for (int i 0; i 200; i) { data.add(Item i); } listView.setAdapter(new SimpleBaseAdapter(this, data)); } }如果你要做分组列表比如联系人按字母分组就需要重写getItemViewType和getViewTypeCount。分组标签返回 0联系人返回 1getViewTypeCount返回 2。这样 RecycleBin 会维护两个 scrap 列表复用不会串类型。Override public int getItemViewType(int position) { return isGroup(position) ? 0 : 1; } Override public int getViewTypeCount() { return 2; }注意getViewTypeCount返回 2 之后getView里必须根据getItemViewType返回的类型去 inflate 对应布局否则会出现类型错乱表现为分组标签里显示了联系人数据。5. 验证请求与成功结果日志与 Layout Inspector 怎么看光写代码不够得能证明复用真的发生了。第一种方式看 Logcat。把上面的代码跑起来过滤 tagRecycleBin你会看到前十几条是new view at position0到new view at positionNN 大约等于屏幕能显示的 item 数加少量缓冲。之后继续向上滑动日志变成reuse view at position...说明convertView不再为 nullRecycleBin 开始工作。如果你看到滑动过程中仍然不断出现new view说明复用没生效。常见原因是getView里每次都在inflate或者getItemViewType返回了超出getViewTypeCount范围的值导致getScrapView里whichScrap mScrapViews.length直接返回 null。第二种方式用 Layout Inspector。在 Android Studio 里运行 App打开 Layout Inspector选中 ListView展开它的 child。你会看到屏幕上只有有限个 View 节点而不是 200 个。滚动列表后重新抓取节点数量基本不变只是内容变了。这直观说明 View 被复用了而不是每次新建。第三种方式在getView里加计数器统计new和reuse的次数滚动到底部后打印比例。正常情况下new的次数应该接近屏幕可见数量reuse次数接近总滚动次数。如果new次数接近数据总量说明 RecycleBin 的 scrap 堆没被用上。private int newCount 0; private int reuseCount 0; // 在 getView 里 if (convertView null) { newCount; } else { reuseCount; } // 滚动结束后 Log.d(RecycleBin, new newCount , reuse reuseCount);实测下来200 条数据在 1080P 屏幕上new大约 12 到 15 次reuse会到 180 次以上。这个比例可以作为你判断复用是否正常的参考。6. 本篇常见错排查convertView 为 null、复用错乱与 OOM第一个高频问题convertView一直为 null。除了没写 ViewHolder更隐蔽的原因是getViewTypeCount返回了 1但getItemViewType返回了 1 或更大值。getScrapView里会判断whichScrap mScrapViews.length不满足就返回 null于是每次都新建。解决方法是让getItemViewType的返回值落在[0, getViewTypeCount()-1]区间内。第二个问题复用后数据错乱比如第 3 条显示第 10 条的内容。这通常是 ViewHolder 没更新全部字段或者getItemViewType和布局不匹配。检查getView里是否对所有需要变化的 TextView 都调用了setText以及分组类型和布局是否一一对应。第三个问题IllegalStateException: The content of the adapter has changed but ListView did not receive a notification。这是数据在非 UI 线程被修改或者修改后没调用notifyDataSetChanged。RecycleBin 依赖 DataSetObserver 来感知数据变化数据变了不通知布局和缓存就会不一致。第四个问题OOM。pruneScrapViews会把超过mActiveViews.length的缓存释放掉但如果你的 Adapter 每次getView都新建 View 且不回收scrap 堆会不断增长。另一个原因是getItemViewType返回了ITEM_VIEW_TYPE_IGNORE之外的非负值但实际布局不该复用导致缓存了不该缓存的 View。检查shouldRecycleViewType的语义不该复用的类型返回 -1。第五个问题HeaderView 或 FooterView 被错误回收。addScrapView里对ITEM_VIEW_TYPE_HEADER_OR_FOOTER会直接removeDetachedView而不加入 scrap 堆。如果你自己给 header 设置了普通 viewType就可能被回收后复用成普通 item导致界面异常。不要手动改 header 的 viewType。提示排查复用问题时先把getItemViewType和getViewTypeCount临时改成默认的 0 和 1确认基础复用正常再逐步加分组逻辑能快速定位是类型系统的问题还是 ViewHolder 的问题。7. 继续深入把复用机制用到自己的控件里理解 RecycleBin 之后你再看AbsListView.obtainView就不会觉得神秘。它先尝试getActiveView再尝试getScrapView都没有才调用mAdapter.getView(position, null, this)。布局结束时scrapActiveViews把没用的 ActiveView 降级到 ScrapViewspruneScrapViews控制缓存上限。这条链路清晰之后你自己写横向列表、瀑布流或者自定义容器时也可以照搬两级缓存加按类型分组的思路。如果你在接入模型辅助阅读 AOSP 时遇到请求格式或鉴权问题可以回到 API Keys 和接入文档核对参数https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite需要长期做 Android 源码分析和 Agent 辅助编码的Coding Plan 更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite只想快速验证某个getView行为或让模型解释retrieveFromScrap的直接用模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteAPI 基础地址https://taotoken.net/api最后留一个可操作的练习把SimpleBaseAdapter的getItemViewType改成根据 position 奇偶返回 0 和 1getViewTypeCount返回 2两个类型用不同背景色。滚动列表观察 Logcat 里new的次数是否变成两组再用 Layout Inspector 确认两种类型的 View 各自被复用。这个练习能帮你把 RecycleBin 按类型分组的机制真正跑通。

相关新闻

xberg Elixir 批量字节提取:extract_batch 对无效 MIME 类型的校验与容错解析

xberg Elixir 批量字节提取:extract_batch 对无效 MIME 类型的校验与容错解析

后端AI 应用NLP 【免费下载链接】xberg Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with …

2026/9/29 2:27:15 阅读更多 →
ccv 中的 SIFT 尺度不变特征变换:ccv_sift API 解析、参数调优与 siftmatch 实战

ccv 中的 SIFT 尺度不变特征变换:ccv_sift API 解析、参数调优与 siftmatch 实战

计算机视觉深度学习 【免费下载链接】ccv C-based/Cached/Core Computer Vision Library, A Modern Computer Vision Library 项目地址: https://gitcode.com/gh_mirrors/cc/ccv 点击查看 免费下载 ccv(C-based/Cached/Core Computer Vision Library&am…

2026/9/29 2:27:15 阅读更多 →
VMware虚拟机安装统信UOS桌面版全攻略:从镜像到排错

VMware虚拟机安装统信UOS桌面版全攻略:从镜像到排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/29 2:27:15 阅读更多 →

最新新闻

Unity iOS手游Deep Link接入指南:URL Scheme与Universal Links实战

Unity iOS手游Deep Link接入指南:URL Scheme与Universal Links实战

Deep Link(深度链接)在手游里是个绕不开的刚需,尤其是做买量发行、KOL 合作、活动拉新的时候——用户从 Safari、微信或者一个推广落地页点开链接,能不能直接从浏览器唤起 App,并且把携带的参数准确交到游戏内部逻辑手…

2026/9/30 4:57:13 阅读更多 →
YOLO手机检测实战:2800张数据集从标注体检到模型部署全链路

YOLO手机检测实战:2800张数据集从标注体检到模型部署全链路

手机检测这个方向,看起来简单,实际做起来坑不少。我前后经手过好几个和手机相关的检测项目,从产线质检到会议室手机使用监测,再到驾驶场景下的手机持有识别,每次都会在数据集这个环节卡上一阵子。这次拿到的是一份2800…

2026/9/30 4:57:13 阅读更多 →
Unity手游iOS Deep Link接入:URL Scheme与Universal Links参数解析全指南

Unity手游iOS Deep Link接入:URL Scheme与Universal Links参数解析全指南

1. 项目背景与整体链路设计做Unity手游客户端的朋友应该都有这个经历:市场投放、短信营销、邮件推送里带着一条链接,用户点开之后,手机上已经装了游戏就直接进游戏,没装就跳去App Store下载。这条链接背后的技术,就是D…

2026/9/30 4:57:13 阅读更多 →
基于YOLO的疼痛检测数据集构建与训练实战

基于YOLO的疼痛检测数据集构建与训练实战

1. 疼痛检测数据集项目整体设计与思路拆解1.1 为什么疼痛检测值得单独做一个数据集疼痛检测这个方向,在医疗健康领域里属于那种“看起来简单、做起来要命”的任务。简单在于,人眼判断一个人是否处于疼痛状态,往往只需要看一眼表情、姿态就能大…

2026/9/30 4:57:13 阅读更多 →
C++编译期类型生成:从模板实例化到类型工厂的实战指南

C++编译期类型生成:从模板实例化到类型工厂的实战指南

我现在跟大家聊一个很多人学了几年 C 都没认真琢磨过的概念——编译期类型生成。说白了就是:在编译阶段,程序还没运行之前,编译器就能帮你"算"出一个以前不存在的新类型,然后用这个类型继续编译后续的代码。第一次意识到…

2026/9/30 4:57:13 阅读更多 →
基于YOLO v3与DIoU的生姜种芽检测与朝向判定实战

基于YOLO v3与DIoU的生姜种芽检测与朝向判定实战

简介:这份PDF文献面向农业机械自动化、计算机视觉方向的研究人员与工程技术人员,聚焦生姜机械化播种中种芽朝向难以保持一致的实际难题,提出一套基于深度学习的快速识别与朝向判定方案。全文以YOLO v3网络为基础,结合Mosaic在线数…

2026/9/30 4:56:12 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 8:16:59 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/29 8:24:48 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →