轉載請标明出處:
http://blog.csdn.net/zxt0601/article/details/52948009
本文出自:【張旭童的部落格】
本系列文章相關代碼傳送門:
自定義LayoutManager實作的流式布局
歡迎star,pr,issue。
本系列文章目錄:
掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。
掌握自定義LayoutManager(二) 實作流式布局
概述
這篇文章是深入掌握自定義LayoutManager系列的開篇,是一份總結報告。部分内容不屬于引言、過于深入,用作系列後續文章的參考,以及浏覽完後的複習之用。
本文内容涉及RecyclerView、LayoutManager、RecyclerViewPool、Recycler。
注:
1 以下問題,初學者如有不了解的,可以不用太在意,等學習完自定義LayoutManager相關知識,寫幾個Demo再回來看更好了解。
2 在RecyclerView中,ItemView和ViewHolder其實是一一綁定的,是以提到的View = ViewHolder。
一 常見誤區、問題、注意事項:
在自定義LayoutManager文章開始之前,我總結了一些我在學習以及閱讀别人的文章、編碼的過程中,遇到的一些疑惑問題,并附上我個人的了解與答案。歡迎拍磚讨論。
因網上有大量半吊子寫的LayoutManager相關的中文文章。(包括我也是半吊子),是以很多文章看完了,心中都有N個疑問,如,作者好牛逼啊,但是為什麼我獨立寫還是寫不出來。 自定義一個LayoutManager就自動複用了嗎?…等等,下面逐個來講講。
Q1 看完了,但是我獨立寫還是不知道怎麼寫。
A1: 自定義LayoutManager是一項頗有難度的工程,你很難僅僅閱讀一兩篇文章,花兩三個小時就能學習完。
裡面涉及到子View的布局,坐标的計算,偏移量的計算,在滑動時、在合适的時機回收螢幕上不再顯示的View,如何判斷這些View是在螢幕上不可見,以及View究竟是暫時detach掉,還是recycle回收掉…等大量問題
。老實說,也許我水準有限,這是我在學習Android過程中,耗時最久的幾個知識點之一。(十幾個小時才寫出第一個及格的作品)
但是它值得你學習。是以獨立寫不出來别灰心,先仿照一個Demo寫一寫,如果用心了解,第二遍第二遍應該就可以獨立完成了。
Q2 學習自定義LayoutManager需要的鋪墊知識
一 :熟練掌握自定義ViewGroup。
(在自定義LayoutManager過程的第一步,
onLayoutChildren()
方法裡,就類似于自定義ViewGroup的onLayout()方法。)
但與自定義LayoutManager相比,自定義ViewGroup是一種靜态的layout 子View的過程,因為ViewGroup内部不支援滑動,是以隻需要無腦layout出所有的View,便不用再操心剩下的事。
而自定義LayoutManager與之不同,在第一步layout時,千萬不要layout出所有的子View,這裡也是網上一些文章裡的錯誤做法,他們帶着老思想,在第一步就layout出了所有的childView,這會導緻一個很嚴重的問題:你的自定義LayoutManager = 自定義ViewGroup。即,他們沒有View複用機制。
why?這裡簡單證明結論,在Q5的回答裡會說明為什麼。
在Adapter的onCreateViewHolder()方法裡增加列印語句,如果你的資料源有100000條資料,那麼在RecyclerView第一次顯示在螢幕上時,onCreateViewHolder()會執行100000次,你就可以盡情的欣賞ANR了。
反觀使用官方提供的三種LayoutManager,開始時螢幕上有n少個ItemView,一般就執行n次onCreateViewHolder(),(也有可能多執行1次),在後續滑動時,大部分情況都隻是執行onBindViewHolder()方法,不會再執行onCreateViewHolder()。
二 : 熟練使用RecyclerView。這個不用多說,畢竟RecyclerView是LayoutManager的宿主。
其實會以上兩點就可以開始我們的學習之旅了,不過如果能對RecyclerView的Adapter、RecyclerViewPool、ItemDecoration也有一定的了解那是最好。
Q3 自定義LayoutManager的實戰場景多嗎?
A3:實戰場景還是相當有限的。系統自帶的三個LayoutManager已經很夠用,滿足絕大部分需求。
我個人從學習自定義LayoutManager至今的收獲 ,大部分是對RecyclerView機制的了解進一步加深,也會伴随一定量的源碼閱讀經驗提升。随沒有我想象中的提升巨大生産力的趕腳,因為很多時候,産品設計要求的布局,現有方案已經可以很好解決。
但是它值得學習。
Q4 自定義一個LayoutManager就自動複用ItemView了嗎?
A4:不是,實際上這是自定義LayoutManager的重頭戲之一,要做到在合适的時機回收 不可見的舊子View ,複用子View layout 新的子View,以及Q2提及的在LayoutManager的初始化時合理布局可見數量的子View等,才算是複用了ItemView。
注意,這裡的回收是recycle,而不是detach。
如果你隻detach了ItemView,并沒有recycle它們,它們會一直被儲存在Recycler的
mAttachedScrap
裡,它是一個ArrayList,儲存了被detach但還沒有recycle的ViewHolder。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
(實際上Recycler内部的緩存機制遠不止一個mAttachedScrap 。)
Q5 用RecyclerView就等于ItemView複用?
A5:顯然也不是。除了Q4的因素外,這裡還有一個很大的誤區:很多人認為使用了RecyclerView,ItemView就都回收複用了。
這裡出個題:基本上APP都有個TopBanner在,它放在RecyclerView裡作為HeaderView(通過特殊的ItemViewType實作),剩下都是普通的ItemView,那麼清單滾動,當Banner早已不可見時,它的View(ViewHolder)會被回收、被其他ItemView複用嗎?
如下圖:
答案:Banner的ViewHolder 會被回收,但該ViewHolder的記憶體空間 不會被釋放 , 不會被其他的ItemView複用。
回收都好了解,在螢幕上不可見時,LayoutManager會把它回收至RecyclerViewPool裡。
然而卻不會給normalItem複用,因為它們的ItemViewType不同。
是以它的記憶體空間不會被釋放,将一直被RecyclerViewPool持有着,等待着需求相同ItemViewType的ViewHolder的請求到來。
即,當頁面滾動回頂部,顯示Banner時,這個View會被複用。
先說為什麼,再說如何去驗證。
為什麼?
這涉及到Recycler、RecyclerViewPool的知識,(小安利,我在http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章的第四節裡對RecyclerViewPool的源碼進行過全解,不過大家也可以自己去檢視,源碼很短。)
在LayoutManager裡,擷取childView是通過如下方法得到:
該方法内部,先通過position去擷取是否有detach掉的scrapView(ViewHolder),
如果沒有則根據position去擷取itemViewType,
final int type = mAdapter.getItemViewType(offsetPosition);
根據itemViewType擷取在RecyclerViewPool裡是否有該ViewHolder,
這裡由于我們的Banner的viewType和normalItem的viewType不一樣,即使Banner被回收進了RecyclerViewPool,但是由于itemViewtype和普通的ItemView不同,它也無法被取出、進而複用,(發散一下,另外一點,它也無法被釋放,被強引用在記憶體裡,http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章有詳細分析)。
再往下由于holder還是空的,最終便會調用Adapter的onCreateViewHolder()方法create一個新的ViewHolder。
驗證:
感興趣的人去重寫任意Adapter的
getItemViewType()
方法:
@Override
public int getItemViewType(int position) {
return position;
}
這樣每一個ItemViewType都不一樣,RecyclerView不會有任何的複用,因為每一個ItemView在RecyclerViewPool裡都找不到可以複用的holder,ItemView有n個,onCreateViewHolder方法會執行n次。
看到這裡就能回答Q2一的問題:
因為在初始化時,Recycler(scrapCache)和RecyclerViewPool裡的緩存都是空的,是以此時得到的ViewHolder都是通過onCreateViewHolder(),new 出的ViewHolder。如果此時get了整個itemCount數量的View,那麼也會new出itemCount數量的ViewHolder,此時這些ViewHolder都存在記憶體裡,和普通ViewGroup毫無分别,也更容易OOM。
Q6 RecyclerView的緩存機制簡述
A6: 上面BB了這麼多,涉及到Recycler、RecyclerViewPool以及scrap,detach,remove,recycle等概念。
這張圖摘自(http://kymjs.com/code/2016/07/10/01),源頭應該是Google官方的視訊裡。
我了解圖上的cache是被detach掉的ViewHolder存放的區域,即scrapCache區域。
這個區域由
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
這三個ArrayList組成。
而被remove掉的ViewHolder會按照ViewType分組被存放在RecyclerViewPool裡,預設最大緩存每組(ViewType)5個。
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
Q7 detach 和recycle的時機。
一個View隻是暫時被清除掉,稍後立刻就要用到,使用detach。它會被緩存進scrapCache的區域。
一個View 不再顯示在螢幕上,需要被清除掉,并且下次再顯示它的時機目前未知 ,使用remove。它會被以viewType分組,緩存進RecyclerViewPool裡。
注意:一個View隻被detach,沒有被recycle的話,不會放進RecyclerViewPool裡,會一直存在recycler的scrap 中。網上有人的Demo就是如此,是以View也沒有被複用,有多少ItemCount,就會new出多少個ViewHolder。
Q8 初始化時,onLayoutChildren()為什麼會執行兩次?
答 :參看RecyclerView源碼,onLayoutChildren 會執行兩次,一次RecyclerView的onMeasure() 一次onLayout()。
李菊福:RecyclerView的onMeasure(),會調用
dispatchLayoutStep2()
方法,該方法内部會調用
mLayout.onLayoutChildren(mRecycler, mState);
,這是第一次。如下:
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
......
dispatchLayoutStep2();
......
}
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
.....
mLayout.onLayoutChildren(mRecycler, mState);
.....
}
onLayout()方法會調用
dispatchLayout();
,該方法内部又調用了
dispatchLayoutStep2();
,這是第二次。
Q9 基于上個問題,我們要注意什麼?
答:即使是在寫onLayoutChildren()方法時,也要考慮将螢幕上的View(如果有),detach掉,否則螢幕初始化時,同一個position的ViewHolder,也會onCreateViewHolder兩次。是以childCount也會翻倍。
最後也是最重要的
LayoutManager API 支援強大且複雜的布局回收,正因為它API強大,是以我們需要實作大量的代碼才能完成功能。不要過度封裝、過度優化你的代碼,隻要能完成你的需求即可。(當然最基本的要求:ViewHolder複用 要滿足)
原話如下:
文章連結:http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
該文章是我見過學習自定義LayoutManager最好的資料。
二 常用API:
布局API:
//找recycler要一個childItemView,我們不管它是從scrap裡取,還是從RecyclerViewPool裡取,亦或是onCreateViewHolder裡拿。
View view = recycler.getViewForPosition(xxx); //擷取postion為xxx的View
addView(view);//将View添加至RecyclerView中,
addView(child, );//将View添加至RecyclerView中,childIndex為0,但是View的位置還是由layout的位置決定,該方法在逆序layout子View時有大用
//将ViewLayout出來,顯示在螢幕上,内部會自動追加上該View的ItemDecoration和Margin。此時我們的View已經可見了
layoutDecoratedWithMargins(view, leftOffset, topOffset,
leftOffset + getDecoratedMeasuredWidth(view),
topOffset + getDecoratedMeasuredHeight(view));
回收API:
detachAndScrapAttachedViews(recycler);//detach輕量回收所有View
detachAndScrapView(view, recycler);//detach輕量回收指定View
// recycle真的回收一個View ,該View再次回來需要執行onBindViewHolder方法
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler);
detachView(view);//超級輕量回收一個View,馬上就要添加回來
attachView(view);//将上個方法detach的View attach回來
recycler.recycleView(viewCache.valueAt(i));//detachView 後 沒有attachView的話 就要真的回收掉他們
移動子ViewAPI:
offsetChildrenVertical(-dy); // 豎直平移容器内的item
offsetChildrenHorizontal(-dx);//水準平移容器内的item
工具API:
public int getPosition(View view)//擷取某個view 的 layoutPosition,很有用的方法,卻鮮(沒)有文章提及,是我翻看源碼找到的。
//以下方法會我們考慮ItemDecoration的存在,但部分函數沒有考慮margin的存在
getDecoratedLeft(view)=view.getLeft()
getDecoratedTop(view)=view.getTop()
getDecoratedRight(view)=view.getRight()
getDecoratedBottom(view)=view.getBottom()
getDecoratedMeasuredHeight(view)=view.getMeasuredWidth()
getDecoratedMeasuredHeight(view)=view.getMeasuredHeight()
//由于上述方法沒有考慮margin的存在,是以我參考LinearLayoutManager的源碼:
/**
* 擷取某個childView在水準方向所占的空間
*
* @param view
* @return
*/
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
/**
* 擷取某個childView在豎直方向所占的空間
*
* @param view
* @return
*/
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}