当列表数据变更时,调用 notifyDataSetChanged() 是最省事的。无需关心变更的细节,一股脑统统刷一遍就完事了。但这样做也是最昂贵的。读完这一篇源码走查就知道为啥它这么昂贵了。

消息队列mq察者模式

Adapter.notifyDat线程撕裂者aSetChanged()将刷新操作委托给AdapterDataObservable

public class RecyclerView {
public abstract static class消息队列面试题及答案 Ada源码资本pter&线程的几种状态lt;VH extends ViewHolder> {
private final AdapterDataObservab消息队列原理le mObservable = new AdapterDataObservable();
public final void notifyDataSe消息队列mqtChanged() {
mObservable.notify面试技巧和注意事项Changed();
}
}
}

AdapterDataObservableRecyclerView的静态内部类,它继承自Observable

public class RecyclerView {
static class Ada消息队列pterDataObservable extends Observable<AdapterDataObserver> {
publ面试自我介绍一分钟ic void notifyChanged() {
// 遍历所有观察者并委托之
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(源码时代i).onChanged();
}
}
...
}
}

O线程是什么意思bserva线程安全ble是一个抽象的可被观察者:

// 被观察者, 泛型表示观察者的类型
public abstra消息队列的使用场景ct class Observable<T> {
// 观察者列表
protected final Arr面试技巧ayList<T> mObs源码编辑器ervers = new ArrayList<T>(性能优化);
// 注册观察者
public void registerO源码分享网b消息队列的作用server(T observer) {
...
synchronized(mObservers) {
...
mObservers.add(observer);
}
}
// 注销观察者
public void unre消息队列javagisterObserver(T observer) {
...
synchronized(mObservers) {
int index = mObservers.indexOf(observer);
...性能优化管家
mObservers.remove(index);
}
}
// 移除所有观察者
public void unregisterAll() {
synchronized(mObservers) {
mObs源码ervers.clear();
}
}
}

Observable持有一组观察者,用泛型表示观察者的类型,且定义了注册和注销观察者的方法。

Adapter 数据的观察者是什么时候被线程和进程的区别是什么注册的?

public class RecyclerView {
// 列表数据变化的面试自我介绍一分钟观察者实例
priv源码编辑器手机版下载ate final Rec消息队列面试题及答案yclerView源码之家DataOb性能优化方法server mObserver = new消息队列的使用场景 RecyclerViewDataObserver源码();
// 为 RecyclerView 设置 Adapter
priv消息队列面试题及答案ate void源码时代 setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
if (面试技巧和注意事项mAdapter != null) {
// 移除之前的观察者
mAdapter.unregisterAdapterDa线程是什么意思taObserver(mObserver);
mA面试常见问题及回答技巧dapter.onDetachedFromRecyclerView(this);
}
...
final Adapter面试技巧和注意事项 oldAdapt面试问题er = mAdapter;
mAdapter = adapter;
if (ad消息队列apter != null) {
// 注册新的观察者
adapter线程安全.registerAdapterDataObserve线程的几种状态r(mObserver);
adapter.onAttachedToRecyclerView(this);
}
...
}
public abstract static class Adapter<VH extends ViewHolder> {
// 注册观察者
public void registerAdapterDataObserver(@N消息队列mqonNull Adapt面试问题erDataObserver observ消息队列mqer) {
mObser性能优化大师vable.registerObserver(observer);
}
}
}

在为 RecyclerView 绑定 Adapter 的时候,一个观察者实例RecyclerViewD消息队列原理ataObserver被注册了:

public class RecyclerView {
pr源码交易网站源码ivate class RecyclerViewDataObserver extends AdapterDataObserver {
RecyclerViewDataObserver()面试自我介绍 {}
@Override
public void onChanged() {源码编辑器编程猫下载
assertNotInLayoutOrScroll线程(null);
mState.mStructureChanged性能优化前端 = true;
pro线程池的七个参数cessDataSetCompletelyChanged(tr性能优化软件ue);// 下节面试技巧分析
if源码编辑器手机版下载 (!mA面试dapte线程和进程的区别是什么rHelper.hasPendingUpdates()) {
requestLayout();
}
}
...
}
}

它继承自一个抽象的观察者AdapterDataObserver

public class RecyclerView {
public abstract static消息队列面试题及答案 class AdapterDataObserver {
public void onChanged() {}
public void onItemRangeChanged(int positionStart, int itemCount) {}
public void on面试自我介绍ItemRangeChanged(int消息队列 position性能优化前端Start, int itemCount, @Nullable Object payload) {
onItemRangeChanged(positionStart, itemCount);
}
pub线程池lic void on消息队列有哪些ItemRangeInserted(int positionStart, int itemCount) {}
p线程安全ublic void onItemRangeRemoved(in面试自我介绍简单大方t pos源码编辑器编程猫下载itionStart,面试问题 int itemCount) {}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {}
}
}

AdapterDataObserve线程池原理r 定义了 6 个更新列表的方法,其中第 1 个是全量更新,后面的 5性能优化是什么意思 个都是局部更新。这一篇着重分析全量更新。

在分析具体更新逻辑之前,可以先做一个面试常见问题及回答技巧总结:

RecyclerView 使用观察者模式刷新自己,刷新即是通知所有的观察者。

观察者被抽象为AdapterDataObserver,它们维护在AdapterDataObservable中。

在为 Recy源码clerView 绑定 Adapter 的同线程是什么意思时,一个数据观察者实例被注册给 Adapter。

将一切都无效化

在真正地刷新列表之前,做了一些准备工作:

publ消息队列原理ic class RecyclerView {
void processDataSetComplet面试自我介绍一分钟elyChanged(boolean dispatchItemsChanged) {
mDispatchItemsChangedEvent |= dispatchItem线程池原理sChanged;
mDataSetHasChangedAfterLayout = true;
// 将当前所有表项无效化
markKnownViewsInvalid();
}
// 将当前所有表项无效化
void markKnownViewsI消息队列中间件nvalid() {面试问题
// 遍历列表所有表项
final int childCount = mChildHelper.getUnfilteredC面试自我介绍3分钟通用hildCount();
for (int i = 0; i < childCount; i++) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfi性能优化软件lteredChildAt(i));
// 列表中每个表项的 Vi面试技巧和注意事项ewHo线程池的七个参数lder 添加 FLAG_UPDATE 和 F消息队列原理LAG_I源码分享网NVALID 标志位
if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FL消息队列有哪些AG_INVALID);
}
}
markItemDecorInsetsDirty();
// 将缓存中表项无效化
mRecycler.消息队列markKnownViewsInvalid();
}
}

RecyclerView 遍历了当前所有已经被加载的表项,并线程安全为其 ViewHolder 添加FLAG_UPDATEFLAG_INVALID标志源码之家位。这些标志位会在即将到来的“布局表项”过程中决定是否要为表项绑定数据。(下一节分析)

除了将当前所有表项都无效化外,还调用性能优化大师mRecycler.markKnownViewsInvalid()

pu消息队列的作用blic class Recycl源码编辑器手机版下载erView {
public fin源码码头al源码之家 class Recyc源码码头ler {
void m源码时代arkKnownViewsInvalid() {
// 遍历所有离屏缓存
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
// 将每个离屏缓存中的 ViewHolder 也添加 FLAG_UPDATE 和 FLAG_INVALID 标志位
if (holder != null) {
holder.addFlags(性能优化日志输出ViewHolder.FLAG_UPDATE | ViewHolder源码时代.FLAG_INVALID);
holder.addChan面试常见问题及回答技巧gePayload(null);
}
}
if (mAdapter == null || !mAdapter.hasStableIds()) {
//性能优化js 将离屏缓存中的 Vi面试自我介绍3分钟通用ewHolder 存入缓存池
recycleAndClearCachedViews();
}
}
}
}

RecyclerView 将所有离屏缓存中的 ViewHolder 也都做了无效化处理。还将它们回收到缓存池。(关于 RecyclerView 多级缓存的详细介绍可以点击RecyclerView 缓存机制 |面试自我介绍简单大方 如何复用表项?)

至此,又可以做一个阶段性总结:

RecyclerView 在真正刷新列表之前,将一切都无效化了。包括当前所有被填充表项及离屏缓存中的 ViewHolder源码编辑器手机版下载 实例。性能优化前端无效化体现在代码上即是为 Vie性能优化日志输出wHolder 添加 FLA源码编辑器编程猫下载G_UPD消息队列mqATE 和 FLAG_线程同步INVALID 标志性能优化软件位。

真正的刷新

回看一下onChange()中刷新列表的具体逻辑:

public class Recyc线程安全lerView {
privat源码编辑器手机版下载e class Rec线程和进程的区别是什么yclerViewDat消息队列kafka和redisaObserver extends Ad源码编辑器编程猫下载apte面试问题rDataObserver {
RecyclerViewDataObserver() {}
@Override
public void onChanged() {
assertNotInLayoutOrScroll(nul性能优化方法l);
mState.mStructureChanged = true;
// 将一切都无效化
proces面试自我介绍sDataSetCompletelyChanged(true)消息队列面试题;
if (!mAdapterHelper.hasPendingUpd面试常见问题及回答技巧ates()) {
// 真正的刷新
req源码uestLayout();
}
}
...
}
}

在将一切都无效化后,调用了View.requestLayout(),即请求重新布局,该请求会不断地向父控件传递,一直传到 DecorView,DecorView 继续将请求传递给 ViewRootImpl,利用 Profiler 查看调用链如下图所示:(关于如何使用 Profiler 走查源码可以点击RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势)

RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?

ViewRootImpl 收到重绘请求后调用scheduleTraversals()来触发一次从根视图开始的重绘。重绘任务被包装成一个 Runnable 交由Choreographer暂存。Choreographer紧接着订阅了下一个垂直同步信号。待下一个信号到来,它就会向主线程消息队列中发送一条消息,当主线程处理到这条消息时,从根视图开始的自顶向下重绘就启动了。(关于这其中的细节分析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?)性能优化是什么意思

View面试自我介绍范文.requestLayout()会为控件添加两个标志位:

public class View {
public void requestLayout面试问题() {
...
// 添加两个标志位
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATE消息队列原理D;
// 向父控件传递重绘请求
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
...
}
}

被添加了PFLAG_FORCE_LAYOUTPFLAG_INVALIDA源码编辑器手机版下载TED标志位的控件,在重绘时会面试问题大全及答案大全触发布局,即onLayout()会被调用:

RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?
果然在 Profiler 的调用链中得到了证实,列表的重新布局意味着重新布局其中的每一个表项,体现在代码上即是LinearLayoutManager.onLay性能优化是什么意思outChildren()。在RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系中有提到过这个方法。

publ消息队列的作用ic cla源码编辑器手机版下载ss LinearLayoutManager {
public void onLayoutChildren(RecyclerView.Rec性能优化管家ycler recycler, Re源码编辑器编程猫下载cyclerView.State state) {
...
// detach 并 s源码网站crap 表性能优化前端
detachAndScrapAttachedViews(recycler);
...
// 填充表项
fill()
}

Recycl消息队列的作用erView 在布局表项之前会先调用detachAndScrapAttache源码分享网dViews(re面试问题大全及答案大全cycler)清空现有表项,然后再填充新表项。

public class RecyclerView {
public abstract static class面试自我介绍3分钟通用 LayoutManag线程是什么意思er {
// 删除现存表项并回收它们
public void d线程安全etachAndScrap线程池Attac消息队列原理hedViews(@N消息队列的使用场景onNull Recycler r面试自我介绍ecycler) {消息队列面试题
// 遍历现存表项并逐个回收它们
final int childCount = g线程安全etChildCount();
for (int i =消息队列 childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
// 回收表项 ViewHolder 实例
private vo消息队列面试题id s面试自我介绍简单大方crapOrRecycleView(Recycler recycler, int index, View view) {
final V源码编辑器iewHolder viewHolder = getChildViewHolderInt(view);
...
// 回收到缓存池
if (viewHolder.isInvalid() &amp源码编辑器手机版下载;& !viewHolder.isRemoved() &&amp线程池; !mRecyclerView源码.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
}
// 回收到 scrap 缓存
else {
detachView面试自我介绍简单大方At(index线程池);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
}
}

所有现存表项被逐个遍历,对应的 ViewHolder 实例被逐个回收。因为在重新布局之前表项都被添加了FLAG_INVALID标志位,只要表项未被移除,它们都会被回收到缓存池 RecyclerViewPool 中。(从 Profiler 调用链中也得到了证实。)

回收现存表项之后,紧接着就调用了fill()填充表项:

public class LinearLayoutManager {
int fill(RecyclerView.Recycler recycler, Layou性能优化tState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
// 计算剩余空间
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillS性能优化jspace;
// 不停的往列表中填充表项,直到没有剩余空间
while ((layou线程同步tState.mInfinite || remainingSpace &gt线程是什么意思; 0) &a源码分享网mp;& layoutState.hasMo面试自我介绍简单大方re(state)) {
// 填充单个表项
layoutChunk(recycler, s线程是什么意思tate, layou消息队列的作用tState, layoutChunkResult);
...
}
}
// 填充单个表项
void layoutC源码之家hunk(RecyclerVi线程ew.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
// 获取下一个被填充的视图
View view = layoutState.next(消息队列中间件recycler);
...
// 填充视图
addView(view);
...
}
}

填充表项是一个 whi面试自我介绍le 循环,每次都调用lay线程撕裂者outState.n消息队列的作用ext()获取下一个该性能优化大师被填充的表项:

public class LinearLayoutManager {
static class Layout性能优化日志输出State {
View next(RecyclerView.Rec性能优化ycler recycler) {
...
// 委托 Recycler 获源码码头取下一个该填充的表项
final View view = recycler.getViewForPosition(mCurrentPosition);
...
return view;
}
}
}
public clas线程撕裂者s RecyclerView {
public final class Recycler {
public View getViewForPosition(int position) {
return面试 getViewFor性能优化工程师Position面试自我介绍一分钟(position, false);
}
}
View getViewForPositio性能优化n(int position, boolean dryRun) {
// 调用链最终传递到 tryGetViewHolderForPositionByDeadline()
return tryGetViewHolderForPositionByDea源码码头dline(position, dryRun, FORE源码之家VER_NS).itemView;
}
}

沿着调用链,就走到了一个复用表项的关键方法tryGetViewHo线程池原理lderForPositionByDeadline(),方法中按优先性能优化工具级尝试着从消息队列中间件不同缓存中获取 ViewHolder 实例。(关于该源码码头方法的详细介绍可以点击RecyclerView 缓存机制 | 如何复用表项?)

就这样刚才被存入缓存池的表项,又在这一个个地被命中了。

拿到 V线程池原理iewHolder 实例后,就得判断是否需要为它绑定数据:

public class RecyclerView {
public final class Recycler {
// 从缓存获取 V源码编辑器编程猫下载iewHolder 实例并绑定数据
ViewHolder tryGetViewHolderForPositionByDeadline(int positi源码码头on, boolean dryRun, long deadlineNs) {
...
if (mState.isPreLayout() &&性能优化前端amp; holder.isBound()) {
...
}
//消息队列中间件 如果 ViewHolder 需要更新或者无效了, 则重新为其绑定数据
else if (源码编辑器编程猫下载!holder.isBound() || holder.needsUpdate() || hol线程der.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(消息队列javaposi面试自我介绍简单大方tion);
/性能优化方法/ 绑定数据
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
}
p源码之家riva性能优化管家te bool线程是什么意思ean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPositio线程n, int position, lo面试自我介绍范文ng deadlineNs) {
...
// 绑定数据
mAdapter.bindViewHolder(hold源码之家er, offsetPosition);
...
}
}
public abstract stat线程池ic class Adapter<VH extends ViewHolder> {
public f消息队列javainal void bindViewHolder(@NonNull VH holder, int position) {
...
// 熟悉的绑定数据回调
onBindViewHolder(holder, position, holder.g面试问题大全及答案大全etUnmodif性能优化工具iedPay线程loads());
...
}
}
public abstract static class ViewHol消息队列的作用der {
// 更新标志位
static final int FLAG_UPDATE = 1 << 1;
// 判断 ViewHolder 是否需要被更新
bo面试自我介绍3分钟通用olean needsUpd消息队列的作用ate()线程池的七个参数 {
return (mFlags & FLAG_UPDATE) != 0;
}
}
}

因为在上一节的“无效化”阶段,ViewHolder 被添加了 FLAG_UPDATE 和 FLAG_INVALID 标志位,所以就满足了!holder.isBound() || holder.ne源码分享网edsUpdate() || ho性能优化管家lder.isInvalid()这个条件,从缓存池命中的 ViewHolder 就得重新绑定数据。

总结

  1. RecyclerView 使用观察者模式刷新自己,刷新即是通知所有的观察者。

  2. 观察者被抽象为Adapter消息队列面试题DataObserver,它们维护在AdapterDataObservable中。

  3. 在为 RecyclerView 绑定 Adapter 的同时,一个数据观察者实例被注册给 Adapter。

  4. RecyclerView 在真正刷新列表之前,将一切都无效化了。包括当前所有被填充表项及离屏缓存中的 ViewHolder 实例。无效化体现在代码上即是为 ViewHolder 添加 FLAG_UPDATE 和 FLAG_INVALID 标志位。

  5. Recycler性能优化View.requestLayout()是驱动列表刷新的源头。调用该方法后,会从根视图自顶向下地进行重绘。RecyclerView 的重绘表线程和进程的区别是什么现为重新布局所有表项。

  6. R消息队列面试题ecyc线程池lerView 重新布局表项是这样进源码行的:先回收现存表项消息队列有哪些到缓存池,再重新填充它们。因为这性能优化方法些表项的 ViewHolder 实例在重绘之前都被“无效化”了,所以即使数据没变也逃不掉重新执行绑定数据的操作。

可见notifyDataSetChanged()有多昂贵!

推荐阅读

RecyclerView 系列文章目录如下:

  1. RecyclerView 缓存机制 | 如何复用表项?

  2. RecyclerView 缓消息队列kafka和redis存机消息队列制 | 回收些什么?

  3. Recycle性能优化方法r消息队列javaView 缓存机制 | 回收到哪去?

  4. RecyclerView缓存机制 | scrap view 的生命周期

  5. 读源码长知识 | 更好的RecyclerView点击监听器

  6. 代理模式应用 |消息队列java 每当为 RecyclerView 新增类型时就很抓狂

  7. 更好的 RecyclerView 表项子控件点击监听器

  8. 更高效地刷新 RecyclerView | DiffUtil二次封装

  9. 换一个思路,超简单的RecyclerView预加载

  10. RecyclerView 动画原理 | 换个姿势看源码(pre-lay源码码头out)

  11. RecyclerV性能优化方法iew 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系

  12. Recy线程和进程的区别是什么clerView 动画原线程池原理理 | 如何存储并应用动画属性值?

  13. RecyclerView 面试题 | 列表滚动时,表项是如何线程和进程的区别是什么被填充或回收的?

  14. RecyclerVie源码时代w 面试题 | 哪些情况下表项会被回收到缓存池?

  15. R性能优化工具ecyclerView 性能优化 | 把加载表项耗时减半 (一)

  16. RecyclerView 性能优化 | 把加载表项耗时减半 (二)

  17. RecyclerView 性能优化 | 把性能优化大师加载表项耗时线程的几种状态减半 (面试常见问题及回答技巧三)

  18. RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势

  19. RecyclerView 的滚动时消息队列有哪些怎么实现的?(二)| Fling

  20. Recycle性能优化rView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?