本文站在巨人的肩膀上 自我感覺又進了一步而成。
基于翔神的大作基礎之上寫的一個為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+個
是的你沒看錯,底部還是個不斷加載更多的清單~,對于這種又臭又長的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沒有兩樣~。
先上預覽動圖:
第二張圖是為了測試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,
可以看到有一個HeaderView的ViewHolder的Depth為空,說明沒設定緩存數量的ViewHolder的确會被回收掉,這時候我們點選黃色小卡車 強制GC,
記憶體降了一半,這裡面就有被回收掉的ViewHolder的功勞,ViewHolder被順利回收,它裡面引用的圖檔也可以被系統回收 騰出空間。
此時再Dump Java Heap 檢視一下,果然被回收掉了一個ViewHolder對象。
再實驗一下,隻添加一個HeaderView(内含4個ImageView),然後滑動出螢幕後,HeaderViewHolder的數量,以及GC後的情況。 對應的圖檔如下:
此時就一個ViewHolderHeader的野對象,
GC後,它順利被回收,記憶體下降(四張圖檔被回收的功效。)
這時,打開使用傳統方式Adapter的Activity,也已經将這個RecyclerView的HeaderView的緩存設定為0
和上次操作一樣,進入後,滑動界面到HeaderView移除螢幕,Dump Java Heap,
發現ViewHolderHeader3 的Depth是9,說明其正在被引用着,是無法被釋放的, 這時我們就算再怎麼點選Initiate GC,記憶體也不會下降很明顯,GC後再檢視Java Heap,ViewHolderHeader3的對象依然存在,說明其确實被強引用着沒有被系統回收。
這裡有個疑問,想問問各位,@翔神,在我的了解裡,HeaderView執行個體由于在Adapter中強引用不會被回收,可是HeaderViewHolder 并沒有被強引用住啊。這裡看它确實被強引用了。這是一個疑問點,
其實本文到這裡,可以結束了,我已經證明了本文的HeaderAdapter 确實可以将其ViewHolder回收,如果此時觸發GC,将釋放該部分記憶體空間,可是那個HeaderViewHolder的強引用的問題 我依然沒有答案。我又做了一些實驗,
不停的上下滑動,令這個HeaderView 一會滑出螢幕 一會進入螢幕,我的猜想中,應該會create N個 HeaderViewHolder。
檢視java heap,果然如下圖:
可以看到由于沒有緩存,我們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
========================================================================