优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿

RecyclerView 是一个高度自由可定制的列表组件,它的复用性流畅性是很好的,但是不恰当的使用也会造成一个些困扰。我最近在写一个购物车的页面,由于需求摆在那里,即便不想使用嵌套,但是似乎也没有什么良策,于是乎就闷着头做了。

   效果是出来了,但是存在两个问题

  • 内层rv 滑动的时候导致图片加载错乱,甚至某些item直接不显示图片
  • 上下滑动整体页面,发现越来越卡,直至出现系统出现ANR弹窗
    这两个问题困扰了我好多天,首先是第一个,图片错乱甚至是不显示,我起初认为是由于Rv 嵌套Rv 导致内层的rv 数据显示不全,照着这个方向,百度一番,按照网上的说法做了很多尝试:
  1. 内层rv改成被相对布局包裹,rv的高度自适应,并且相对布局屏蔽rv的焦点descendantFocusability,事实证明在,这个是无效的,即便奏效也是在低版本手机上,7.0以上问题依旧存在
    2.修改内外层Rv的Inflate:
    外层
  @Override
    protected DescHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        return new DescHolder(mInflater.inflate(R.layout.layout_shaopcar_item, parent, false));
    }

内层

  @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View inflate = LayoutInflater.from(mContext).inflate(R.layout.layout_recy_item_goods_info_new, null);
        return new ViewHolder(inflate);
    }

enm,这种方法吧,怎么说呢,在我看来就是骚操作,治标不治本,很久以前用过这种,不建议,并且,因为里层的rv在解析布局的时候没有parent和是否依附parent参数的约束,很容易就整体布局歪歪扭扭的,体验非常不好,不多我是发现,把内层的写成:

  @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View inflate = LayoutInflater.from(mContext).inflate(R.layout.layout_recy_item_goods_info_new, null,parent,true);
        return new ViewHolder(inflate);
    }

倒是误打误撞解决了问题,但是随着rv的滑动和复用,很可能会再次出现错乱
3.重写recyclerview 并且重写onMeasure

 @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST);
        super.onMeasure(widthSpec, expandSpec);
    }

感兴趣的朋友可以试试,意义真的不大。
4.就剩下最后一招了,重写布局管理者,在测量的时候改变下,来计算每个item进行显示

public class FullyLinearLayoutManager extends LinearLayoutManager {

    private static final String TAG = FullyLinearLayoutManager.class.getSimpleName();

    public FullyLinearLayoutManager(Context context) {
        super(context);
    }

    public FullyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    private int[] mMeasuredDimension = new int[2];

    @Override
    public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {

        final int widthMode = View.MeasureSpec.getMode(widthSpec);
        final int heightMode = View.MeasureSpec.getMode(heightSpec);
        final int widthSize = View.MeasureSpec.getSize(widthSpec);
        final int heightSize = View.MeasureSpec.getSize(heightSpec);

        Log.i(TAG, "onMeasure called. \nwidthMode " + widthMode + " \nheightMode " + heightSpec + " \nwidthSize "
                + widthSize + " \nheightSize " + heightSize + " \ngetItemCount() " + getItemCount());

        int width = 0;
        int height = 0;
        for (int i = 0; i < getItemCount(); i++) {
            measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension);

            if (getOrientation() == HORIZONTAL) {
                width = width + mMeasuredDimension[0];
                if (i == 0) {
                    height = mMeasuredDimension[1];
                }
            } else {
                height = height + mMeasuredDimension[1];
                if (i == 0) {
                    width = mMeasuredDimension[0];
                }
            }
        }
        switch (widthMode) {
        case View.MeasureSpec.EXACTLY:
            width = widthSize;
        case View.MeasureSpec.AT_MOST:
        case View.MeasureSpec.UNSPECIFIED:
        }

        switch (heightMode) {
        case View.MeasureSpec.EXACTLY:
            height = heightSize;
        case View.MeasureSpec.AT_MOST:
        case View.MeasureSpec.UNSPECIFIED:
        }

        setMeasuredDimension(width, height);
    }

    private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec,
            int[] measuredDimension) {
        try {
            View view = recycler.getViewForPosition(0);// fix
                                                        // 动态添加时报IndexOutOfBoundsException

            if (view != null) {
                RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();

                int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, getPaddingLeft() + getPaddingRight(),
                        p.width);

                int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, getPaddingTop() + getPaddingBottom(),
                        p.height);

                view.measure(childWidthSpec, childHeightSpec);
                measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
                measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
                recycler.recycleView(view);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }
}

这个嘛,真的吃性能,而且显示是凑合了,但是真的是往后面会很卡,而且会出现空白页面的情况,当然也是拒绝的了。

  1. 会不会是加载图片的时候出问题了,所以就想到了以前解决Recyclerview里checkBox 刷新后状态混乱的情况,加Tag 获取Tag,每次加载的时候都要对比tag,一致的时候再去设置图片
@Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        //设置本地资源占位
        holder.goodIcon.setImageResource(R.drawable.ic_launcher);
        holder.goodIcon.setTag(R.id.goodIcon, "goodsIcon");
        if (holder.goodIcon.getTag() != null && holder.goodIcon.getTag(R.id.goodIcon).equals("goodsIcon")) {
               Glide.with(mContext)
               .load(url)
               .into(holder.goodIcon);        
        }
}

这个方法吗,也是看运气,上面是我复现的伪代码,我也是试过之后感觉无效才删了,哈哈

  1. RecyclerView 的adapter里重写以下方法,
@Override
public int getItemViewType(int position) {
    return position;
}

并且在给RecycleView设置适配器前,要先设置adapter.setHasStableIds(true),这句是表明使用这个,相当于给图片加了一个tag,tag不变的话,不用重新加载图片。但是也有问题,这会使得 列表的 数据项 重复了,所以还要去实现一个方法:

@Override
public long getItemId(int position) {
    return position;
}
  1. 就是检查加载图片加载的情况了,Glide,这个是我常用的,加载的时候要尽量使用占位图和缓存,一般我是这样写的
    Glide.with(mContext)
             .load(findImag.get(0))
             .diskCacheStrategy(DiskCacheStrategy.ALL)
             .placeholder(R.drawable.rectang_holder)
             .centerCrop()
             .into(itemImage);

这个需要大家根据自己的情况去判断,是不是因为加载图片的方式导致的。
尝试了这么多,最后我也终于找到了自己的问题所在,约束布局ConstraintLayout,没错就这货!

优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿_第1张图片
image.png

当我无计可施的时候,我突然想到了会不会是布局出问题了,于是把约束布局全都换成的线性布局,奇迹就出现了,错乱的布局显示正常了,这样我挺意外的,约束布局的初衷是为了解决布局嵌套太深,怎么还带来了坑,其实可以从源码中得到一些启示
先看约束布局;


public class ConstraintLayout extends ViewGroup {
    static final boolean ALLOWS_EMBEDDED = false;
-----------省略一部分-----------
 private void setChildrenConstraints() {
        if (this.mConstraintSet != null) {
            this.mConstraintSet.applyToInternal(this);
        }

        int count = this.getChildCount();
        this.mLayoutWidget.removeAllChildren();

        for(int i = 0; i < count; ++i) {
            View child = this.getChildAt(i);
            ConstraintWidget widget = this.getViewWidget(child);
            if (widget != null) {
                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams)child.getLayoutParams();
                widget.reset();
                widget.setVisibility(child.getVisibility());
                widget.setCompanionWidget(child);
                this.mLayoutWidget.add(widget);
                if (!layoutParams.verticalDimensionFixed || !layoutParams.horizontalDimensionFixed) {
                    this.mVariableDimensionsWidgets.add(widget);
                }

                if (layoutParams.isGuideline) {
                    android.support.constraint.solver.widgets.Guideline guideline = (android.support.constraint.solver.widgets.Guideline)widget;
                    if (layoutParams.guideBegin != -1) {
                        guideline.setGuideBegin(layoutParams.guideBegin);
                    }

                    if (layoutParams.guideEnd != -1) {
                        guideline.setGuideEnd(layoutParams.guideEnd);
                    }

                    if (layoutParams.guidePercent != -1.0F) {
                        guideline.setGuidePercent(layoutParams.guidePercent);
                    }

再看Recyclerview

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {

    static final String TAG = "RecyclerView";

    static final boolean DEBUG = false;

    static final boolean VERBOSE_TRACING = false;

-------------------------省略一部分-----------------------------
  private void initChildrenHelper() {
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            @Override
            public int getChildCount() {
                return RecyclerView.this.getChildCount();
            }

            @Override
            public void addView(View child, int index) {
                if (VERBOSE_TRACING) {
                    TraceCompat.beginSection("RV addView");
                }
                RecyclerView.this.addView(child, index);
                if (VERBOSE_TRACING) {
                    TraceCompat.endSection();
                }
                dispatchChildAttached(child);
            }

可以看到约束布局和Rv都是继承自viewGroup,但是,Rv还同时实现了ScrollView和NestedScrollingChild ,相应的就是需要处理滑动时间和事件分发,那么约束布局的设计初衷是减少嵌套,约束布局不建议用在有滑动控件的情况下,这是在约束布局设计的时候就是这样设计的。因为约束布局里面的每个控件的位置都是被约束给相对锁定的。
但是我的结构确实是复杂了点,外层Rv的item跟布局是约束布局,下面又存在一个Rv,这样会导致事件分发和处理变得很耗时,也就造成了卡顿和错乱。

好了,解决了Rv滑动错乱的问题,我们再来解决另一头恶魔---ANR

说实话,在这之前我是没想到会让我遇到ANR的问题,我是很注意bitemap的回收以及数据库游标的关闭,也不会在主线程执行耗时操作,这怎么就出现了ANR呢,赫然的一个弹窗,像是赤裸裸的讽刺哇

优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿_第2张图片
image.png

一般来说Android上造成ANR无非是以下几种情况:
1.按键和触摸事件5s内没被处理完
2.广播:Broadcast ,前台广播为10s处理时间,后台广播为60s处理时间,未在规定时间内完成就会造成ANR
3.service服务: 前台服务20s,后台200s未完成启动
4.内容提供者ContentProvider的publish在10s内没进行完
既然出现了ANR就要从以下几个方面考虑了:
1.主线程在做一些耗时的工作
2.主线程被其他线程锁
3.cpu被其他进程占用,该进程没被分配到足够的cpu资源。
逐一排除后,我发现,
我的卡顿和ANR完全是因为Rv嵌套Rv,在滑动的时候处理事件无法及时响应造成的。这多亏了Android studio 的profile
优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿_第3张图片
image.png

通过检测cpu性能逐步recode到了问题所在。
但是存在一个问题,似乎对于我现在需要的需求的来说,除了嵌套,似乎也没有什么好的方法。百度一番,有说重写Recyclerview解决的;

public class MyRecycleView extends RecyclerView {
 
    public MyRecycleView(Context context) {
        super(context);
    }
 
    public MyRecycleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
 
    public MyRecycleView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        //返回false,则把事件交给子控件的onInterceptTouchEvent()处理
        return false;
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        //返回true,则后续事件可以继续传递给该View的onTouchEvent()处理
        return true;
    }
}

但是这样就掉坑了,哈哈,恭喜你,你的外层rv将不能滑动了。


优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿_第4张图片
image.png

所以啊,尽量不要使用Rv嵌套Rv,否则进坑容易跳坑难,但是呢,如果非要这么做,也不是不能解决,我们还可以通过以下的暴力手段去优雅的解决掉嵌套带来的卡顿和AN**

        //优化嵌套卡顿
        shoppingCar.setHasFixedSize(true);
        shoppingCar.setNestedScrollingEnabled(false);
        shoppingCar.setItemViewCacheSize(600);
        RecyclerView.RecycledViewPool recycledViewPool = new 
        RecyclerView.RecycledViewPool();
        shoppingCar.setRecycledViewPool(recycledViewPool);
  • setHasFixedSize,作用在于当知道Adapter内Item的改变不会影响RecyclerView宽高的时候,可以设置为true让RecyclerView避免重新计算大小
  • setNestedScrollingEnabled 这个是在处理滑动卡顿时常用的,牵扯到时间分发和手势,不再赘述
  • setItemViewCacheSize 是设置子视图的缓存处理大小,这里为了立杆见影,我设置成了600,哈哈,一般200就行
  • recycledViewPool 则是重新给定义个新的存放视图的pool
    好了,目前就是这么多,时间仓促,总结不周,如果有不准确的地方可以私信我,欢迎交流。

看看时间,22点多了,洗洗睡了

优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿_第5张图片
image.png

你可能感兴趣的:(优雅(暴力)解决RecyclerView 嵌套RecyclerView 导致的卡顿)