1. 程式人生 > >RecyclerView你所不知道的祕密

RecyclerView你所不知道的祕密

問題是什麼

問題還要從接入百度廣告說起,這裡要說的是百度原生廣告,接入過廣告的同學可能知道,接入廣告SDK,廣告的點選事件基本都是由SDK處理的,開發者只需要傳入需要被點選的View即可。雖然這給開發者省了不少事,有時候出了問題,反而會阻礙我們去分析問題。

下面這個介面,就是向百度廣告用來註冊點選事件的介面:

NativeAd.registerViewForInteraction(View view);

我在把廣告接入RecycleView中時,遇到一個坑,初始顯示的時候,點選一切正常,當廣告item被滑出螢幕,然後再次滑回來後,item就點選不了。這是什麼原因呢?真是百思不得其解。

上圖,先看下問題場景:
在這裡插入圖片描述

上面一行4個應用item就是接入的百度廣告資料,其中被我框起來的應用,點選沒有反應了。

問題分析

1、思路一
既然廣告item不能點選,那就去確認點選事件有沒有設定好,這是最直接的思路。RecyclerView.Adapter的onBindViewHolder方法中會呼叫registerViewForInteraction(View)註冊View的點選事件,即使item的View被回收了,下次顯示時會再次註冊事件,正常應該沒有問題才對。需要去跟一下registerViewForInteraction方法的實現,但是百度沒有開放程式碼,只能望而卻步。

這裡貼下onBindViewHolder中的實現程式碼:

    @Override
    public void onBindViewHolder(@NonNull MultiAdsAdapter.ViewHolder holder, final int position) {
        if (holder.getItemViewType() == TYPE_AD) {//百度廣告型別的資料
            NativeAd nativeAd = mList.get(position).getAd();
            holder.getTextView().setText(nativeAd.getAdTitle());
            if (!TextUtils.isEmpty(nativeAd.getAdIconUrl())) {
                Glide.with(mContext.getApplicationContext()).load(nativeAd.getAdIconUrl()).diskCacheStrategy(DiskCacheStrategy.NONE).into(holder.getImageView());
            }
            nativeAd.registerViewForInteraction(holder.imageView);//註冊View的點選事件
        } else {
            holder.getTextView().setText("test" + position);
            holder.getImageView().setImageResource(R.drawable.ic_launcher);
        }

    }

2、思路二
通過在adapter生命週期中新增跟蹤日誌,我發現,在item在滑出螢幕時,View會被回收,會被快取起來,在下次再次滑動回來,會使用快取中的View,重新去繫結資料。這個其實是正常的adapterView的實現方案,沒什麼問題啊?理論上確實沒有問題,巧就巧在遇上了百度廣告,這種半吊子SDK,裡面的坑誰踩誰知道。
根據日誌發現,最上面的四個item,在再次顯示時,重用了快取裡面的View,但是View可能不再是之前對應位置的View了。我就想,既然重用有問題,能不能每次滑出螢幕的時候,讓View自動回收了,不讓它重用。

    @Override
    public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        if (holder.getItemViewType() == TYPE_AD) {
            Log.d("MultiAdsAdapter", "onViewAttachedToWindow" + holder.itemView);
        }

    }

    @Override
    public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        if (holder.getItemViewType() == TYPE_AD) {
            Log.d("MultiAdsAdapter", "onViewDetachedFromWindow" + holder.itemView + ", isRecyclable = " + holder.isRecyclable());
            if (holder.isRecyclable()) {
                holder.setIsRecyclable(false);
            }
        }
    }

通過在onViewDetachedFromWindow方法中呼叫holder.setIsRecyclable(false),意思是,是否可回收利用,設定為false後,就是不重用,下次展示時會重新建立View,此方法果然奏效。
但是對於追求高效的我一想,如果不能重用View,RecyclerView的作用何在,於是有了思路三。

3、思路三
根據日誌發現,最上面的四個item,在再次顯示時,重用了快取裡面的View,但是View可能不再是之前對應位置的View了,就是這個位置問題導致了百度廣告無法點選了。既然是位置問題導致,我可不可以,在重新繫結資料時,保證重用的View還是之前使用的那個。於是查閱RecyclerView的方法,果不其然,居然有現成的介面可以設定View快取,雖然還沒去嘗試,但是我感覺已經找到了解決方案。
這個方法就是:

    /**
     * Sets a new {@link ViewCacheExtension} to be used by the Recycler.
     *
     * @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
     *
     * @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)
     */
    public void setViewCacheExtension(ViewCacheExtension extension) {
        mRecycler.setViewCacheExtension(extension);
    }

再看下ViewCacheExtension介面引數,RecyclerView會根據position(資料索引)和type(View的型別)去取快取,自己可以去自定義返回的View。

    public abstract static class ViewCacheExtension {

        /**
         * Returns a View that can be binded to the given Adapter position.
         * <p>
         * This method should <b>not</b> create a new View. Instead, it is expected to return
         * an already created View that can be re-used for the given type and position.
         * If the View is marked as ignored, it should first call
         * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
         * <p>
         * RecyclerView will re-bind the returned View to the position if necessary.
         *
         * @param recycler The Recycler that can be used to bind the View
         * @param position The adapter position
         * @param type     The type of the View, defined by adapter
         * @return A View that is bound to the given position or NULL if there is no View to re-use
         * @see LayoutManager#ignoreView(View)
         */
        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
    }

解決方案

通過對思路三的嘗試,證明此方法可行。

下面就看下ViewCacheExtension的實現:

public class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {

    private SparseArray<View> mViewCache;

    public MyViewCacheExtension() {
        mViewCache = new SparseArray(4);
    }

    @Override
    public View getViewForPositionAndType(RecyclerView.Recycler recycler, int position, int type) {
        if (type == MultiAdsAdapter.TYPE_AD
        && mViewCache.size() > position) {
            return mViewCache.get(position);
        }
        return null;
    }

    public void cacheView(int position, int type, View view) {
        if (type != MultiAdsAdapter.TYPE_AD) {
            return;
        }
        if (mViewCache.get(position) != view) {
            mViewCache.put(position, view);
        }
    }

    public void clearCache() {
        mViewCache.clear();
    }

}

那在什麼地方去快取初始的View呢?當然是在onBindViewHolder回撥的時候了

    @Override
    public void onBindViewHolder(@NonNull MultiAdsAdapter.ViewHolder holder, final int position) {
        if (holder.getItemViewType() == TYPE_AD) {
            mAdViewCache.cacheView(position, TYPE_AD, holder.itemView);
            NativeAd nativeAd = mList.get(position).getAd();
            Log.d("MultiAdsAdapter", "onBindViewHolder, position = " + position + ", holder.itemView = " + holder.itemView);
            holder.getTextView().setText(nativeAd.getAdTitle());
            if (!TextUtils.isEmpty(nativeAd.getAdIconUrl())) {
                Glide.with(mContext.getApplicationContext()).load(nativeAd.getAdIconUrl()).diskCacheStrategy(DiskCacheStrategy.NONE).into(holder.getImageView());
            }
            nativeAd.registerViewForInteraction(holder.imageView);
            });
        } else {
            holder.getTextView().setText("test" + position);
            holder.getImageView().setImageResource(R.drawable.ic_launcher);

        }

    }

哇塞!可以點選了,完美解決。

總結

整合百度廣告到RecyclerView,讓我偶然遇到了這個問題,並且偶然讓我解決了這個問題,使我有所反思,我們常用的RecyclerView,其實我們瞭解的並不夠,有必要研究下它的實現原理,怎麼實現的重用,還有它賴以成名的View的快取機制,後續文章再和大家分享這個主題。