8. 2026金三银四 Android别再说你会用 RecyclerView了!20道面试题测测你的真实水平

18 阅读13分钟

Q1:RecyclerView 的四级缓存机制是什么?如何复用 ViewHolder?

答案核心

  • 四级缓存(从上到下查找)
    1. mAttachedScrap:屏幕内可见 ViewHolder,未分离,用于布局传递(不重建)。
    2. mCachedViews:刚滑出屏幕的 ViewHolder,默认容量 2,数据完全干净,无需重新绑定
    3. mViewCacheExtension:开发者自定义缓存(极少使用)。
    4. mRecyclerPool:多列表共享缓存,按 itemViewType 分区,默认每区 5 个。从池中取出的 ViewHolder 会清理数据,必须重新绑定。
  • 复用流程:LayoutManager 请求 View → 先查 scrap → 再查 cache → 再查自定义 → 最后从 pool 获取或创建。

流程图

graph TD
A[LayoutManager 请求 position] --> B{mAttachedScrap?}
B -->|是| C[直接返回 无需bind]
B -->|否| D{mCachedViews?}
D -->|是| C
D -->|否| E{RecycledViewPool?}
E -->|是| F[取ViewHolder并reset]
F --> G[调用 onBindViewHolder]
E -->|否| H[createViewHolder + bind]

精简源码

// Recycler 核心复用逻辑简化
ViewHolder tryGetViewHolderForPosition(...) {
    // 1. scrap
    holder = getScrapOrHiddenOrCachedHolderForPosition(position);
    if (holder != null) return holder;
    // 2. cache
    holder = getScrapOrCachedViewForId(...);
    if (holder != null) return holder;
    // 3. pool
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal(); // 清空数据
        return holder;
    }
    // 4. 创建新
    holder = mAdapter.createViewHolder(parent, type);
    return holder;
}

与后续 Q8(RecycledViewPool 共享)的「异」:Q1 讲四层完整结构,Q8 专门深化 pool 的跨列表复用。


Q2:RecyclerView 局部刷新原理?Payload 有什么用?

答案核心

  • 局部刷新原理:调用 notifyItemChanged(position, payload) 后,RecyclerView 标记该 Item 为“脏”,布局时仅刷新该 Item,不重建其他 Item。
  • Payload 作用精准更新部分 UI,避免全量绑定。
    • 无 payload:执行完整的 onBindViewHolder(holder, position)
    • 有 payload:只调用 onBindViewHolder(holder, position, payloads),根据 payload 仅更新指定控件(如只改点赞数字)。

流程图

graph TD
A[notifyItemChanged(pos, payload)] --> B[标记Item脏]
B --> C[布局阶段]
C --> D[回调 onBindViewHolder(holder, pos, payloads)]
D --> E{payloads 非空?}
E -->|是| F[只更新指定UI]
E -->|否| G[全量刷新Item]

精简源码

// Adapter 中实现带 payload 的绑定
@Override
public void onBindViewHolder(Holder holder, int position, List<Object> payloads) {
    if (!payloads.isEmpty()) {
        String payload = (String) payloads.get(0);
        if ("UPDATE_LIKE".equals(payload)) {
            holder.tvLike.setText(data.get(position).likeCount + "");
        }
    } else {
        onBindViewHolder(holder, position); // 全量刷新
    }
}
// 调用处
adapter.notifyItemChanged(position, "UPDATE_LIKE");

与 Q1 的「异」:Q1 解决“创建/复用”问题,Q2 解决“更新数据时如何高效刷新”。


Q3:DiffUtil 原理及使用场景?如何自动计算差异并局部刷新?

答案核心

  • 原理:基于 Myers 差分算法(O(N+M)),比较新旧两个数据集,生成最小编辑操作(增、删、改、移)。
  • 核心方法
    • areItemsTheSame:判断是否是同一个 item(通常用 id)。
    • areContentsTheSame:判断内容是否变化(仅在 items same 时调用)。
  • 场景:替代手动 notifyItemXXX,尤其适合数据整体替换(如网络请求刷新列表),自动完成精准局部刷新。

流程图

graph TD
A[旧列表 + 新列表] --> B[DiffUtil.calculateDiff]
B --> C[areItemsTheSame?]
C -->|否| D[标记 REMOVE / INSERT]
C -->|是| E[areContentsTheSame?]
E -->|否| F[标记 CHANGE]
E -->|是| G[无变化]
D & F --> H[DiffResult]
H --> I[diffResult.dispatchUpdatesTo(adapter)]
I --> J[自动调用 notifyItemXXX]

精简源码

DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
    @Override public boolean areItemsTheSame(int oldPos, int newPos) {
        return oldList.get(oldPos).id == newList.get(newPos).id;
    }
    @Override public boolean areContentsTheSame(int oldPos, int newPos) {
        return oldList.get(oldPos).equals(newList.get(newPos));
    }
});
result.dispatchUpdatesTo(adapter);
adapter.setList(newList); // 更新内部数据

与 Q2 的「异」:Q2 是手动局部刷新,Q3 是自动化差异计算,适合大批量数据替换。


Q4:RecyclerView 卡顿如何优化?(综合方案)

答案核心

  • 缓存优化:增大 mCachedViews 容量(setItemViewCacheSize(20)),共享 RecycledViewPool
  • 刷新优化:禁用 notifyDataSetChanged,使用局部刷新、Payload、DiffUtil。
  • 布局优化:Item 布局层级 ≤5,固定尺寸用 setHasFixedSize(true),关闭动画 setItemAnimator(null)
  • 异步优化:图片用 Glide 缩略图 + 滑动暂停加载;复杂数据处理放子线程。
  • 预加载setItemPrefetchEnabled(true)(默认开启),自定义 LayoutManager 时实现 collectAdjacentPrefetchPositions

流程图

graph TD
A[卡顿优化方向] --> B[缓存]
A --> C[刷新]
A --> D[布局]
A --> E[异步]
A --> F[预加载]
B --> B1[增大 Cache 容量]
B --> B2[共享 Pool]
C --> C1[DiffUtil + Payload]
D --> D1[减少层级 + 固定尺寸]
E --> E1[Glide 滑动暂停]
F --> F1[setItemPrefetchEnabled]

精简源码

// 综合优化示例
recyclerView.setItemViewCacheSize(20);
recyclerView.setRecycledViewPool(sharedPool);
recyclerView.setHasFixedSize(true);
recyclerView.setItemAnimator(null);
((LinearLayoutManager) recyclerView.getLayoutManager()).setItemPrefetchEnabled(true);

与后续 Q16(卡顿监控)、Q18(首屏速度)、Q19(滑动流畅)的「异」:Q4 是优化措施的全景图,后面三个是更细的“监控”、“首屏”、“丝滑”专项。


Q5:如何自定义 LayoutManager?必须实现哪些核心方法?

答案核心

  • 必须实现的方法
    • generateLayoutParams():返回自定义 LayoutParams。
    • onLayoutChildren()核心,布局所有子 View(通常先 detach + scrap,再 fill)。
    • canScrollHorizontally() / canScrollVertically():支持滚动方向。
    • scrollHorizontallyBy() / scrollVerticallyBy():处理滚动偏移,回收/添加 View。
  • 关键流程detachAndScrapAttachedViews(recycler) → 根据锚点布局可见范围 → fill() 填充剩余空间。

流程图(直线型 LayoutManager)

graph TD
A[onLayoutChildren] --> B[detachAndScrapAttachedViews]
B --> C[获取锚点position/offset]
C --> D[循环填充]
D --> E[调用 recycler.getViewForPosition]
E --> F[addView + measure + layout]
F --> G[offset += childHeight]
G --> D[直至填满屏幕]

精简源码

public class MyLinearLayoutManager extends RecyclerView.LayoutManager {
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachAndScrapAttachedViews(recycler);
        int offsetY = 0;
        for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            layoutDecorated(view, 0, offsetY, getDecoratedMeasuredWidth(view),
                            offsetY + getDecoratedMeasuredHeight(view));
            offsetY += getDecoratedMeasuredHeight(view);
        }
    }
    @Override public boolean canScrollVertically() { return true; }
    @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                             ViewGroup.LayoutParams.WRAP_CONTENT);
    }
}

与 Q13(预布局 pre-layout)的「异」:Q5 是基础实现,Q13 深入讲解支持动画时必须处理的 pre-layout 阶段。


Q6:预取(Prefetch)机制如何提升滑动流畅度?

答案核心

  • 定义:在惯性滑动(Fling)期间,RecyclerView 提前创建并缓存未来可能进入屏幕的 ViewHolder。
  • 实现GapWorker 每一帧空闲时调用 LayoutManager.collectAdjacentPrefetchPositions() 获取预取位置列表,异步创建 ViewHolder 并放入 mCachedViews
  • 对比上拉加载更多:Prefetch 是系统级 ViewHolder 预创建(缓存层),上拉加载是业务级数据预加载(数据层)。
  • 优化:自定义 LayoutManager 需实现 collectAdjacentPrefetchPositions;嵌套 RecyclerView 用 setInitialPrefetchItemCount(4)

流程图

graph TD
A[惯性滑动] --> B[GapWorker 调度]
B --> C[collectAdjacentPrefetchPositions]
C --> D[返回未来 N 个 position]
D --> E[Recycler 异步创建 ViewHolder]
E --> F[放入 mCachedViews]
F --> G[滚动到时直接从缓存取]

精简源码

// 自定义 LayoutManager 参与预取
@Override
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
                                              LayoutPrefetchRegistry registry) {
    int delta = (dy > 0) ? 1 : -1;
    int firstPos = findFirstVisibleItemPosition();
    for (int i = 1; i <= 3; i++) {
        int pos = firstPos + delta * i;
        if (pos >= 0 && pos < getItemCount()) registry.addPosition(pos, 0);
    }
}

与 Q4(综合优化)的「异」:Q4 提到预加载但未展开,Q6 专门深度解析 Prefetch 原理。


Q7:嵌套滑动机制(NestedScrolling)原理?如何解决 RecyclerView 嵌套冲突?

答案核心

  • 原理:子 View(RecyclerView 已实现 NestedScrollingChild)滑动前先询问父 View(NestedScrollingParent),父子协同分配滚动距离。
    • startNestedScroll() → 父响应。
    • dispatchNestedPreScroll() → 父先消耗。
    • 子消耗剩余 → dispatchNestedScroll() 传回未消耗部分。
  • 解决冲突方案
    1. 简单粗暴:recyclerView.setNestedScrollingEnabled(false)(子不再滚动,完全交给父)。
    2. 自定义父容器实现 NestedScrollingParent2 接口,精细控制。
    3. 使用 CoordinatorLayout + Behavior(如 AppBarLayout 折叠)。

流程图

sequenceDiagram
    participant Child as RecyclerView
    participant Parent as 父布局
    Child->>Child: 手指滑动 dy
    Child->>Parent: startNestedScroll()
    Parent-->>Child: true
    Child->>Parent: dispatchNestedPreScroll(dy)
    Parent->>Parent: 先消耗部分(如折叠栏)
    Parent-->>Child: consumed[1]=消耗值
    Child->>Child: 滚动剩余距离
    Child->>Parent: dispatchNestedScroll(未消耗)
    Parent-->>Child: 可能处理边缘效果
    Child->>Parent: stopNestedScroll()

精简源码(父容器简化实现)

public class CustomParent extends FrameLayout implements NestedScrollingParent2 {
    private NestedScrollingParentHelper helper = new NestedScrollingParentHelper(this);
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        return (axes & View.SCROLL_AXIS_VERTICAL) != 0;
    }
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        int consumedY = Math.min(dy, mTopView.getHeight()); // 让顶部 View 先折叠
        mTopView.offsetTopAndBottom(-consumedY);
        consumed[1] = consumedY;
    }
    // 其他方法省略...
}

与 Q5(自定义 LayoutManager)的「异」:Q5 侧重布局管理,Q7 侧重事件分发与滑动协作。


Q8:RecycledViewPool 如何实现跨列表共享?与 mCachedViews 有何区别?

答案核心

  • 作用:存放超出 mCachedViews 容量的 ViewHolder,按 itemViewType 分区存储,默认每区 5 个。
  • 跨列表共享:多个 RecyclerView 调用 setRecycledViewPool(pool) 共用同一池子,减少 onCreateViewHolder 调用。
  • 与 mCachedViews 区别
    • mCachedViews:最多 2 个,ViewHolder 不清空数据,复用无需 re-bind。
    • RecycledViewPool:容量更大,ViewHolder 会清理数据(调用 resetInternal()),复用必须重新绑定。

流程图

graph TD
A[RecyclerView1] -->|setRecycledViewPool| P[共用 RecycledViewPool]
B[RecyclerView2] -->|setRecycledViewPool| P
C[RecyclerView3] -->|setRecycledViewPool| P
P --> D[按 type 分区]
D --> E[type0 池: 最大10]
D --> F[type1 池: 最大10]

精简源码

RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();
recyclerViewA.setRecycledViewPool(sharedPool);
recyclerViewB.setRecycledViewPool(sharedPool);
sharedPool.setMaxRecycledViews(0, 20); // type 0 最多缓存20个

与 Q1 的「异」:Q1 讲四级缓存整体结构,Q8 深入最后一级 pool 的共享特性。


Q9:如何高效实现 ItemDecoration?画分割线时注意什么?

答案核心

  • 核心方法
    • getItemOffsets(outRect, ...):为每个 item 预留绘制空间。
    • onDraw(Canvas, ...):绘制在 item 下层(背景分割线)。
    • onDrawOver(...):绘制在 item 上层(悬浮效果)。
  • 性能注意
    • 不在 getItemOffsets 中创建对象(复用一个 Rect)。
    • 避免在 onDraw 中频繁分配 Paint/Drawable,预先初始化。
    • 只绘制可见范围内的 item(遍历 parent.getChildCount())。

流程图

graph TD
A[测量/布局阶段] --> B[getItemOffsets 预留空间]
B --> C[子 View 布局时获得偏移]
D[绘制阶段] --> E[onDraw 遍历可见子 View]
E --> F[根据 position 决定是否画分割线]
F --> G[drawRect / draw(drawable)]

精简源码

public class SimpleDivider extends RecyclerView.ItemDecoration {
    private final Paint paint = new Paint();
    private int dividerHeight = 1;
    public SimpleDivider(int color) { paint.setColor(color); }
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (parent.getChildAdapterPosition(view) != parent.getAdapter().getItemCount() - 1) {
            outRect.bottom = dividerHeight;
        }
    }
    @Override
    public void onDraw(@NonNull Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        for (int i = 0; i < parent.getChildCount() - 1; i++) {
            View child = parent.getChildAt(i);
            canvas.drawRect(child.getLeft(), child.getBottom(), child.getRight(), 
                            child.getBottom() + dividerHeight, paint);
        }
    }
}

与 Q4(卡顿优化)的「异」:Q9 是装饰绘制优化,属于视觉细节,而非滑动性能。


Q10:SnapHelper 如何实现滑动后自动对齐?(如 ViewPager 效果)

答案核心

  • 作用:让 RecyclerView 滑动停止后自动对齐到某个 item 的特定位置。
  • 官方实现LinearSnapHelper(居中)、PagerSnapHelper(一次一页,类似 ViewPager)。
  • 工作原理
    • 重写 onFling() 限制滑动速度和距离。
    • findSnapView() 找到目标 View。
    • calculateDistanceToFinalSnap() 计算偏移量。
    • smoothScrollBy() 完成对齐。

流程图

graph TD
A[用户滑动] --> B[onFling 拦截]
B --> C[计算速度/距离]
C --> D[findSnapView 目标]
D --> E[calculateDistanceToFinalSnap]
E --> F[smoothScrollBy 平滑对齐]

精简源码

// 一行代码实现 ViewPager 效果
new PagerSnapHelper().attachToRecyclerView(recyclerView);

// 自定义左对齐 SnapHelper
public class StartSnapHelper extends LinearSnapHelper {
    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull LayoutManager lm, @NonNull View target) {
        int[] out = new int[2];
        out[0] = target.getLeft(); // 左边缘对齐
        out[1] = 0;
        return out;
    }
}

与 Q7(嵌套滑动)的「异」:Q10 是交互体验优化,不涉及滚动冲突。


Q11:MergeAdapter 是什么?如何简化多类型列表开发?

答案核心

  • 定义:AndroidX 1.2.0+ 提供的合并多个 Adapter 的工具,将多个独立 Adapter 按顺序串联成一个。
  • 优势
    • 解耦:每个业务模块有自己的 Adapter,不再需要一个 Adapter 内通过 getItemViewType 写大量 if-else。
    • 复用:相同类型的 Adapter 可在不同页面重复使用。
  • 场景:Header + 列表 + Footer,或多个不同数据源拼接。

流程图

graph TD
A[页面包含头部/商品列表/推荐/底部] --> B[传统: 单一Adapter]
A --> C[MergeAdapter]
C --> D[HeaderAdapter]
C --> E[ProductAdapter]
C --> F[RecommendAdapter]
C --> G[FooterAdapter]
D & E & F & G --> H[MergeAdapter.concat]

精简源码

Adapter header = new HeaderAdapter();
Adapter products = new ProductAdapter(productList);
Adapter footer = new FooterAdapter();
MergeAdapter mergeAdapter = new MergeAdapter(header, products, footer);
recyclerView.setAdapter(mergeAdapter);
// 单独更新产品部分
products.notifyItemChanged(0);

与 Q3(DiffUtil)的「异」:Q11 是结构解耦,Q3 是数据更新优化,两者可结合使用。


Q12:StaggeredGridLayoutManager(瀑布流)有哪些常见坑?如何解决?

答案核心

  1. Item 跳动/位置错乱 → 禁用间隙处理:setGapStrategy(GAP_HANDLING_NONE)
  2. 滑动到顶部回滑出现空白 → 调用 invalidateSpanAssignments() 强制重算布局。
  3. DiffUtil + 瀑布流导致跳跃 → 刷新后手动 requestLayout()
  4. 图片高度变化导致布局抖动 → 预先设置 ImageView 固定宽高比(如 android:scaleType="centerCrop" + Glide.override)。

流程图

graph TD
A[瀑布流坑] --> B[Item跳跃]
A --> C[回滑空白]
A --> D[DiffUtil跳跃]
B --> E[setGapStrategy(NONE)]
C --> F[invalidateSpanAssignments]
D --> G[刷新后 requestLayout]

精简源码

// 禁用间隙
StaggeredGridLayoutManager lm = new StaggeredGridLayoutManager(2, VERTICAL);
lm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
recyclerView.setLayoutManager(lm);
// DiffUtil 后强制刷新布局
diffResult.dispatchUpdatesTo(adapter);
recyclerView.post(() -> lm.invalidateSpanAssignments());

与 Q5(自定义 LayoutManager)的「异」:Q12 是系统自带 LayoutManager 的实战陷阱,面试常问“你遇到过什么问题”。


Q13:自定义 LayoutManager 时如何处理预布局(Pre-layout)?为什么需要两次布局?

答案核心

  • 预布局定义:当 supportsPredictiveItemAnimations() == true 且数据变化时,onLayoutChildren 会被调用两次:Pre-layout 和 Real-layout。
  • 目的:Pre-layout 按旧数据布局,用于记录动画起始位置(如删除 item2 时,item5 应先出现在预布局中,以便执行平滑动画)。
  • 处理方法:通过 state.isPreLayout() 区分两次布局,分别执行不同逻辑。

流程图

graph TD
A[notifyItemRemoved] --> B[supportsAnimations?]
B -->|true| C[Pre-layout: 旧数据布局]
C --> D[记录动画起始位置]
D --> E[执行删除动画]
E --> F[Real-layout: 新数据布局]
B -->|false| F

精简源码

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (state.isPreLayout()) {
        layoutForPreLayout(recycler, state);  // 按旧数据
    } else {
        detachAndScrapAttachedViews(recycler);
        layoutForRealLayout(recycler, state); // 按新数据
    }
}
@Override
public boolean supportsPredictiveItemAnimations() {
    return true; // 告诉RecyclerView需要预布局
}

与 Q5(自定义 LayoutManager)的「异」:Q5 讲基础实现,Q13 深入支持动画的细节,是资深加分点。


Q14:ViewPager2 与 RecyclerView 如何配合实现 Banner + 列表联动?

答案核心

  • 场景:顶部 ViewPager2 Banner,下方列表,上滑列表时 Banner 渐变消失。
  • 推荐方案CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout
    • ViewPager2 放在 AppBarLayout 内,RecyclerView 设置 app:layout_behavior="@string/appbar_scrolling_view_behavior"
  • 嵌套滑动冲突:ViewPager2 内部也是 RecyclerView,若内部有横向滑动,需调用 setNestedScrollingEnabled(false) 避免冲突。
  • 缓存池共享:ViewPager2 中的多个 Fragment 内的 RecyclerView 可共享 RecycledViewPool

流程图

graph TD
A[CoordinatorLayout] --> B[AppBarLayout]
B --> C[CollapsingToolbarLayout]
C --> D[ViewPager2 Banner]
A --> E[RecyclerView]
E -->|behavior=appbar_scrolling| B
B -->|滑动| F[Banner 折叠/渐变]

精简源码

<androidx.coordinatorlayout.widget.CoordinatorLayout>
    <com.google.android.material.appbar.AppBarLayout>
        <com.google.android.material.appbar.CollapsingToolbarLayout>
            <androidx.viewpager2.widget.ViewPager2 android:id="@+id/banner"/>
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <androidx.recyclerview.widget.RecyclerView
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

与 Q7(嵌套滑动)和 Q8(缓存池共享)的「异」:Q14 是两者的综合应用场景,实际项目中非常常见。


Q15:ItemAnimator 动画原理?如何自定义?

答案核心

  • 原理:数据变化时,RecyclerView 记录每个 ViewHolder 动画前位置 → 重新布局记录动画后位置 → ItemAnimator 根据位置差执行动效。
  • 默认动画DefaultItemAnimator(淡入淡出、移动、删除)。
  • 问题:动画可能导致闪烁,可关闭 setItemAnimator(null) 快速排查。
  • 自定义:继承 SimpleItemAnimator,重写 animateAdd/Remove/Move/Change

流程图

graph TD
A[notifyItemInserted] --> B[记录动画前位置]
B --> C[重新布局 记录后位置]
C --> D[调用 animateAdd]
D --> E[执行动画(平移/透明度)]
E --> F[动画结束 dispatchAnimationFinished]

精简源码

// 自定义删除动画:淡出
public class FadeItemAnimator extends DefaultItemAnimator {
    @Override
    public boolean animateRemove(ViewHolder holder) {
        holder.itemView.animate().alpha(0f).setDuration(200)
            .withEndAction(() -> dispatchRemoveFinished(holder)).start();
        return true;
    }
}
recyclerView.setItemAnimator(new FadeItemAnimator());

与 Q2(局部刷新)的「异」:Q2 关注数据更新方式,Q15 关注更新时的视觉效果。


Q16:RecyclerView 卡顿如何监控和定位?有哪些工具?

答案核心

  • 系统工具
    • Profile GPU Rendering:查看是否掉帧(超过 16ms 绿线)。
    • Systrace / Perfetto:抓取 UI 线程,定位 onBindViewHolderonLayoutChildren 耗时。
    • Layout Inspector:检查过度绘制、View 层级。
  • 代码埋点
    • 自定义 OnScrollListener,记录帧间隔时间。
    • Choreographer.FrameCallback 计算掉帧数。
  • 线上监控BlockCanaryMatrix 等卡顿检测库。

流程图

graph TD
A[滑动卡顿] --> B[打开 GPU 渲染条]
B --> C[红柱多?]
C -->|是| D[抓取 Systrace]
D --> E[查看 UIThread 长任务]
E --> F{定位到 onBind 或 layout 耗时}
F --> G[针对性优化]

精简源码(Choreographer 掉帧检测)

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long last = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (last != 0) {
            long diffMs = (frameTimeNanos - last) / 1_000_000;
            if (diffMs > 16.6) Log.w("FPS", "掉帧 " + (diffMs / 16.6));
        }
        last = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

与 Q4(卡顿优化)的「异」:Q4 讲优化手段,Q16 讲定位手段,先监控后优化。


Q17:RecyclerView 闪烁问题有哪些原因?如何排查?

答案核心

  • 常见原因
    1. 动画冲突 → 关闭 setItemAnimator(null) 验证。
    2. 图片异步加载错位 → 未用 Glide 等自动处理生命周期。
    3. 局部刷新调用不当 → notifyItemRangeChanged 触发了不必要动画。
    4. DiffUtil 误判 areContentsTheSame 返回 false 导致重建 View。
  • 排查步骤
    1. 关闭动画 → 若不闪,动画问题。
    2. 检查图片加载回调是否用 setTag 或 Glide。
    3. 打印 onBindViewHolder 调用次数。

流程图

graph TD
A[Item 闪烁] --> B[setItemAnimator(null)]
B --> C[不闪了?]
C -->|是| D[修改动画器或关闭]
C -->|否| E[检查图片异步加载]
E --> F[改用 Glide 或手动校验position]

精简源码

// 错误用法:异步回调不校验位置
loadImage(url, bitmap -> holder.imageView.setImageBitmap(bitmap)); // 可能设错

// 正确:Glide 保证正确性
Glide.with(context).load(url).into(holder.imageView);

与 Q2(局部刷新)的「异」:Q2 讲如何刷新,Q17 讲刷新引起的异常现象。


Q18:如何提高 RecyclerView 首次加载速度?(首屏速度)

答案核心

  • 减少初始布局计算:提前 setAdapter,或用 setFixedSize(true) 避免重复 measure。
  • 异步预创建 ViewHolder:后台线程创建 ViewHolder 并预热到 RecycledViewPool(需特殊技巧)。
  • 缓存池预热:提前设置 setItemViewCacheSize(20)setRecycledViewPool
  • 布局扁平化:Item 布局层级 ≤5。
  • 分页加载:首屏只加载前 N 条数据,滚动后再加载更多。

流程图

graph TD
A[首屏加载慢] --> B[预热缓存池]
B --> C[增大 mCachedViews]
A --> D[布局优化]
D --> E[减少层级 + 固定尺寸]
A --> F[数据分页]
F --> G[首次只加载前15条]

精简源码(预热缓存池示例)

// 提前创建一些 ViewHolder 放入 pool(需要反射或临时RecyclerView)
// 更简单的方法:增大缓存 + 异步数据加载
recyclerView.setItemViewCacheSize(20);
recyclerView.setRecycledViewPool(sharedPool);
// 数据批量分页
adapter.setList(data.subList(0, Math.min(15, data.size())));

与 Q6(Prefetch)的「异」:Q6 是滑动过程中的预创建,Q18 是首次显示的加速。


Q19:如何让 RecyclerView 滑动更流畅(跟手性)?

答案核心

  • 减少主线程计算onBindViewHolder 不做 IO、排序、数据库查询。
  • 滑动时暂停非必要任务:监听 SCROLL_STATE_DRAGGING 时暂停图片加载、动画、日志。
  • 布局优化:避免 wrap_content 父布局,固定 Item 尺寸用 setHasFixedSize(true)
  • 预缓存setItemPrefetchEnabled(true)(默认开启)。
  • 抑制 GC:滚动中不创建新对象,复用 RectPaint 等。

流程图

graph TD
A[滑动状态] --> B{DRAGGING/FLING?}
B -->|是| C[暂停图片加载]
C --> D[Glide.pauseRequests]
B -->|IDLE| E[恢复加载]

精简源码

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            Glide.with(context).resumeRequests();
        } else {
            Glide.with(context).pauseRequests();
        }
    }
});

与 Q4(卡顿优化)的「异」:Q4 是综合方案,Q19 专注滑动跟手性、动态暂停任务。


Q20:如何实现 RecyclerView 的上拉加载更多?(数据预加载)

答案核心

  • 原理:监听滑动状态,当最后一个可见 Item 距离数据总量小于阈值时,触发加载更多。
  • 防抖:用 isLoading 标志位避免重复请求。
  • 优化:剩余 3 个 Item 时触发;惯性滑动时不立即触发,可 postDelayed 等待滑动停止。
  • 与 Prefetch(Q6)的区别:Prefetch 预创建 ViewHolder,上拉加载预加载数据。

流程图

graph TD
A[滑动 onScrolled] --> B[lastVisible >= total - threshold]
B -->|是| C{isLoading?}
C -->|否| D[加载更多数据]
D --> E[用 DiffUtil 增量刷新]
E --> F[isLoading = false]
C -->|是| G[等待]

精简源码

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    int threshold = 3;
    boolean isLoading = false;
    @Override
    public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
        if (dy <= 0 || isLoading) return;
        LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
        int last = lm.findLastVisibleItemPosition();
        if (last >= lm.getItemCount() - threshold) {
            isLoading = true;
            loadMore(() -> {
                isLoading = false;
                adapter.notifyItemRangeInserted(oldSize, newItems.size());
            });
        }
    }
});

与 Q6(Prefetch)的「异」:Q6 是系统预创建 ViewHolder,Q20 是业务预加载数据。


总结:20题优先级排序速查表

优先级问题编号核心主题
最高Q1四级缓存机制
Q2局部刷新 + Payload
Q3DiffUtil 自动差异
Q4卡顿综合优化方案
Q5自定义 LayoutManager
中高Q6预取 Prefetch
中高Q7嵌套滑动与冲突
中高Q8RecycledViewPool 共享
Q9ItemDecoration
Q10SnapHelper 对齐
Q11MergeAdapter 多类型
Q12瀑布流坑
Q13预布局 Pre-layout
Q14ViewPager2 联动
Q15ItemAnimator 动画
中低Q16卡顿监控工具
中低Q17闪烁问题排查
中低Q18首屏加载速度
中低Q19滑动流畅度
中低Q20上拉加载更多