天天看點

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

本文站在巨人的肩膀上 自我感覺又進了一步而成。

基于翔神的大作基礎之上寫的一個為RecyclerView添加HeaderView FooterView 的另一種解決方案, 

翔神連結文首鎮樓:http://blog.csdn.net/lmj623565791/article/details/51854533 

上次翔神發表這篇文章時,我就提了個問題:說headerView和FooterView都是強引用在Adapter中的,這樣即使他所屬的ViewHolder被回收複用(後實踐發現,就算設定了HeaderView的ViewHolder不緩存,但是始終有一個HeaderView的ViewHolder在被強引用),但是View本身的執行個體還是在被強引用,記憶體空間也無法釋放的。 這樣做雖然速度沒任何問題,(甚至還有提升,但是HeaderView過大記憶體空間就會吃緊了吧) 因為我司項目大多HeaderView又臭又長,是以我想了好久 改寫了一下,換了種思路,給RecyclerView提供資料和布局,并且可以讓開發者動态配置headerView在RecyclerViewPool裡的緩存數,将UI的建立 和 資料的綁定分開來做,都交由Adapter維護。

先給大家看一下我司app的某個界面設計稿:這種頁面在我們的APP裡有10+個

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

是的你沒看錯,底部還是個不斷加載更多的清單~,對于這種又臭又長的HeaderView,我一想到它在記憶體裡不能釋放,我就渾身難受。

牆裂建議大家先閱讀翔神文章後 再立刻閱讀此文,威力翻倍。這樣對本文使用到的一些吊炸天的東西就不會陌生了,例如通用的CommonAdapter和ViewHolder。

敲黑闆,如果隻是伸手黨,建議直接看 【2 使用方法】,并直接到文末下載下傳連結裡的工程,拷貝recyclerview包下的幾個檔案即可使用。

工程裡已經參考解決,HeaderView适配GridLayoutManager 和StaggeredGridLayoutManager。

========================================================================

【1 引言】

衆所周知,RecyclerView已經是主流,ListView已經成為過去式,而兩者之間有些許的不同,其中比較重要的一點就是ListView自帶addHeaderView,addFooterView方法,而RecyclerView并沒有提供。So,我們開發者要自己想辦法實作這個功能。

市面上大多為RecyclerView添加HeaderView的方案,都是在使用RecyclerView的類中(Activity Fragment)裡建構一個View,并綁定好資料,然後通過XXXAdapter提供的addHeaderView方法,将這個View set進Adapter裡。

Adapter内部使用ArrayList、或者翔神使用的是SparseArray存儲這個View,并為HeaderView FooterView配置設定不同的itemViewType,然後Adapter在onCreateViewHolder和onBindViewHolder方法裡,根據ViewType的不同來判斷這是HeaderView 還是普通item。

這種方法目前為止我隻發現一個弊端(也是本文改進的地方),就是這個HeaderView由于在Adapter裡是被ArrayList、SparseArray強引用的,就算其所屬的RecyclerView.ViewHolder在RecyclerViewPool的緩存池裡 被設定緩存數量為0,被回收了(後來經過實測,發現HeaderViewHolder數量多後,始終有一個ViewHolder在被引用 沒有被釋放,其餘的被成功釋放),但是這個View會因為被ArrayList等強引用着,依然停留在記憶體中。是以該HeaderView并沒有被回收。而想一想普通的item都隻有資料和layoutId傳遞給Adapter,并沒有View的執行個體。

一般情況下 這并沒有任何問題,因為普通項目的HeaderView也不大,但是若HeaderView過于龐大,(就像我司的項目,動辄HeaderView就三+個螢幕長度,三屏之後才是普通的item),在這個頁面已經往下滑了很多距離,浏覽了很多内容,HeaderView早已不可見,此時按照RecyclerView的思路,這個龐大的HeaderView所屬的VIewHolder應該已經進入了RecyclerViewPool的緩存池中,如果設定該種viewType的緩存數量為0,即不緩存,ok,那麼RecyclerView做了它該做的事,不再緩存這個HeaderView寄身的VIewHolder了,在GC垃圾回收觸發後,雖然該種type的ViewHolder被回收了,可惜上文提到,此時HeaderView被強引用住,被回收的隻是其所屬的那個ViewHolder,這個龐大的VIew所占的記憶體空間依然沒有被釋放。

其實我們仔細想一想,RecyclerView Adapter裡是不儲存View對象的,它儲存的隻是資料和layout,而我們也應該遵循此原則 為其添加HeaderView(FooterView)。

(題外話,和ListView相比,RecyclerView更是進一步的 将 UI的建立 和資料的綁定 分成了兩步,(oncreateViewHolder,onBindViewHolder))

敲黑闆,本文就參考翔神的裝飾者模式,為RecyclerView 添加 HeaderView(FooterView),

并且将HeaderView的UI建立,和資料綁定強制分開,提供配置每種headerView的緩存數量的方法,令HeaderView執行個體在Adapter中不再被強引用,讓HeaderView和普通的ItemView沒有兩樣~。

先上預覽動圖:

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案
【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

第二張圖是為了測試headerViewHolder是否真的被回收特意選用4個ImageView組成的HeaderView看效果。

========================================================================

【2 使用方法】

//HeaderView使用方法小窺: 以下為Rv添加兩個HeaderView
mHeaderAdapter = new HeaderRecyclerAndFooterWrapperAdapter(mAdapter) {
    @Override
    protected void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o) {
        switch (layoutId) {
            case R.layout.item_header_1:
                TestHeader1 header1 = (TestHeader1) o;
                holder.setText(R.id.tv, header1.getText());
                break;
            case R.layout.item_header_2:
                TestHeader2 header2 = (TestHeader2) o;
                holder.setText(R.id.tv1, header2.getTxt1());
                holder.setText(R.id.tv2, header2.getTxt2());
                break;
            default:
                break;
        }
    }
};
mHeaderAdapter.addHeaderView(R.layout.item_header_1,new TestHeader1("第一個HeaderView"));
mHeaderAdapter.addHeaderView(R.layout.item_header_2,new TestHeader2("第二個","HeaderView"));
mRv.setAdapter(mHeaderAdapter);      

以上每個HeaderView在緩存池RecyclerViewPool的數量都是預設的5個,

使用如下方法:

mHeaderAdapter.addHeaderView(R.layout.item_header_4, new TestHeader4(pics),0);      

将該種類型的headerView的緩存數量配置為0個。

粗略這麼一看,我擦 什麼辣雞,比翔神那個真是差十萬八千裡,人家隻要4行代碼就加一個HeaderView,而且還不用實作父類Adapter的方法,你這還要switch case 看起來就一坨好麻煩的樣子,走了走了。

客官留步留步,如果客官有這種想法,先冷靜一下,裡聽我港。

這個寫法猛地看起來是略複雜了一些,但是它強制的讓我們将UI的建立和資料的綁定分開了,我們重寫的onBindHeaderHolder()方法,就是資料的綁定過程, 試想一下,基本上每個帶HeaderView的頁面都有下拉重新整理功能,如果你使用傳統方法添加HeaderView,那麼你必須要持有HeaderView的引用才能在資料重新整理時改變頭部資料,而且那些煩人的set方法一樣是要寫一遍,你可能需要将 寫在Activity(Fragment)裡的 建立HeaderView時的set資料方法抽成一個函數,再調用一遍。是以工作量是一點沒減少的。

而且重要的是,使用這種方法,如果将緩存數量設定為0,HeaderView在移出螢幕後,觸發GC事件時,是可以被回收滴。文末給實驗證明。

是以我們這種做法,你的工作量也是一點沒增加滴!反而還是友善滴!優雅滴!

(躲開丢過來的雞蛋)廢話不多說,用法已經看到,下面看我們是怎麼實作的。 如果伸手黨看到這裡覺得已經夠了,那麼就可以去文末直接下載下傳源碼copy使用了,裡面使用的幾個類版權大多歸翔神所有。

========================================================================

【三,實作】

直接貼出核心代碼:

public abstract class HeaderRecyclerAndFooterWrapperAdapter2 extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private class HeaderBean {
        private final int DEFAULT_HEADER_VIEW_CACHE_SIZE = 5;//預設是5 和RecyclerViewPool的預設值一樣
        private int layoutId;//viewType當做layoutId
        private Object data;//該viewType(LayoutId)對應的資料
        private int cacheSize;//該種viewType的HeaderView 在RecyclerViewPool的緩存池内的緩存數量

        public HeaderBean(int layoutId, Object data, int cacheSize) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = cacheSize;
        }

        public HeaderBean(int layoutId, Object data) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = DEFAULT_HEADER_VIEW_CACHE_SIZE;
        }

        public int getLayoutId() {
            return layoutId;
        }

        public void setLayoutId(int layoutId) {
            this.layoutId = layoutId;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public int getCacheSize() {
            return cacheSize;
        }

        public void setCacheSize(int cacheSize) {
            this.cacheSize = cacheSize;
        }

    }      
//按照add順序存放HeaderView的bean,bean包括layoutId,資料Data,和緩存數量cacheSize。
// 在createViewHOlder裡根據layoutId建立UI,在onbindViewHOlder裡依據這個data渲染UI,
// 在onAttachedToRecyclerView 為每種layoutId(同時也是viewType)的headerView設定緩存數量
private ArrayList<HeaderBean> mHeaderDatas = new ArrayList<HeaderBean>();      
@Override
public int getItemViewType(int position) {
    if (isHeaderViewPos(position)) {
        return mHeaderDatas.get(position).getLayoutId();       
//HeaderView的layoutId就是viewType      
} return super.getItemViewType(position - getHeaderViewCount());}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    if (mHeaderDatas != null && !mHeaderDatas.isEmpty()) {//不為空,說明有headerview
        for (HeaderBean HeaderBean : mHeaderDatas) {
            if (HeaderBean.getLayoutId() == viewType) {//比對上了說明是headerView
                return ViewHolderHeader3.get(parent.getContext(), null, parent, viewType, -1);
            }
        }
    }      
protected abstract void onBindHeaderHolder(ViewHolderHeader3 holder, int headerPos, int layoutId, Object o);//多回傳一個layoutId出去,用于判斷是第幾個headerview

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderViewPos(position)) {
        onBindHeaderHolder((ViewHolderHeader3) holder, position, mHeaderDatas.get(position).getLayoutId(), mHeaderDatas.get(position).getData());
        return;      
/**
 * 添加HeaderView
 *
 * @param layoutId headerView 的LayoutId
 * @param data     headerView 的data(可能多種不同類型的header 隻能用Object了)
 */
public void addHeaderView(int layoutId, Object data) {
    mHeaderDatas.add(new HeaderBean(layoutId, data));
}

/**
 * 添加HeaderView
 *
 * @param layoutId  headerView 的LayoutId
 * @param data      headerView 的data(可能多種不同類型的header 隻能用Object了)
 * @param cacheSize 該種headerView在緩存池中的緩存個數
 */
public void addHeaderView(int layoutId, Object data, int cacheSize) {
    mHeaderDatas.add(new HeaderBean(layoutId, data, cacheSize));
}      

定義一個HeaderBean,存放HeaderView 的布局id,需要綁定的資料data,以及該種HeaderView在RecyclerViewPool緩存池中的緩存數量。

将layoutId作為viewType。

定義個ArrayList<HeaderBean>,按照add進來的順序 存放HeaderBean。

首先需要重寫的就是getItemViewType()方法,在這個方法裡根據postion判斷是否在headerView的範圍内,如果是傳回layoutId作為viewType。

然後重寫onCreateViewHolder()方法,如果headerData不為空,說明有HeaderView,那麼周遊headerData,比較目前要建立的這個ViewHolder的type和headerData裡的type,如果一樣,說明則是要建立一個HeaderViewHolder。

在onBindViewHolder()方法中,先根據postion判斷是否是HeaderView,如果是,那麼我們便根據postion從headerDatas裡取出相應的layoutId和data,回調一個 abstract  的 onBindHeaderHolder()的方法,将這些參數都傳入,交由子類去自由處理。 子類在這個方法裡 完成資料的綁定即可。

addHeaderView()方法比較簡單,就是new 一個HeaderBean 然後add進HeaderDatas裡即可。

下面,重點來了,我們将在onAttachedToRecyclerView()方法裡,設定headerView的在RecyclerViewPool裡的緩存數量。

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    mInnerAdapter.onAttachedToRecyclerView(recyclerView);
    //設定HeaderView的ViewHolder的緩存數量
    if (null != mHeaderDatas && !mHeaderDatas.isEmpty()) {
        for (HeaderBean HeaderBean : mHeaderDatas) {
            recyclerView.getRecycledViewPool().setMaxRecycledViews(HeaderBean.getLayoutId(), HeaderBean.getCacheSize());
        }
    }      

判斷如果headerdatas不為空,則周遊其中HeaderBean,并調用 recyclerView.getRecycledViewPool()擷取RecyclerViewPool對象,再調用它的 setMaxRecycledViews()方法,傳入viewType 和相應viewType的緩存數量。

====================================================================================================

【4 RecyclerViewPool 源碼淺析】

關于RecyclerViewPool,可能還有很多人不是很了解(我也是最近才開始了解),這裡簡單說下我的了解,後續要深入研究一下RecyclerView相關的知識。

RecyclerViewPool是一個為RecyclerView緩存ViewHolder的緩存池,它預設會為每種viewType緩存5個ViewHolder。

源碼為證:

public static class RecycledViewPool {
    private SparseArray<ArrayList<ViewHolder>> mScrap =
            new SparseArray<ArrayList<ViewHolder>>();
    private SparseIntArray mMaxScrap = new SparseIntArray();
    private int mAttachCount = 0;

    private static final int DEFAULT_MAX_SCRAP = 5;
      

這裡的DEFAULT_MAX_SCRAP 就是每種viewType的預設緩存數量,本文所定義的預設數量和它保持一緻,為5.

mScrap 以viewType為key,value是一個ArrayList,裡面存放的就是該種ViewType的ViewHolder啦。

mMaxScrap 也以viewType為key,value就是每種viewType的最大緩存數量。

mAttachCount 是用來計數的,每當RecyclerViewPool與一個adapter綁定、解綁,回調onAdapterChanged()方法,在其中便會+1 -1,當它為0時,就會清空這個RecyclerViewPool緩存池裡的所有ViewHolder。這便是緩存池中ViewHolder被清空的時刻了。

源碼如下:

public void clear() {
    mScrap.clear();
}      
void attach(Adapter adapter) {
    mAttachCount++;
}

void detach() {
    mAttachCount--;
}


/**
 * Detaches the old adapter and attaches the new one.
 * <p>
 * RecycledViewPool will clear its cache if it has only one adapter attached and the new
 * adapter uses a different ViewHolder than the oldAdapter.
 *
 * @param oldAdapter The previous adapter instance. Will be detached.
 * @param newAdapter The new adapter instance. Will be attached.
 * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
 *                               ViewHolder and view types.
 */
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
        boolean compatibleWithPrevious) {
    if (oldAdapter != null) {
        detach();
    }
    if (!compatibleWithPrevious && mAttachCount == 0) {
        clear();
    }
    if (newAdapter != null) {
        attach(newAdapter);
    }
}      

我們在onAttachedtoRecyclerVie()方法裡調用的recyclerView.getRecycledViewPool() 方法的源碼如下:很簡單 就是通過mRecycler擷取RecyclerViewPool對象。

/**
 * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
 * if no pool is set for this view a new one will be created. See
 * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
 *
 * @return The pool used to store recycled item views for reuse.
 * @see #setRecycledViewPool(RecycledViewPool)
 */
public RecycledViewPool getRecycledViewPool() {
    return mRecycler.getRecycledViewPool();
}      

緊接着調用的 設定緩存數量的方法源碼如下:

public void setMaxRecycledViews(int viewType, int max) {
    mMaxScrap.put(viewType, max);
    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    if (scrapHeap != null) {
        while (scrapHeap.size() > max) {
            scrapHeap.remove(scrapHeap.size() - 1);
        }
    }
}      

這裡除了将mMaxScrap裡的value改變以外,還從mScrap裡取出了該種ViewType的緩存隊列list,并且判斷size,如果超過最大值,會remove掉相應ViewHolder。

RecyclerViewPool類還有幾個其他的方法:

擷取緩存ViewHolder

public ViewHolder getRecycledView(int viewType) {
    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    if (scrapHeap != null && !scrapHeap.isEmpty()) {
        final int index = scrapHeap.size() - 1;
        final ViewHolder scrap = scrapHeap.get(index);
        scrapHeap.remove(index);
        return scrap;
    }
    return null;
}      

該方法通過viewType擷取緩存的ViewHolder,可以看出,是從緩存list的尾部逐個取出ViewHolder的。

存入ViewHolder

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList scrapHeap = getScrapHeapForType(viewType);
    if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
        return;
    }
    if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
}      

存入時 ,通過viewType擷取對應ViewType的最大緩存數量 以及 對應viewType的緩存list,如果list.size大于等于最大值,則不緩存。 是以我們設定為0永遠>=0,即沒有緩存。(廢話) 如果緩存list未滿,則将該ViewHolder add進去,并且調用

void resetInternal() {
    mFlags = 0;
    mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
}      

為這個ViewHolder恢複一些初始值,可以看到,都是一些flag itemId,postion等。

RecyclerViewPool緩存池一共緩存的ViewHolder數量:是所有viewType的ViewHolder數量之和。

int size() {
    int count = 0;
    for (int i = 0; i < mScrap.size(); i ++) {
        ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i);
        if (viewHolders != null) {
            count += viewHolders.size();
        }
    }
    return count;
}      

最後一個方法是根據viewType擷取相應緩存list,很簡單,有就取出,沒有就new一個,new的時候,要将自己put進mScrap中,如果從mMaxScrap中沒有找到該種viewType對應的緩存數量上限,那麼就使用預設值。

如果我們已經調用過了setMaxRecycledViews()方法,設定過緩存上限,那麼mMaxScrap就能找到該種viewType對應的index,是以就不會設定為預設值5.

private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    ArrayList<ViewHolder> scrap = mScrap.get(viewType);
    if (scrap == null) {
        scrap = new ArrayList<>();
        mScrap.put(viewType, scrap);
        if (mMaxScrap.indexOfKey(viewType) < 0) {
            mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
        }
    }
    return scrap;
}      

至此,所有的RecyclerViewPool的源碼都已經看完啦,這個類還是比較簡單的。

========================================================================

【五,完整代碼】

這份代碼FooterView并沒有用此方法實作,是“強引用VIew方法實作的”。

理由:

1 因為FooterView往往是一個LoadMore相關的提示控件,記憶體占用很有限。

2 LoadMore相關提示的控件 是需要強引用在Fragment Activity 或者相關類中,即使我在Adapter類裡将其引用釋放,這個View在記憶體的空間依然是無法被釋放的。

3 兩種實作方法都放上來,大家可以根據本文描述的方法,自行嘗試将FooterView也改寫,可以和我讨論,稍後我也會附加上我修改的版本。

/**
 * 介紹:一個給RecyclerView添加HeaderView FooterView的裝飾Adapter類
 * 重點哦~ RecyclerView的HeaderView将可以被系統回收,不像老版的HeaderView是一個強引用在記憶體裡
 * 作者:zhangxutong
 * 郵箱:[email protected]
 * 時間: 2016/8/2.
 */
public abstract class HeaderRecyclerAndFooterWrapperAdapter2 extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private class HeaderBean {
        private final int DEFAULT_HEADER_VIEW_CACHE_SIZE = 5;//預設是5 和RecyclerViewPool的預設值一樣
        private int layoutId;//viewType當做layoutId
        private Object data;//該viewType(LayoutId)對應的資料
        private int cacheSize;//該種viewType的HeaderView 在RecyclerViewPool的緩存池内的緩存數量

        public HeaderBean(int layoutId, Object data, int cacheSize) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = cacheSize;
        }

        public HeaderBean(int layoutId, Object data) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = DEFAULT_HEADER_VIEW_CACHE_SIZE;
        }

        public int getLayoutId() {
            return layoutId;
        }

        public void setLayoutId(int layoutId) {
            this.layoutId = layoutId;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public int getCacheSize() {
            return cacheSize;
        }

        public void setCacheSize(int cacheSize) {
            this.cacheSize = cacheSize;
        }

    }

    private static final int BASE_ITEM_TYPE_FOOTER = 2000000;//footerView的ViewType基準值

    //按照add順序存放HeaderView的bean,bean包括layoutId,資料Data,和緩存數量cacheSize。
    // 在createViewHOlder裡根據layoutId建立UI,在onbindViewHOlder裡依據這個data渲染UI,
    // 在onAttachedToRecyclerView 為每種layoutId(同時也是viewType)的headerView設定緩存數量
    private ArrayList<HeaderBean> mHeaderDatas = new ArrayList<HeaderBean>();
    private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();//存放FooterViews,key是viewType

    protected RecyclerView.Adapter mInnerAdapter;//内部的的普通Adapter

    public HeaderRecyclerAndFooterWrapperAdapter2(RecyclerView.Adapter mInnerAdapter) {
        this.mInnerAdapter = mInnerAdapter;
    }

    public int getHeaderViewCount() {
        return mHeaderDatas.size();
    }

    public int getFooterViewCount() {
        return mFooterViews.size();
    }

    private int getInnerItemCount() {
        return mInnerAdapter != null ? mInnerAdapter.getItemCount() : 0;
    }

    /**
     * 傳入position 判斷是否是headerview
     *
     * @param position
     * @return
     */
    public boolean isHeaderViewPos(int position) {// 舉例, 2 個頭,pos 0 1,true, 2+ false
        return getHeaderViewCount() > position;
    }

    /**
     * 傳入postion判斷是否是footerview
     *
     * @param position
     * @return
     */
    public boolean isFooterViewPos(int position) {//舉例, 2個頭,2個inner,pos 0 1 2 3 ,false,4+true
        return position >= getHeaderViewCount() + getInnerItemCount();
    }

    /**
     * 添加HeaderView
     *
     * @param layoutId headerView 的LayoutId
     * @param data     headerView 的data(可能多種不同類型的header 隻能用Object了)
     */
    public void addHeaderView(int layoutId, Object data) {
        //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);

/*        SparseArrayCompat headerContainer = new SparseArrayCompat();
        headerContainer.put(layoutId, data);
        mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);*/

        mHeaderDatas.add(new HeaderBean(layoutId, data));
    }

    /**
     * 添加HeaderView
     *
     * @param layoutId  headerView 的LayoutId
     * @param data      headerView 的data(可能多種不同類型的header 隻能用Object了)
     * @param cacheSize 該種headerView在緩存池中的緩存個數
     */
    public void addHeaderView(int layoutId, Object data, int cacheSize) {
        //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);

/*        SparseArrayCompat headerContainer = new SparseArrayCompat();
        headerContainer.put(layoutId, data);
        mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);*/

        mHeaderDatas.add(new HeaderBean(layoutId, data, cacheSize));
    }

    /**
     * 設定某個位置的HeaderView
     *
     * @param headerPos 從0開始,如果pos過大 就是addHeaderview
     * @param layoutId
     * @param data
     */
    public void setHeaderView(int headerPos, int layoutId, Object data) {
        if (mHeaderDatas.size() > headerPos) {
/*            SparseArrayCompat headerContainer = new SparseArrayCompat();
            headerContainer.put(layoutId, data);
            mHeaderDatas.setValueAt(headerPos, headerContainer);*/
            mHeaderDatas.get(headerPos).setLayoutId(layoutId);
            mHeaderDatas.get(headerPos).setData(data);
        } else if (mHeaderDatas.size() == headerPos) {//調用addHeaderView
            addHeaderView(layoutId, data);
        } else {
            //
            addHeaderView(layoutId, data);
        }
    }

    /**
     * 設定某個位置的HeaderView
     *
     * @param headerPos 從0開始,如果pos過大 就是addHeaderview
     * @param layoutId
     * @param data
     * @param cacheSize 該種headerView在緩存池中的緩存個數
     */
    public void setHeaderView(int headerPos, int layoutId, Object data, int cacheSize) {
        if (mHeaderDatas.size() > headerPos) {
/*            SparseArrayCompat headerContainer = new SparseArrayCompat();
            headerContainer.put(layoutId, data);
            mHeaderDatas.setValueAt(headerPos, headerContainer);*/
            mHeaderDatas.get(headerPos).setLayoutId(layoutId);
            mHeaderDatas.get(headerPos).setData(data);
            mHeaderDatas.get(headerPos).setCacheSize(cacheSize);
        } else if (mHeaderDatas.size() == headerPos) {//調用addHeaderView
            addHeaderView(layoutId, data, cacheSize);
        } else {
            //
            addHeaderView(layoutId, data, cacheSize);
        }
    }

    /**
     * 添加FooterView
     *
     * @param v
     */
    public void addFooterView(View v) {
        mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, v);
    }

    /**
     * 清空HeaderView資料
     */
    public void clearHeaderView() {
        mHeaderDatas.clear();
    }

    public void clearFooterView() {
        mFooterViews.clear();
    }


    public void setFooterView(View v) {
        clearFooterView();
        addFooterView(v);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPos(position)) {
            return mHeaderDatas.get(position).getLayoutId();//HeaderView的layoutId就是viewType
        } else if (isFooterViewPos(position)) {//舉例:header 2, innter 2, 0123都不是,4才是,4-2-2 = 0,ok。
            return mFooterViews.keyAt(position - getHeaderViewCount() - getInnerItemCount());
        }
        return super.getItemViewType(position - getHeaderViewCount());
    }

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

        if (mHeaderDatas != null && !mHeaderDatas.isEmpty()) {//不為空,說明有headerview
            for (HeaderBean HeaderBean : mHeaderDatas) {
                if (HeaderBean.getLayoutId() == viewType) {//比對上了說明是headerView
                    return ViewHolderHeader3.get(parent.getContext(), null, parent, viewType, -1);
                }
            }
        }
        if (mFooterViews.get(viewType) != null) {//不為空,說明是footerview
            return new ViewHolder(parent.getContext(), mFooterViews.get(viewType));
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    //protected abstract RecyclerView.ViewHolder createHeader(ViewGroup parent, int headerPos);

    protected abstract void onBindHeaderHolder(ViewHolderHeader3 holder, int headerPos, int layoutId, Object o);//多回傳一個layoutId出去,用于判斷是第幾個headerview

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderViewPos(position)) {
            onBindHeaderHolder((ViewHolderHeader3) holder, position, mHeaderDatas.get(position).getLayoutId(), mHeaderDatas.get(position).getData());
            return;
        } else if (isFooterViewPos(position)) {
            return;
        }
        //舉例子,2個header,0 1是頭,2是開始,2-2 = 0
        mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
    }


    @Override
    public int getItemCount() {
        return getInnerItemCount() + getHeaderViewCount() + getFooterViewCount();
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mInnerAdapter.onAttachedToRecyclerView(recyclerView);
        //設定HeaderView的ViewHolder的緩存數量
        if (null != mHeaderDatas && !mHeaderDatas.isEmpty()) {
            for (HeaderBean HeaderBean : mHeaderDatas) {
                recyclerView.getRecycledViewPool().setMaxRecycledViews(HeaderBean.getLayoutId(), HeaderBean.getCacheSize());
            }
        }
        //為了相容GridLayout
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int viewType = getItemViewType(position);
                    if (isHeaderViewPos(position)) {
                        return gridLayoutManager.getSpanCount();
                    } else if (mFooterViews.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    }
                    if (spanSizeLookup != null)
                        return spanSizeLookup.getSpanSize(position);
                    return 1;
                }
            });
            gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
        }

    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        mInnerAdapter.onViewAttachedToWindow(holder);
        int position = holder.getLayoutPosition();
        if (isHeaderViewPos(position) || isFooterViewPos(position)) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams) {

                StaggeredGridLayoutManager.LayoutParams p =
                        (StaggeredGridLayoutManager.LayoutParams) lp;

                p.setFullSpan(true);
            }
        }
    }
}
      

========================================================================

【6 實驗】

是否使用了我這種方法,設定了headerView的緩存數量為0後,該HeaderView不在螢幕顯示時,觸發GC,記憶體空間真的能被回收呢,?那麼就實驗見真相。

這裡我們借助 Android Studio 的Android Monitor裡的dump java heap,它可以列印出目前記憶體裡的對象的情況,還有旁邊的小卡車按鈕 Initiate GC,可以觸發GC事件。

我的猜想,設定HeaderView的緩存數量為0,當向下滑動一段距離,HeaderView已不可見時,列印記憶體對象情況,HeaderViewHolder應該是一個“”野對象“”,沒有任何引用,觸發GC後,它将被回收,記憶體降下降。

而使用傳統方法添加的HeaderView,即使設定緩存數量為0,由于View被強引用,記憶體空間也無法釋放。

(劇透一點,我原本想的是HeaderViewHolder應該被釋放,而HeaderView不會被釋放,但實際上記憶體空間真的沒有減少,但是HeaderViewHolder也被強引用住了,如有知情者 望不吝賜教 )

使用本方法:添加6個HeaderView,5個不設定緩存數量,1個設定緩存數量(加載四張圖檔的View),滑動到螢幕低端,Dump Java Heap,

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案
【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

可以看到有一個HeaderView的ViewHolder的Depth為空,說明沒設定緩存數量的ViewHolder的确會被回收掉,這時候我們點選黃色小卡車 強制GC,

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案
【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

記憶體降了一半,這裡面就有被回收掉的ViewHolder的功勞,ViewHolder被順利回收,它裡面引用的圖檔也可以被系統回收 騰出空間。

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

此時再Dump Java Heap 檢視一下,果然被回收掉了一個ViewHolder對象。

再實驗一下,隻添加一個HeaderView(内含4個ImageView),然後滑動出螢幕後,HeaderViewHolder的數量,以及GC後的情況。 對應的圖檔如下:

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

此時就一個ViewHolderHeader的野對象,

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

GC後,它順利被回收,記憶體下降(四張圖檔被回收的功效。)

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

這時,打開使用傳統方式Adapter的Activity,也已經将這個RecyclerView的HeaderView的緩存設定為0

和上次操作一樣,進入後,滑動界面到HeaderView移除螢幕,Dump Java Heap,

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

發現ViewHolderHeader3 的Depth是9,說明其正在被引用着,是無法被釋放的, 這時我們就算再怎麼點選Initiate GC,記憶體也不會下降很明顯,GC後再檢視Java Heap,ViewHolderHeader3的對象依然存在,說明其确實被強引用着沒有被系統回收。

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

這裡有個疑問,想問問各位,@翔神,在我的了解裡,HeaderView執行個體由于在Adapter中強引用不會被回收,可是HeaderViewHolder 并沒有被強引用住啊。這裡看它确實被強引用了。這是一個疑問點,

其實本文到這裡,可以結束了,我已經證明了本文的HeaderAdapter 确實可以将其ViewHolder回收,如果此時觸發GC,将釋放該部分記憶體空間,可是那個HeaderViewHolder的強引用的問題 我依然沒有答案。我又做了一些實驗,

不停的上下滑動,令這個HeaderView 一會滑出螢幕 一會進入螢幕,我的猜想中,應該會create N個 HeaderViewHolder。

檢視java heap,果然如下圖:

【Android】讓HeaderView也參與回收機制,自我感覺是優雅的為 RecyclerView 添加 HeaderView (FooterView)的解決方案

可以看到由于沒有緩存,我們new了 9個ViewHolder出來,但是其中8個是野對象,可以被GC回收,此時螢幕上HeaderView已經不可見了,不明白為何還會有一個強引用。

此問題我将繼續研究下去。。

========================================================================

【7 總結:】

以本文的方法添加的HeaderView以layoutId做itemType。

可以添加多種HeaderView。

可以設定每種HeaderView的ViewHolder在RecyclerViewPool緩存池中的數量。

設定為0則不緩存,當HeaderView不在螢幕可見時,觸發GC的話, 它的ViewHolder将被回收,因為ViewHolder裡的ItemView(HeaderView)本身也沒有被其他強引用,是以它的記憶體空間将被釋放。

就算不設定cacheSize,預設預設值為5(和RecyclerViewPool的預設值一緻)。

同一種類型(相同的layoutId,viewType)的HeaderView也将被RecyclerVIew管理,不像之前那種方法 永遠存于記憶體中。

例如,有兩種item1類型的HeaderView,設定緩存數量為1,那麼 RecyclerViewPool裡會緩存 一份item1類型的ViewHolder,有一份View的空間可以被系統回收。

緩存本身是一種用空間換時間的技術,我們這麼做,将更加靈活,headerView本身簡單時,可以用預設配置,被RecyclerViewPool緩存住無法釋放記憶體也無傷大雅,如果HeaderView本身很臃腫,占記憶體,滑出頁面後,希望記憶體空間可以被回收,那麼可以配置緩存數量,為0 則不緩存,但與此同時,我們擷取了空間,就要付出時間的代價,每次滾回螢幕時,它的ViewHolder已經不存在,是以會重走onCreateViewHolder方法,這需要一定的時間。

是以說,這種方法隻是讓開發者多一種選擇,萬一出現特殊情況,想要釋放HeaderView,也有計可施,實際開發中,大部分情況不需要設定緩存個數,

如果考慮多個RecyclerView共用同一個RecyclerViewPool,可為HeaderView設定1-2個緩存數。

RecyclerView涉及的東西還很多,多個RecyclerView共用一個RecyclerViewPool 我稍後将研究一下,争取也研究出一點小心得分享給大家。即使沒有什麼浏覽量,多一個人看到也是好事。

說實話,設定緩存數量,95%的情況下用不到,但是假設一個場景,(我司app就如此),首頁5個Tab,每個頁面HeaderView長度2-3個螢幕,上面各種圖檔,5個Framgent切換是用hide show做的,是以5個頁面全開時,将全部存于記憶體中,更可怕的是,每個頁面頭部再分2-3個子tab,每個子tab點開又是一坨。。。每個頁面的風格都像h5。

而不用replace是希望使用者每次切換tab回來後還能停留在上次浏覽的地方,這就需要我們做一個抉擇,很多時候還要和産品商量(不過他們既然設計出這種界面,我已經放棄和他們商量)。

如果依然要流暢的速度,還要這種又臭又長的頁面,還不想記憶體OOMcrash,那麼我隻能選擇!狗帶~

========================================================================

本文工具類已經被收入該庫:

https://github.com/mcxtzhang/all-base-adapter

該庫還包括了N多的好用的Adapter。

========================================================================

源碼連結:http://download.csdn.net/detail/zxt0601/9609911

========================================================================