优雅(暴力)解决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,没错就这货!

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呢,赫然的一个弹窗,像是赤裸裸的讽刺哇

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
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将不能滑动了。


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点多了,洗洗睡了

image.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容