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

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

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


观察者模式


Adapter.notifyDataSetChanged()将刷新操作委托给AdapterDataObservable


public class RecyclerView {
    public abstract static class Adapter<VH extends ViewHolder> {
        private final AdapterDataObservable mObservable = new AdapterDataObservable();
        public final void notifyDataSetChanged() {
            mObservable.notifyChanged();
        }
    }
}


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


public class RecyclerView {
    static class AdapterDataObservable extends Observable<AdapterDataObserver> {
        public void notifyChanged() {
            // 遍历所有观察者并委托之
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
        ...
    }
}


Observable是一个抽象的可被观察者:


// 被观察者, 泛型表示观察者的类型
public abstract class Observable<T> {
    // 观察者列表
    protected final ArrayList<T> mObservers = new ArrayList<T>();
    // 注册观察者
    public void registerObserver(T observer) {
        ...
        synchronized(mObservers) {
            ...
            mObservers.add(observer);
        }
    }
    // 注销观察者
    public void unregisterObserver(T observer) {
        ...
        synchronized(mObservers) {
            int index = mObservers.indexOf(observer);
            ...
            mObservers.remove(index);
        }
    }
    // 移除所有观察者
    public void unregisterAll() {
        synchronized(mObservers) {
            mObservers.clear();
        }
    }
}


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


Adapter 数据的观察者是什么时候被注册的?


public class RecyclerView {
    // 列表数据变化的观察者实例
    private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
    // 为 RecyclerView 设置 Adapter
    private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            // 移除之前的观察者
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        ...
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            // 注册新的观察者
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        ...
    }
    public abstract static class Adapter<VH extends ViewHolder> {
        // 注册观察者
        public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
            mObservable.registerObserver(observer);
        }
    }
}


在为 RecyclerView 绑定 Adapter 的时候,一个观察者实例RecyclerViewDataObserver被注册了:


public class RecyclerView {
    private class RecyclerViewDataObserver extends AdapterDataObserver {
        RecyclerViewDataObserver() {}
        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            mState.mStructureChanged = true;
            processDataSetCompletelyChanged(true);// 下节分析
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();
            }
        }
        ...
    }
}


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


public class RecyclerView {
    public abstract static class AdapterDataObserver {
        public void onChanged() {}
        public void onItemRangeChanged(int positionStart, int itemCount) {}
        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
            onItemRangeChanged(positionStart, itemCount);
        }
        public void onItemRangeInserted(int positionStart, int itemCount) {}
        public void onItemRangeRemoved(int positionStart, int itemCount) {}
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {}
    }
}


AdapterDataObserver 定义了 6 个更新列表的方法,其中第 1 个是全量更新,后面的 5 个都是局部更新。这一篇着重分析全量更新。


在分析具体更新逻辑之前,可以先做一个总结:


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


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


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


将一切都无效化


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


public class RecyclerView {
    void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
        mDispatchItemsChangedEvent |= dispatchItemsChanged;
        mDataSetHasChangedAfterLayout = true;
        // 将当前所有表项无效化
        markKnownViewsInvalid();
    }
    // 将当前所有表项无效化
    void markKnownViewsInvalid() {
        // 遍历列表所有表项
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
            // 列表中每个表项的 ViewHolder 添加 FLAG_UPDATE 和 FLAG_INVALID 标志位
            if (holder != null && !holder.shouldIgnore()) {
                holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
            }
        }
        markItemDecorInsetsDirty();
        // 将缓存中表项无效化
        mRecycler.markKnownViewsInvalid();
    }
}


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


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


public class RecyclerView {
    public final class Recycler {
        void markKnownViewsInvalid() {
            // 遍历所有离屏缓存
            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.addChangePayload(null);
                }
            }
            if (mAdapter == null || !mAdapter.hasStableIds()) {
                // 将离屏缓存中的 ViewHolder 存入缓存池
                recycleAndClearCachedViews();
            }
        }
    }
}


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


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


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


真正的刷新


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


public class RecyclerView {
    private class RecyclerViewDataObserver extends AdapterDataObserver {
        RecyclerViewDataObserver() {}
        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            mState.mStructureChanged = true;
            // 将一切都无效化
            processDataSetCompletelyChanged(true);
            if (!mAdapterHelper.hasPendingUpdates()) {
                // 真正的刷新
                requestLayout();
            }
        }
        ...
    }
}


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


image.png


ViewRootImpl 收到重绘请求后调用scheduleTraversals()来触发一次从根视图开始的重绘。重绘任务被包装成一个 Runnable 交由Choreographer暂存。


Choreographer紧接着订阅了下一个垂直同步信号。待下一个信号到来,它就会向主线程消息队列中发送一条消息,当主线程处理到这条消息时,从根视图开始的自顶向下重绘就启动了。(关于这其中的细节分析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?


View.requestLayout()会为控件添加两个标志位:


public class View {
    public void requestLayout() {
        ...
        // 添加两个标志位
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        // 向父控件传递重绘请求
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        ...
    }
}


被添加了PFLAG_FORCE_LAYOUTPFLAG_INVALIDATED标志位的控件,在重绘时会触发布局,即onLayout()会被调用:


image.png


果然在 Profiler 的调用链中得到了证实,列表的重新布局意味着重新布局其中的每一个表项,体现在代码上即是LinearLayoutManager.onLayoutChildren()。在RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系中有提到过这个方法。


public class LinearLayoutManager {
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        // detach 并 scrap 表项
        detachAndScrapAttachedViews(recycler);
        ...
        // 填充表项
        fill()
}


RecyclerView 在布局表项之前会先调用detachAndScrapAttachedViews(recycler)清空现有表项,然后再填充新表项。


public class RecyclerView {
    public abstract static class LayoutManager {
        // 删除现存表项并回收它们
        public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            // 遍历现存表项并逐个回收它们
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }
        // 回收表项 ViewHolder 实例
        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            ...
            // 回收到缓存池
            if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } 
            // 回收到 scrap 缓存
            else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }
    }
}


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


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


public class LinearLayoutManager {
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        // 计算剩余空间
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 不停的往列表中填充表项,直到没有剩余空间
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // 填充单个表项
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
        }
    }
    // 填充单个表项
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // 获取下一个被填充的视图
        View view = layoutState.next(recycler);
        ...
        // 填充视图
        addView(view);
        ...
    }
}


填充表项是一个 while 循环,每次都调用layoutState.next()获取下一个该被填充的表项:


public class LinearLayoutManager {
    static class LayoutState {
        View next(RecyclerView.Recycler recycler) {
            ...
            // 委托 Recycler 获取下一个该填充的表项
            final View view = recycler.getViewForPosition(mCurrentPosition);
            ...
            return view;
        }
    }
}
public class RecyclerView {
    public final class Recycler {
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }
     }
     View getViewForPosition(int position, boolean dryRun) {
         // 调用链最终传递到 tryGetViewHolderForPositionByDeadline()
         return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
     }
}


沿着调用链,就走到了一个复用表项的关键方法tryGetViewHolderForPositionByDeadline(),方法中按优先级尝试着从不同缓存中获取 ViewHolder 实例。(关于该方法的详细介绍可以点击RecyclerView 缓存机制 | 如何复用表项?


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


拿到 ViewHolder 实例后,就得判断是否需要为它绑定数据:


public class RecyclerView {
    public final class Recycler {
        // 从缓存获取 ViewHolder 实例并绑定数据
        ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
            ...
            if (mState.isPreLayout() && holder.isBound()) {
                ...
            } 
            // 如果 ViewHolder 需要更新或者无效了, 则重新为其绑定数据
            else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                // 绑定数据
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            ...
        }
        private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, int position, long deadlineNs) {
            ...
            // 绑定数据
            mAdapter.bindViewHolder(holder, offsetPosition);
            ...
        }
    }
    public abstract static class Adapter<VH extends ViewHolder> {
        public final void bindViewHolder(@NonNull VH holder, int position) {
            ...
            // 熟悉的绑定数据回调
            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
            ...
        }
    }
    public abstract static class ViewHolder {
        // 更新标志位
        static final int FLAG_UPDATE = 1 << 1;
        // 判断 ViewHolder 是否需要被更新
        boolean needsUpdate() {
            return (mFlags & FLAG_UPDATE) != 0;
        }
    }
}


因为在上一节的“无效化”阶段,ViewHolder 被添加了 FLAG_UPDATE 和 FLAG_INVALID 标志位,所以就满足了!holder.isBound() || holder.needsUpdate() || holder.isInvalid()这个条件,从缓存池命中的 ViewHolder 就得重新绑定数据。


总结


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


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


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


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


  1. RecyclerView.requestLayout()是驱动列表刷新的源头。调用该方法后,会从根视图自顶向下地进行重绘。RecyclerView 的重绘表现为重新布局所有表项。


  1. RecyclerView 重新布局表项是这样进行的:先回收现存表项到缓存池,再重新填充它们。因为这些表项的 ViewHolder 实例在重绘之前都被“无效化”了,所以即使数据没变也逃不掉重新执行绑定数据的操作。


可见notifyDataSetChanged()有多昂贵!


推荐阅读


RecyclerView 系列文章目录如下:


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


  1. RecyclerView 缓存机制 | 回收些什么?


  1. RecyclerView 缓存机制 | 回收到哪去?


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


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


  1. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂


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


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


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


  1. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)


  1. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系


  1. RecyclerView 动画原理 | 如何存储并应用动画属性值?


  1. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?


  1. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?


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


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


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


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


  1. RecyclerView 的滚动时怎么实现的?(二)| Fling


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


目录
相关文章
|
存储 Kubernetes 调度
在k8S中,共享存储的作用是什么?
在k8S中,共享存储的作用是什么?
|
9月前
|
安全 Java API
实现跨域请求:Spring Boot后端的解决方案
本文介绍了在Spring Boot中处理跨域请求的三种方法:使用`@CrossOrigin`注解、全局配置以及自定义过滤器。每种方法都适用于不同的场景和需求,帮助开发者灵活地解决跨域问题,确保前后端交互顺畅与安全。
1121 0
|
11月前
|
JSON 缓存 Java
优雅至极!Spring Boot 3.3 中 ObjectMapper 的最佳实践
【10月更文挑战第5天】在Spring Boot的开发中,ObjectMapper作为Jackson框架的核心组件,扮演着处理JSON格式数据的核心角色。它不仅能够将Java对象与JSON字符串进行相互转换,还支持复杂的Java类型,如泛型、嵌套对象、集合等。在Spring Boot 3.3中,通过优雅地配置和使用ObjectMapper,我们可以更加高效地处理JSON数据,提升开发效率和代码质量。本文将从ObjectMapper的基本功能、配置方法、最佳实践以及性能优化等方面进行详细探讨。
864 2
|
机器学习/深度学习 数据采集 算法
机器学习入门:scikit-learn库详解与实战
本文是面向初学者的scikit-learn机器学习指南,介绍了机器学习基础知识,包括监督和无监督学习,并详细讲解了如何使用scikit-learn进行数据预处理、线性回归、逻辑回归、K-means聚类等实战操作。文章还涵盖了模型评估与选择,强调实践对于掌握机器学习的重要性。通过本文,读者将学会使用scikit-learn进行基本的机器学习任务。【6月更文挑战第10天】
1343 3
|
Android开发
Android获取当前系统日期和时间的三种方法
Android获取当前系统日期和时间的三种方法
784 4
|
Java
【Java系列】if-else代码优化的八种方案
目录 前言 优化方案一:提前return,去除不必要的else 优化方案二:使用条件三目运算符 优化方案三:使用枚举 优化方案四:合并条件表达式 优化方案五:使用 Optional 优化方案六:表驱动法 优化方案七:优化逻辑结构,让正常流程走主干 优化方案八:策略模式+工厂方法消除if else 前言 代码中如果if-else比较多,阅读起来比较困难,维护起来也比较困难,很容易出bug,接下来,本文将介绍优化if-else代码的八种方案。 优化方案一:
904 0
【Java系列】if-else代码优化的八种方案
|
JavaScript
Ant design vue 样式调整(包含导航栏、a-table表格、分页)
Ant design vue 样式调整(包含导航栏、a-table表格、分页)
1469 1
|
Unix Linux Android开发
时间问题
时间问题
303 0
|
JavaScript 前端开发 网络架构
1024特刊|前端开发:Vue路由传递参数和重定向的使用总结
前端开发过程中,作为前端开发者来说关于vue的使用并不陌生,vue相关常用的知识点也是非常重要的,不管是在实际开发中还是在求职面试中都很重要。在vue使用中,路由相关的知识点是非常重要的,而且在实际开发中也是必用知识点,那么本篇博文就来聊聊vue的路由参数传递和路由重定向相关的知识点。
964 2
1024特刊|前端开发:Vue路由传递参数和重定向的使用总结
|
安全 关系型数据库 芯片
全面认识MOS管,一篇文章就够了
础知识中 MOS 部分迟迟未整理,实际分享的电路中大部分常用电路都用到了MOS管,今天势必要来一篇文章,彻底掌握mos管!
1776 1
全面认识MOS管,一篇文章就够了

热门文章

最新文章