Android嵌套RecyclerView滑动冲突与性能优化实战
简介这是一份面向 Android 开发者的 RecyclerView 嵌套 RecyclerView 实战案例聚焦多层列表与网格数据的展示需求适合已掌握基础控件、希望进阶复杂列表交互的开发者。内容围绕内外层适配器与布局管理器的配合展开涵盖 ViewHolder 复用、外层线性列表嵌套内层网格、嵌套滚动事件传递、上下拉刷新接入以及 ItemDecoration 分隔效果等实现要点并涉及 NestedScrolling 机制下的滑动协调与性能优化思路。压缩包为 rar 格式共 65 个文件约 260KB包含 10 个 java 源码、30 个 xml 布局与配置、9 个 png 图片资源以及 gradle、properties、jar 等工程依赖和构建文件可直接导入 Android Studio 对照学习。该案例已有 1248 人学习能帮助读者梳理适配器绑定、滚动协调和刷新逻辑并借助完整工程目录积累结构组织与排错经验。对理解多层列表的数据组织与交互衔接具有参考价值。1. 嵌套不是把两个列表摆进同一个布局商详页的楼层列表里横向秒杀条和纵向商品推荐原本各自跑得挺顺合并进同一个 RecyclerView 套 RecyclerView 的结构之后横向条划不动整页还跟着上下窜。第一次遇到这个现场我把事件分发、测量、复用池三条路径全猜了一遍最后定位到外层 RecyclerView 的 onInterceptTouchEvent 抢在内层之前把 ACTION_MOVE 吃掉了。这份案例覆盖方向正交与方向同向两种嵌套形态从布局搭建、滑动冲突到 RecycledViewPool 共享和 ConcatAdapter 取舍逐条拆开。适合正在做楼层化首页、聊天列表或商品详情页的 Android 开发新手能按步骤跑通熟手可以直接跳到第 5 章对照翻车记录。2. 结构选型横向套纵向与纵向套纵向的三种典型组合嵌套 RecyclerView 最容易翻车的地方不是写不出来而是一开始就没想清楚内外层谁负责滚动上下文、谁负责局部列表。结构选错了后面所有的 touch 拦截、wrap_content、复用池优化都只是给错误结构打补丁。这一章先把三种常见组合摆出来讲清楚选型边界再落到具体布局代码。2.1 先分清内外层的职责边界我一般用三个问题判断一个嵌套是否成立一是内层列表的 item 数量有没有上限。横向秒杀条、标签栏、日期选择器这类一眼能看到头的适合放进内层商品流、评论列表这类会随数据无限长的放进内层就是灾难因为内层一旦使用 wrap_content就等于要求 RecyclerView 一次性把所有子项布局出来虚拟化收益直接归零。二是内层的高度或宽度由谁决定。内层尺寸如果依赖外层剩余空间比如“占满屏幕剩下的一半”那这个嵌套就不该存在应该改成外层用 GridLayoutManager 或者拆成两个独立滚动容器。内层能自己决定尺寸才谈得上嵌套。三是内层是否需要独立滚动。横向内层天然独立方向和外层正交用户手势可以很自然地分流纵向内层如果也要独立滚动那内外层就是同一方向上的两级滚动冲突处理复杂度会翻倍这时候优先考虑 ConcatAdapter 拍平而不是硬做嵌套。把这三条过一遍大部分没必要的嵌套就会被拦在设计阶段。2.2 方向正交横向内层放进纵向外层这是最常见的组合外层纵向 RecyclerView 渲染楼层每个楼层 ViewHolder 里放一个横向 RecyclerView。方向不同手势分流本身不复杂真正要盯住的是内层尺寸和复用。// InnerSectionViewHolder.kt class InnerSectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val innerRv itemView.findViewByIdRecyclerView(R.id.rv_inner) init { innerRv.apply { // 横向排列第三个参数 reverseLayout 保持 false layoutManager LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) // 内层高度在 XML 里固定尺寸不随 adapter 变化跳过重复 requestLayout setHasFixedSize(true) // 内层列表频繁做局部刷新关掉默认动画减少闪烁 itemAnimator null // 关闭内层嵌套滚动避免和外层形成两条滚动链路 isNestedScrollingEnabled false } } fun bind(items: ListCardItem, pool: RecyclerView.RecycledViewPool) { if (innerRv.adapter null) { innerRv.adapter CardAdapter() // 同一页面里结构相同的内层列表共用缓存池横向滑动时复用同一批 ViewHolder innerRv.setRecycledViewPool(pool) } (innerRv.adapter as CardAdapter).submit(items) } }这段代码里几个参数值得单独说。setHasFixedSize(true)的含义是“adapter 内容变化不会改变 RecyclerView 自身的尺寸”前提是内层高度在布局里写死比如android:layout_height120dp。如果内层高度写成wrap_content这个开关就不能开否则会出现刷新后高度不更新的问题。isNestedScrollingEnabled false针对的是外层也参与嵌套滚动协议的场景比如外层包了 CoordinatorLayout 或 NestedScrollView。关掉之后内层的横向滑动不会再向外层冒泡手势链路更干净。如果外层就是普通 RecyclerView这一行可以省掉。setRecycledViewPool(pool)是把多个内层列表的 ViewHolder 缓存合并。默认每个 RecycledViewPool 实例每个 viewType 只缓存 5 个 ViewHolder一个页面里有五六个横向楼层时各自维护 5 个缓存很浪费。共享之后需要保证内层的 viewType id 在同一池内唯一具体做法在 4.2 展开。布局文件对应部分!-- item_section_horizontal.xml -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical TextView android:idid/tv_title android:layout_widthmatch_parent android:layout_heightwrap_content android:padding12dp android:textSize16sp / androidx.recyclerview.widget.RecyclerView android:idid/rv_inner android:layout_widthmatch_parent android:layout_height120dp android:clipToPaddingfalse android:paddingStart12dp android:paddingEnd12dp / /LinearLayoutclipToPaddingfalse配合左右 padding让第一个和最后一个卡片能贴到屏幕边缘又不被裁掉这是横向列表比较常用的视觉处理。注意不要在横向 RecyclerView 上同时写android:scrollbarshorizontal和clipToPaddingfalse滚动条会被 padding 区域截断观感很怪。2.3 方向同向纵向内层的高度测量与 wrap_content同向嵌套是坑最集中的地方。外层纵向内层也纵向天然存在两级滚动而且内层的高度测量在 wrap_content 下行为很反直觉。RecyclerView 在wrap_content高度下会把所有子项都测量一遍来确定自身高度虚拟化基本失效。更麻烦的是 GridLayoutManager它在wrap_content高度下只按第一行子项的高度推算整体高度行数一多列表底部就会被截断表现为“只显示了两行”。这是很多人第一次做嵌套时遇到的“黑匣子”现象。常见的处理方式有三种按推荐程度从高到低第一种内层高度写死列表本身限制 item 数超出部分在数据层截断。适合“最多展示 6 个”这类业务规则明确的场景简单、可控、没有测量问题。第二种给内层套一个最大高度限制超出后内层自己滚动。需要自定义 RecyclerView 的 onMeasure把父容器给的高度约束压到一个上限以内。第三种放弃嵌套用 ConcatAdapter 把内层数据拍平到外层。这一条在第 6 章展开。最大高度限制的实现// MaxHeightRecyclerView.kt class MaxHeightRecyclerView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : RecyclerView(context, attrs, defStyleAttr) { // 由外部设置单位 px0 表示不限制 var maxHeightPx: Int 0 override fun onMeasure(widthSpec: Int, heightSpec: Int) { val limitSpec if (maxHeightPx 0) { // AT_MOST 保留子项的测量空间同时把上限压到 maxHeightPx MeasureSpec.makeMeasureSpec(maxHeightPx, MeasureSpec.AT_MOST) } else { heightSpec } super.onMeasure(widthSpec, limitSpec) } }onMeasure里做的是把父容器传下来的heightSpec替换成以maxHeightPx为上界的AT_MOST规格。这样 RecyclerView 内部依然会遍历子项测量但整体高度不会超过上限。maxHeightPx建议在代码里用dp * resources.displayMetrics.density换算后赋值不要直接在 XML 里写像素值。还要提醒一点wrap_content的内层 RecyclerView 如果放在ScrollView里会触发ScrollView的高度测量警告同时产生嵌套滚动冲突。这种组合应该直接换成NestedScrollViewisNestedScrollingEnabled false或者更彻底地改成单层 RecyclerView。3. 滑动冲突与事件分发内外层手势的接管顺序嵌套 RecyclerView 的滑动问题九成出在对onInterceptTouchEvent返回时机的误判。外层 RecyclerView 在滑动距离超过 touchSlop、且自身可以沿该方向滚动时会拦截事件并开始自己的滚动。如果内层列表是同一方向、且用户想滑的是内层就会看到“手指按在内层上滚的却是外层”。3.1 从 dispatchTouchEvent 到 onInterceptTouchEvent 的链路一次触摸事件的分发顺序是 Activity → PhoneWindow → DecorView → 视图树每经过一个 ViewGroup 都会先走dispatchTouchEvent再由它决定是否调用onInterceptTouchEvent拦截给子 View。RecyclerView 作为 ViewGroup在onInterceptTouchEvent里做的主要判断是ACTION_DOWN 时记录初始坐标返回 false让子 View 先有机会处理ACTION_MOVE 时计算纵向、横向位移超过ViewConfiguration.getScaledTouchSlop()且该方向可以滚动时返回 true 并开始滚动一旦开始拦截后续同一手势序列的 MOVE、UP 都不会再下发给子 View。这就是嵌套冲突的根源外层在 MOVE 阶段抢在内层消费之前就吃了事件。要解决就得让内层在合适的时候明确告诉外层“这一段手势别抢”。3.2 同向嵌套内层滑到边界才把接管权还回去标准的做法是内层在 ACTION_DOWN 时调用requestDisallowInterceptTouchEvent(true)让祖先链上的所有 ViewGroup 都不再拦截等内层滑到顶部或底部、用户继续往同方向滑时再把这个标志置回 false让外层接管。// NestedScrollHelper.kt挂到内层 RecyclerView 上 fun RecyclerView.bindNestedScrolling() { addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() { private var lastX 0f private var lastY 0f override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { when (e.actionMasked) { MotionEvent.ACTION_DOWN - { lastX e.x lastY e.y // 按下即接管避免外层在第一次 MOVE 就抢走 rv.parent?.requestDisallowInterceptTouchEvent(true) } MotionEvent.ACTION_MOVE - { val dx e.x - lastX val dy e.y - lastY val lm rv.layoutManager as? LinearLayoutManager if (lm ! null) { val atStart lm.findFirstCompletelyVisibleItemPosition() 0 val atEnd lm.findLastCompletelyVisibleItemPosition() lm.itemCount - 1 // 内层已到边界且还在往边界外滑把接管权交还外层 val release (atStart dy 0) || (atEnd dy 0) rv.parent?.requestDisallowInterceptTouchEvent(!release) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL - { // 手势结束必须复位否则下一次触摸会一直处于禁止拦截状态 rv.parent?.requestDisallowInterceptTouchEvent(false) } } // 返回 false 表示只做标记不消费事件滚动手势仍然交给 RecyclerView 自己处理 return false } }) }这段代码有三处容易写错。第一onInterceptTouchEvent必须返回 false。返回 true 会把这个事件从 RecyclerView 的滚动处理里抽走列表反而滑不动。第二ACTION_UP和ACTION_CANCEL都要复位标志位只处理 UP 会在被上层 View 打断手势时留下脏状态。第三边界判断用findFirstCompletelyVisibleItemPosition而不是findFirstVisibleItemPosition后者在内层只露出半个 item 时依然返回 0会导致边界判断提前触发。requestDisallowInterceptTouchEvent会自动沿父链向上传递不需要手动逐层调用。但要注意如果在ACTION_DOWN之前外层已经因为别的手势开始拦截这个调用对内层当下这次手势就不生效了必须在下一次 DOWN 时重新接管。3.3 嵌套滚动与 CoordinatorLayout 的取舍如果外层是NestedScrollView或者CoordinatorLayoutAppBarLayout事件分发链路会先经过NestedScrollingParent接口再落到onInterceptTouchEvent。这套机制的初衷是让内外层协商滚动偏移量的分配而不是靠拦截来决定谁滚适合“上滑先收起标题栏、再滚动列表”这类联动效果。代价是链路长、断点多。NestedScrollView里放纵向 RecyclerView 时内层的isNestedScrollingEnabled必须置 false否则内层会开始自己消费嵌套滚动外层标题栏收不起来但置 false 之后内层又失去了虚拟化的滚动优势所有 item 都被展开布局。这就是典型的“两条路都有代价”。我一般的判断标准是如果外层需要和 AppBar 联动那就用 CoordinatorLayout 这套协议同时把内层数据量卡死在上限以内如果只是普通页面滚动优先用 ConcatAdapter 拍平彻底绕开嵌套滚动协议。把两套机制混着用最后表现出来的症状是“有时候能滑、有时候滑一半卡住”排查成本极高。4. 性能与复用嵌套场景下的测量、缓存与卡顿定位嵌套 RecyclerView 的性能问题往往不是单点慢而是重复测量、重复创建 ViewHolder、bind 里有重活这几件事叠加。这一章按“先测出来、再改对”的顺序把可量化的排查手段过一遍。4.1 高度包裹带来的重复测量RecyclerView 的测量开销主要在onMeasure阶段。外层如果高度是wrap_content每次 adapter 数据变化都会触发一次 requestLayout进而把整棵视图树的测量重走一遍。嵌套结构里内层的测量会随外层测量一起被调用内层 item 越多这一轮测量越贵。可用的手段有三个外层尺寸固定时开setHasFixedSize(true)告诉 RecyclerView 数据变化不影响自身尺寸跳过不必要的 requestLayout内层高度避免wrap_content改成固定值或最大高度限制item 布局用ConstraintLayout压平层级减少单次测量的遍历节点数。setHasFixedSize(true)有个前提容易被忽略它只对“adapter 变化不改变 RecyclerView 尺寸”成立。如果外层是wrap_content高度item 增删会导致高度变化这时候开这个开关会出现列表刷新后高度不更新、底部留白或者被截断。不确定的情况下宁可不开先测清楚再决定。测量次数可以用ViewTreeObserver.OnPreDrawListener做个临时打点// 临时排查用确认没问题后删掉 recyclerView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { private var count 0 override fun onPreDraw(): Boolean { count if (count % 20 0) { Log.d(MeasureCount, onPreDraw 第 $count 次adapter${recyclerView.adapter?.itemCount}) } return true } })如果短时间内这个计数暴涨说明有地方在持续触发重绘先查是不是在onBindViewHolder里调用了notifyDataSetChanged或者修改了布局参数。4.2 共享 RecycledViewPool 的正确用法RecycledViewPool 是 RecyclerView 的 ViewHolder 回收站。默认每个 RecyclerView 实例各有一个每个 viewType 最多缓存 5 个 ViewHolderDEFAULT_MAX_SCRAP。一个页面上有多个结构相同的内层列表时各自维护一份缓存等于把回收站的容量切成了碎片。共享的做法很简单在创建内层 RecyclerView 时把外层或其他内层的池传进去// 在 Fragment / Activity 里创建一次所有内层共用 private val sharedPool RecyclerView.RecycledViewPool().apply { // 手动抬高单个 viewType 的缓存上限默认是 5 setMaxRecycledViews(ItemType.CARD, 12) setMaxRecycledViews(ItemType.BANNER, 4) } // 内层 ViewHolder 里 innerRv.setRecycledViewPool(sharedPool)共享池生效的前提是 viewType 的定义在整个池内唯一且稳定。如果两个不同的内层 Adapter 都用0作为 viewType共享池就会把 A 类型的 ViewHolder 交给 B 类型使用表现出来是“卡片莫名变成了另一张卡片的样子”或者布局参数错乱。稳妥的做法是在各自 Adapter 里定义常量// CardAdapter.kt object ItemType { const val CARD 1001 const val BANNER 1002 const val DIVIDER 1003 } class CardAdapter : RecyclerView.AdapterRecyclerView.ViewHolder() { override fun getItemViewType(position: Int): Int when (data[position]) { is Data.Card - ItemType.CARD is Data.Banner - ItemType.BANNER else - ItemType.DIVIDER } // onBindViewHolder 里不要复用不同 ItemType 的绑定逻辑 }viewType 建议从 1000 起跳给外层可能用到的类型留出空间也方便在日志里区分内外层。共享池不会影响内层自己的滚动位置也不会因为共享而导致数据串位唯一需要保证的就是 viewType → ViewHolder 的映射在整个池内是一致的。4.3 用 Layout Inspector 与 GPU 渲染定位卡顿嵌套列表卡顿的排查顺序我一般固定成三步第一步看 Layout Inspector 的视图层级。重点看内层 RecyclerView 的实际测量高度和 item 数量是否匹配。如果内层高度明显小于内容总高说明被截断了如果 View 树深度超过 15 层item 布局需要压平。第二步看 GPU 渲染模式下的柱状图。绿色竖条超过 16ms 表示掉帧连续出现说明有稳定耗时。这时候再去对比滚动内层和滚动外层时的柱状图能快速判断是内层 bind 重还是外层布局重。第三步在内层 Adapter 的onBindViewHolder里打点找出超过 4ms 的项override fun onBindViewHolder(holder: ItemHolder, position: Int) { val t0 System.nanoTime() // 实际绑定逻辑 bindData(holder, data[position]) val costMs (System.nanoTime() - t0) / 1_000_000.0 if (costMs 4.0) { // 超过 4ms 视为风险项查图片解码、格式化字符串、复杂计算是否放在 bind 里 Log.w(NestedBind, pos$position cost${%.2f.format(costMs)}ms type${holder.itemViewType}) } }4ms 这个阈值是根据 60fps 反推的一帧 16msChoreographer 回调、measure、layout、draw 各占一部分onBindViewHolder 留给自己的时间通常不超过 4ms。超过这个值滚动时就会有可感知的掉帧。绑定时不要在 bind 里做网络请求、图片原图解码、复杂字符串拼接这些都应该前移到数据准备阶段或者用异步加载。另外提醒一点嵌套结构里内层 Adapter 的notifyDataSetChanged()尽量换成DiffUtil或者notifyItemChanged。notifyDataSetChanged会让 RecyclerView 重新布局所有子项滚动位置也容易丢失表现为“局部刷新之后列表跳回顶部”。5. 避坑与常见问题排查五个高频翻车点5.1 五个高频翻车点的现象与解法第一坑内层只显示一行内容或者高度明显不对。现象纵向内层放进外层只露出第一个 item剩下的被裁掉scrollbar 也不出现。原因GridLayoutManager 在wrap_content高度下只按第一行测量整体高度LinearLayoutManager 在部分版本里也只在第一项测量后就确定尺寸。解决给内层设置固定高度或者用 2.3 的MaxHeightRecyclerView限制最大高度同时检查 item 根布局的高度不要写成match_parent否则单个 item 会撑满内层高度。第二坑横向内层划不动整页跟着上下窜。现象手指按在横向卡片上左右滑页面往上滚卡片纹丝不动。原因外层 RecyclerView 的onInterceptTouchEvent在 MOVE 阶段先于内层返回 true把事件截走。解决用 3.2 的bindNestedScrolling()在内层 DOWN 时调用requestDisallowInterceptTouchEvent(true)。如果外层是 NestedScrollView 或 CoordinatorLayout还要检查内层的isNestedScrollingEnabled是否被意外打开。第三坑内层滑到底部后整页卡死不再往上滚。现象内层列表本身能滑滑到最后一个 item 之后继续拖拽页面没反应。原因内层接管事件后没有在边界处把控制权交还外层requestDisallowInterceptTouchEvent(true)一直生效。解决在 MOVE 阶段判断内层是否已到边界到边界时把标志置回 false。注意用findLastCompletelyVisibleItemPosition() itemCount - 1判断而不是findLastVisibleItemPosition()。第四坑局部刷新后内层跳回顶部。现象内层 Adapter 调用notifyDataSetChanged更新数据列表滚动位置归零。原因notifyDataSetChanged会清空位置记录RecyclerView 无法判断哪些 item 是同一个。解决改用DiffUtil.calculateDiff计算差异后dispatchUpdatesTo如果数据结构简单至少改成notifyItemChanged(position)。长期方案是在 Adapter 里setHasStableIds(true)并实现getItemId。第五坑滚动一段时间后内存飙升严重时 OOM。现象纵向内层列表 item 数量很大比如几百条滚动几屏后内存曲线一路往上。原因纵向内层在wrap_content下会把所有子项都 measure layout 出来ViewHolder 无法回收等同于把整个列表完全展开。外层再嵌套几层内存直接翻倍。解决三条路选一条——数据层截断内层只保留固定条数给内层设最大高度并允许自身滚动改用 ConcatAdapter 把内层数据拍平到外层让整个页面只有一个 RecyclerView 负责回收。5.2 一套可复用的排查顺序碰到嵌套列表的现场问题我按下面这个顺序过一遍基本能定位到层先量高度。打开 Layout Inspector点中内层 RecyclerView看mMeasuredHeight和 item 总数乘以单个 item 高度是否一致。不一致就是测量问题回到 2.3。再看触摸。给外层的onInterceptTouchEvent和内层的onInterceptTouchEvent各加一行日志观察一次滑动中两者的返回值顺序。外层先返回 true就是拦截冲突回到 3.2。然后看 bind。按 4.3 的方法打点定位单次 bind 超过 4ms 的位置优先排查图片加载和字符串处理。最后看内存。用 Android Profiler 观察滚动过程中 ViewHolder 的回收情况如果滚动之后回收站数量持续增长就是测量问题导致的完全展开回到第五坑的解决方案。把这四步固化成一个检查清单比凭直觉猜要快得多。嵌套列表的问题很少只出现在一层通常是测量引起布局膨胀、布局膨胀放大 bind 开销、bind 开销又反过来加重掉帧按顺序切分能避免在错误的方向上改代码。6. 进阶与验证把嵌套列表收敛成一个可复用组件做到这一步嵌套列表基本能跑了。接下来要考虑的是可维护性和上线前的验证。6.1 用 ConcatAdapter 把同向结构拍平如果内层和外层方向一致最彻底的方案是不做嵌套。ConcatAdapter允许把多个 Adapter 顺序拼接到同一个 RecyclerView 上每个 section 依然独立维护数据但最终只挂载在一个 RecyclerView 里复用池天然共享滚动位置统一管理。// HomeFragment.kt val concatAdapter ConcatAdapter( headerAdapter, // 顶部搜索栏 bannerAdapter, // 轮播楼层 sectionOneAdapter, // 商品楼层一 sectionTwoAdapter // 商品楼层二 ) recyclerView.apply { layoutManager LinearLayoutManager(context) adapter concatAdapter setHasFixedSize(false) }要注意的是ConcatAdapter需要较新版本的androidx.recyclerview才有老项目升级前先确认依赖版本。它的局限也很明确不能给每个 section 单独设置 LayoutManager 方向。横向秒杀条这类方向不同的楼层依然只能靠嵌套实现。所以现实的工程做法通常是混合的横向楼层用嵌套纵向楼层用ConcatAdapter拍平页面上只有一个外层 RecyclerView 承担纵向滚动。6.2 一套可落地的验证清单上线前我一般按这张表过一遍每一项都能用工具测出来不靠肉眼判断。验证项方法通过标准内层高度Layout Inspector 查看 measuredHeight等于 item 数 × 单 item 高度或等于预设最大高度滑动接管手动滑动内外层各一轮内层滑到底部后外层能继续滚动反向亦然复用池Profiler 观察 ViewHolder 回收数滚动结束后回收站数量稳定不持续增长bind 耗时4.3 的打点日志95% 的 bind 在 4ms 以内内存Android Profiler 滚动 5 屏内存曲线平稳无明显阶梯式上升局部刷新触发一次数据更新列表不跳回顶部视觉无闪烁这张表里我踩得最深的是“滑动接管”那一项。早前做过一个聊天列表内层是消息列表、外层是会话列表两层的边界处理写反了测试时没注意到上线后用户反馈“滑到一半会突然跳一下”。后来定位到是findFirstCompletelyVisibleItemPosition在内层只露出半个 item 时返回了 0边界判断提前释放了拦截权。从那以后我每次做嵌套列表都会先把这张表过一遍尤其是边界判断那一行宁可多写两行日志也不靠手感验证。希望帮到你。本文还有配套的精品资源点击获取