观心静

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

问题描述

  在使用RecyclerView实现列表的时候会有极低的概率出现点击后数组越界的报错的问题。

 

问题原因

  请看下面这个几行在RecyclerView.Adapter里的一段代码

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mSelectedPosition = viewHolder.getAdapterPosition();
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }

数组越界的关键点就是使用了getAdapterPosition();来获取点击的位置。而getAdapterPosition();方法获取位置有概率在Adapter在刷新视图的时候返回 -1 这个值。这个时候就会导致数组越界了。

 

复现问题

  为什么要复现问题? 因为我是测试转开发,个人习惯解决问题的时候同时去找到复现问题的条件。一般正常情况下,使用getAdapterPosition()方法在上面的代码中点击后获取的数据的位置,在人工进行测试的时候是极难复现这个数组越界问题的。只因为你是人类你没有这个手速,你没办法在刷新视图的一瞬间(只有16ms的时间)去点击item获取getAdapterPosition()数据位置,触发这个bug。所以,此问题一般是Android设备UI线程轻微被堵塞或者在跑monkey的情况下才会出现这个报错。

  但是,从代码上刻意去制作条件复现这个问题没这么难,我们可以增加在获取getAdapterPosition()前面添加一行notifyDataSetChanged() 刷新视图即可马上复现此问题。代码如下:

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    notifyDataSetChanged(); //增加这行代码
                    mSelectedPosition = viewHolder.getAdapterPosition();
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }

 

解决问题

  最主要的还是理解getAdapterPosition() 与 getLayoutPosition() 的区别,根据实际情况使用对应的方法。下面有说明getAdapterPosition() 与 getLayoutPosition() 的区别,这里提供几种获取位置的解决办法的思维。

  方式1,如果是在数据很少变化的情况下,将getAdapterPosition()方法替换成getLayoutPosition()方法就可以解决此问题了,代码如下:

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mSelectedPosition = viewHolder.getLayoutPosition();
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }

  方式2,在public void onBindViewHolder(@NonNull ViewHolder holder, int position) {} 方法里实现点击,因为直接就有position了。但是这种方法缺点是影响性能,onBindViewHolder方法调用的十分频繁。这里方法里负责给View装载数据。在滚动的时候会大量触发,这里频繁new 一个接口进去不是非常好。

  方式3,使用findContainingViewHolder来定位点击的ViewHolder在获取位置

 /**
     * Returns the ViewHolder that contains the given view.
     *
     * @param view The view that is a descendant of the RecyclerView.
     *
     * @return The ViewHolder that contains the given view or null if the provided view is not a
     * descendant of this RecyclerView.
     */
    @Nullable
    public ViewHolder findContainingViewHolder(@NonNull View view) {
        View itemView = findContainingItemView(view);
        return itemView == null ? null : getChildViewHolder(itemView);
    }

 

 getAdapterPosition() 与 getLayoutPosition() 的区别

  getAdapterPosition 与 getLayoutPosition 方法是google替代 getPosition提供的新api。 你们也可以在Android studio上看他们的注释了解下google的想法。

  个人认为getAdapterPosition() 是提供数据在刷新的时候提供一个-1的返回值,来告知视图其实正在重新绘制。这个时候点击位置与你想要的数据正在变化中是不一致的。(因为绘制View与数据位置这2组内容其实是异步的,所以Position理所当然的是不准确的

  而getLayoutPosition()更加简单暴力,你点击不会告诉你数据是否正在刷新,始终会返回一个位置值。这个位置值有可能是之前的视图item位置,也有可能是刷新视图后的item位置。

 

  那么问题来了,应该如何取舍他们或者在什么情况下使用他们呢?

  getLayoutPosition() 更适合在短时间内数据变动少,View刷新不频繁的情况下使用,或者是固定列表数据,一切简单化没这么复杂。

  getAdapterPosition() 更适合在频繁变动数据的情况下使用,指那种数据刷新极快而且是连续刷新的情况下使用,-1的无位置的返回值告诉你视图正在变化,你需要判断是否执行这次点击。如下代码:

 

    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_guardian_home_video, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mSelectedPosition = viewHolder.getAdapterPosition();
                    if (mSelectedPosition == RecyclerView.NO_POSITION){
                        return;
                    }
                    mListener.onClick(mList.get(mSelectedPosition), mSelectedPosition);
                }
            }
        });
        return viewHolder;
    }
onBindViewHolder
posted on 2020-01-14 15:08  观心静  阅读(5481)  评论(0编辑  收藏  举报