喜新不厌旧之——RecyclerView,事关小坑和优化

最近有个小学弟问了我一个问题:

“ X哥,我这RecyclerView的item中的控件宽度为什么总是不充满屏幕啊,我设置的明明是match_parent啊 ”

我:“ 你列表的item内容布局复杂不 ”

“ 不复杂,就俩TextView ”

我:“ 那改用ListView就好了 ”

“ 。。。 ”

 

当然善良的我肯定还是给他解决掉了问题,误人子弟可不是在下的风格。他Adapter里onCreateViewHolder方法中用的是view = LayoutInflater.from(mContext).inflate(itemLayoutId, null),换成LayoutInflater.from(mContext).inflate(itemLayoutId, parent,false)就好了。

好学的小学弟还追问为什么换成这个就没问题,说来属实令我老脸一红,我竟一时没想起来根原是什么。索性就在这里记录一下。

  • item布局尺寸失效问题原因

item尺寸失效肯定和itemView有关

。。。

话不多说,直接看RecyclerView源码。RecyclerView源码里有这么一段:


...   省略无用代码

    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }

...

就是这段代码给itemView设置LayoutParams的。可以看到,首先获取itemView的LayoutParams赋值给变量lp,再声明一个LayoutParams变量rvLayoutParams。注意这里不是同一个类,获取的item的LayoutParams是ViewGroup.LayoutParams,而rvLayoutParams是RecyclerView中定义的一个LayoutParams,继承自ViewGroup.MarginLayoutParams

public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
        ViewHolder mViewHolder;
        final Rect mDecorInsets = new Rect();
        boolean mInsetsDirty = true;
        // Flag is set to true if the view is bound while it is detached from RV.
        // In this case, we need to manually call invalidate after view is added to guarantee that
        // invalidation is populated through the View hierarchy
        boolean mPendingInvalidate = false;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

...

}

我们不用太关心这个,只需知道它是RecyclerView中定义的一个LayoutParams即可。接着往下分析源码,先判断一下lp是否为null,如果是null,就调用generateDefaultLayoutParams()方法给rvLayoutParams赋值,并将rvLayoutParams设置给itemView;如果lp不为null,就再判断checkLayoutParams(lp)的值,如果为false,就调用generateLayoutParams(lp)方法给rvLayoutParams赋值,并将rvLayoutParams设置给itemView;如果是true,就只将lp类型强转为RecyclerView.LayoutParams后赋值给rvLayoutParams。那么我们一步步来,先看一下lp为null时走的generateDefaultLayoutParams()方法,故名思意,这个方法作用是生成默认的LayoutParams:

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        if (mLayout == null) {
            throw new IllegalStateException("RecyclerView has no LayoutManager");
        }
        return mLayout.generateDefaultLayoutParams();
    }

它里面只是调用了mLayout.generateDefaultLayoutParams()。这里我要吐槽一下开发者,这个mLayout实际上是LayoutManager类的对象

@VisibleForTesting LayoutManager mLayout;

你方法命名知道要见名知意,这里变量命名你搞个这个名字,光看名字的话谁能够想到mLayout是LayoutManager的对象?你哪怕起个mManager都比mLayout强吧

好掐完我们继续(擦擦手)。点进mLayout.generateDefaultLayoutParams()方法:

public abstract LayoutParams generateDefaultLayoutParams();

这是LayoutManager中的一个抽象方法,所以就要到实现类里面去找了。LayoutManager的默认实现类有三个,LinearLayoutManager、GridLayouManager和StaggeredGridLayouManager,也就是我们经常使用的线性布局,网格布局和瀑布流布局了。我们继续点进三种Manager看一下:

    //这是LinearLayoutManager的实现
    @Override
    public LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }


    //这是GridLayoutManager和StaggeredGridLayoutManager的实现
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        if (mOrientation == HORIZONTAL) {
            return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.MATCH_PARENT);
        } else {
            return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    }

可以看到,LinearLayoutManager返回了一个新建的LayoutParams,这个params宽高都设置的是WRAP_CONTENT,而另外两个Manager返回的LayoutParams的宽高是根据方向决定的,横向就是高度为MATCH_PARENT,纵向就是宽度为MATCH_PARENT。看到这里我们就能大概猜出来为什么item宽度失效了吧。

上面这些说的都是lp==null的时候走的流程,那么我们还需要看一下获取到的lp到底是不是null。别急,我们先把lp不为null的时候都做了些什么简单看一下。lp不为null的时候判断了一下checkLayoutParams(lp),我们点进这个方法看一下:

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p);
    }

很简单,返回的是lp是否是RecyclerView.LayoutParams的实例并且lp是否为null,(mLayout.checkLayoutParams((LayoutParams) p)方法就是一个简单的判空,这里就不贴源码了,而且这里必然不为null,因为判空在第一步就已经做了,只有lp不为null才会走到这里)如果checkLayoutParams(lp)返回false,说明lp不是RecyclerView.LayoutParams的实例,就调用generateLayoutParams(lp)方法将lp转换成RecyclerView.LayoutParams然后设置给itemView。

接下来我们就来看一下获取到的lp是不是null。lp是从itemView获取的,而itemView就是我们在Adapter中的onCreateViewHolder生成的。生成itemView的时候用LayoutInflater.from(mContext).inflate(itemLayoutId, null)方法生成存在尺寸失效问题,用LayoutInflater.from(mContext).inflate(itemLayoutId, parent,false)方法生成就没有问题,所以问题肯定就出在inflate方法上,我们来看一下这个inflate方法。这个方法共有四个重载方法:

    //方法1
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    
    //方法2
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }
    
    //3方法
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean         
    attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) +"\" ("+ Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    //方法4
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean 
    attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;

        ...
    }

我们常用的就是方法1和3,小学弟使用的就是方法1,可以看到,方法1实际上也是调用了方法3,而方法123实际上最终都是调用的方法4,我们来详细看一下方法4:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            
            //...去掉无用代码
            View result = root;

            try {
                    //...去掉无用代码
               
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                       
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                         
                            temp.setLayoutParams(params);
                        }
                    }

                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

代码中temp就是根据传入的layoutId生成的view,root就是inflate(itemLayoutId, parent,false)中的parent参数,还声明了一个ViewGroup.LayoutParams变量params。先是将要返回的result设置为root;然后首先判断root是否为null,如果不为null,就根据root在xml中设置的属性生成一个LayoutParams赋值给params,再判断attachToRoot也就是inflate方法的第三个参数,如果是false,就将params设置给temp,这也正是我们所传入的参数走的流程;接着往下,如果root!=null&&attachToRoot,就将temp添加进root(注意在Adapter里生成View的时候inflate的第三个参数不能传true,否则会导致崩溃,原因就是传true时inflate方法这里会addView一次,然后RecyclerView代码里又会addView一次,所以这里一定不能传true);再往下最后判断一下如果root == null || !attachToRoot,就把返回值result设置为temp。

看到这里就彻底明白了,联系RecyclerView的源码总结一下:如果Adapter里onCreateViewHolder中使用inflate(itemLayoutId, null)方法生成itemView,那么由于parent参数传的是null,inflate方法里就不会给itemView设置LayoutParams,那么到RecyclerView中holder.itemView.getLayoutParams()也就是lp的值也就为null,就会走LayoutManager中的generateDefaultLayoutParams()方法获取一个默认LayoutParams设置给itemView,如果你用的是LinearLayoutManager,那么generateDefaultLayoutParams()方法返回的LayoutParams宽高都是wrap_content,那自然无论你在xml中如何设置item的尺寸都会失效;如果用inflate(itemLayoutId, parent,false)生成itemView,那么就会根据parent生成LayoutParams设置给itemView,到RecyclerView中lp就不为null,item尺寸自然也就不会失效了。所以在使用RecyclerView时,onCreateViewHolder中要使用inflate(itemLayoutId, parent,false)生成view(再说一次第三个参数一定不能传true!!!)。

 

  • RecyclerView性能优化

说完了item布局失效问题,再来记录一下RecyclerView的优化点 

  1. 在ViewHolder中声明一个SparseArray<View>用于保存item中的控件,首次使用控件时findViewById,再次使用控件时就从SparseArray中获取,避免反复findViewById。
    public class BaseRvViewHolder extends RecyclerView.ViewHolder {
    
        private SparseArray<View> mViewArray;
        public BaseRvViewHolder(View itemView) {
            super(itemView);
            mViewArray=new SparseArray<>();
        }
    
        public <V extends View> V getView(int id){
            View view=mViewArray.get(id);
            if (view==null){    //如果为空说明是首次使用该view,就findViewById获取,并添加到mViewArray,下次再使用该view就从mViewArray中获取,避免反复findViewById
                view=itemView.findViewById(id);
                mViewArray.put(id,view);
            }
            return (V)view;
        }
    ...
    }

     

  2. 设置item点击事件时不要在onBindViewHolder中设置。
    通常童鞋们设置item点击事件代码如下:
        @Override
        public void onBindViewHolder(BaseRvViewHolder holder, final int position) {
            ...
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mOnItemClickListener.onItemClick(position);
                }
            });
        }

    在onBindViewHolder方法中给itemView设置点击事件。但是由于在RecyclerView来回滚动的时候,onBindViewHolder方法会被频繁的调用,就会频繁的给itenView设置点击事件,这就造成了资源的浪费。所以我们可以在onCreateViewHolder方法中给itemView设置点击事件。onCreateViewHolder方法只有在没有可复用的ViewHolder时才会被调用,调用后会创建一个ViewHolder,以后再需要ViewHolder就直接复用,不会频繁的调用onCreateViewHolder方法,所以在这里设置item点击事件就不会随着RecyclerView来回滚动而频繁设置点击事件了。不过一般onCreateViewHolder方法里只做创建ViewHolder的工作,所以我们可以再优雅一点,在ViewHolder中给itemView设置点击事件。有童鞋可能要问了:item点击事件需要用到position,不在onBindViewHolder里设置的话position怎么得到?

     getLayoutPosition()方法是用来炖着吃的嘛?直接上代码:

        @Override
        public BaseRvViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view;
            ...
            return new BaseRvViewHolder(view,mOnItemClickListener);
        }

    在onCreateViewHolder中创建ViewHolder时传入一个自定义的OnItemClickListener接口

    public class BaseRvViewHolder extends RecyclerView.ViewHolder {
    
        private SparseArray<View> mViewArray;
        public BaseRvViewHolder(View view, final BaseRvAdapter.OnItemClickListener onItemClickListener) {
            super(view);
            mViewArray=new SparseArray<>();
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (getLayoutPosition()!=RecyclerView.NO_POSITION) {    //注意这里为安全起见确保一下getLayoutPosition()的值不为RecyclerView.NO_POSITION(也就是-1)
                        onItemClickListener.onItemClick(getLayoutPosition());
                    }
                }
            });
        }
        ...
    }

    然后在ViewHolder的构造方法中给itemView添加点击事件。

  3. 如果多个RecyclerView具有相同的ViewHolder,也就是具有相同的item布局时,可以共用一个RecycledViewPool。
    RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
    recyclerView1.setRecycledViewPool(pool);
    recyclerView2.setRecycledViewPool(pool);
    recyclerView3.setRecycledViewPool(pool);

     

  4. 当确定Item的改变不会影响RecyclerView的宽高时可以设置setHasFixedSize(true),这样就可以使RecyclerView不再重新计算尺寸。需要注意的是刷新数据要用Adapter的几个notifyItemXXX方法才不会重新计算尺寸,用notifyDataSetChanged()依然会计算,原因是RecyclerView源码中notifyDataSetChanged()方法没有用到mHasFixedSize字段,直接调用了requestLayout()方法,而那几个notifyItemXXX方法中判断了mHasFixedSize的值,为false才会调用requestLayout()方法。

  5. 使用notifyItemXXX方法或者借助DiffUtil进行局部数据刷新。当RecyclerView只有个别条目有变化时,不要无脑地暴力使用notifyDataSetChanged()。

上面五个优化点已经基本能满足需求了,还想进一步优化的话就是优化item布局减少层级嵌套过度绘制,还有以空间换时间比如增大RecyclerView的缓存和预留空间等,这里就不再多介绍了,有需要的童鞋可以自行百度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值