5月28日 01:42

Android中RecyclerView和ListView的区别是什么,为什么推荐使用RecyclerView?

核心区别一览

RecyclerView从Android 5.0(API 21)引入,设计目标就是替代ListView。两者最本质的差异在于架构理念:ListView是一个大而全的控件,把布局、复用、点击都包在自己身上;RecyclerView则把职责拆散,布局交给LayoutManager,复用交给Recycler,动画交给ItemAnimator,装饰交给ItemDecoration——每个环节都可替换。

对比维度ListViewRecyclerView
ViewHolder可选,需手动实现强制,内置在Adapter中
布局方式仅垂直列表Linear/Grid/Staggered,支持水平
缓存层级2级(mActiveViews + mScrapViews)4级(Scrap → CachedViews → Extension → Pool)
局部刷新仅notifyDataSetChanged()notifyItemChanged/Inserted/Removed + DiffUtil
Item动画无内置支持内置ItemAnimator
分割线divider属性ItemDecoration自定义
点击事件setOnItemClickListenerViewHolder中回调
嵌套滚动不支持实现NestedScrollingChild
Header/FooteraddHeaderView/addFooterView无直接API,需多ViewType实现

为什么推荐RecyclerView

强制ViewHolder模式

ListView时代,ViewHolder是一个最佳实践但不是强制要求。忘记写ViewHolder的代码依然能跑,只是滑动时反复调用findViewById导致卡顿。RecyclerView直接把ViewHolder变成Adapter的内部类,你不写就无法编译通过,从根本上杜绝了性能隐患。

java
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { static class ViewHolder extends RecyclerView.ViewHolder { TextView textView; ViewHolder(View view) { super(view); textView = view.findViewById(R.id.text); } } }

四级缓存机制

这是面试最爱追问的细节。ListView只有两级缓存:屏幕内的mActiveViews和屏幕外的mScrapViews,且mScrapViews缓存的是裸View,取出后必须重新bindView。RecyclerView有四级缓存,缓存的是ViewHolder(含View和绑定状态),数据未变时连bindView都省了:

  1. mAttachedScrap:屏幕内缓存。Item短暂移出屏幕(如滑动后弹回)直接从这里取,不需要重新bind,复用速度最快
  2. mCachedViews:屏幕外缓存,默认容量2个。刚滑出屏幕的Item暂存于此,也是不需要重新bind的
  3. ViewCacheExtension:自定义缓存层,开发者可按业务逻辑实现,比如按ViewType做LRU缓存
  4. RecycledViewPool:缓存池,按ViewType分类存储,默认每种ViewType最多5个。从这里取出的ViewHolder需要重新bind,但省去了inflate的开销

关键区别在于:ListView的缓存取出后一定重新bindView,RecyclerView的Scrap和CachedViews层取出后可以跳过bindView,这就是性能优势的核心来源。

灵活的布局管理

RecyclerView通过LayoutManager解耦了布局逻辑,一种Adapter可以配合不同LayoutManager展示不同效果:

java
// 垂直列表 recyclerView.setLayoutManager(new LinearLayoutManager(context)); // 网格 recyclerView.setLayoutManager(new GridLayoutManager(context, 2)); // 瀑布流 recyclerView.setLayoutManager( new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));

ListView只能做垂直列表,想做横向或网格得换控件。

内置动画与局部刷新

RecyclerView默认支持Item增删动画,一行代码启用:

java
recyclerView.setItemAnimator(new DefaultItemAnimator());

更实用的是局部刷新能力。ListView只能notifyDataSetChanged()全量刷新,RecyclerView可以精确到单个Item:

java
adapter.notifyItemInserted(position); // 插入 adapter.notifyItemRemoved(position); // 删除 adapter.notifyItemChanged(position); // 更新

DiffUtil则更进一步,自动计算新旧数据集的差异并派发最小范围的刷新事件,避免不必要的重绘:

java
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new MyDiffCallback(oldList, newList)); result.dispatchUpdatesTo(adapter);

嵌套滚动支持

RecyclerView实现了NestedScrollingChild接口,可以与CoordinatorLayout、AppBarLayout等协同工作,实现折叠Toolbar、吸顶等交互效果。ListView不支持嵌套滚动,放在CoordinatorLayout中会出现滑动冲突。

RecyclerView的优化实践

setHasFixedSize

如果Item高度固定,设置此标记可以避免每次数据变化都重新测量RecyclerView自身尺寸:

java
recyclerView.setHasFixedSize(true);

共享RecycledViewPool

多个RecyclerView使用相同ViewType时,共享缓存池可以减少inflate次数,常见于ViewPager+Tab场景:

java
RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool(); pool.setMaxRecycledViews(TYPE_ITEM, 10); recyclerView1.setRecycledViewPool(pool); recyclerView2.setRecycledViewPool(pool);

预加载

在嵌套滑动的父RecyclerView中,子RecyclerView可以提前预加载Item,减少滑动卡顿:

java
linearLayoutManager.setInitialPrefetchItemCount(4);

DiffUtil替代notifyDataSetChanged

全量刷新是最常见的性能浪费。DiffUtil在后台线程计算差异,主线程只刷新变化的Item,对于列表数据频繁变化的场景性能提升显著。

ListView还有用武之地吗

RecyclerView功能强大但代码量大,一个最简单的列表RecyclerView需要写Adapter、ViewHolder、LayoutManager三部分。如果需求就是展示一个固定数据的小列表,不需要动画、不需要多布局,ListView写起来更快。另外ListView的addHeaderView/addFooterView用起来确实比RecyclerView的多ViewType方案简单。但新项目原则上应全部使用RecyclerView,ListView只在维护老代码时才会遇到。

面试追问方向

  • RecyclerView四级缓存各层的作用和复用条件是什么?重点区分哪些层需要重新bind,哪些不需要
  • ListView两级缓存 vs RecyclerView四级缓存,性能差距到底体现在哪?答案在bindView的调用次数
  • DiffUtil的内部实现原理?基于Eugene W. Myers差分算法,时间复杂度O(N),空间复杂度O(N²),数据量大时建议用AsyncListDiffer
  • RecyclerView嵌套滚动的原理?通过NestedScrollingChild向父View分发滚动事件,配合NestedScrollingParent完成协调
标签:Android