本文源自 Yig?it Boyar 在 360|AnDev 的演講,建議有空看下,絕對會有收獲:Yotube | Article | Slide,這里 Boyar 從整體架構(gòu)講述了 RecyclerView : 它是如何工作,有哪些優(yōu)先注意事項,以及你該如何充分利用它們。
Introduction
RecylerView 有三種主要組件:
- LayoutManager 負(fù)責(zé)定位 views
- Item Animator 負(fù)責(zé)管理動畫
- Adapter 負(fù)責(zé)處理 views
在后續(xù)優(yōu)化 RecyclerView 的過程中,我們并沒有添加新的 API,而是加入新的組件,比如控制 Item drag,drop 和 swipe 動作的 ItemTouchHelper;以及控制 snap 動作的 SnapHelper。
I. 視圖更新 View Updates
首先要討論 View::requestLayout。這與 RecyclerView 無關(guān),它是 Android 視圖系統(tǒng)的一部分。

當(dāng)你對某個 view 做了一些修改,這個 view 會向上層的 viewGroup 說:“我現(xiàn)在需要 request layout,因為我發(fā)生了變化?!比缓筮@句話會向上冒泡直到 根布局( root layout ) 回復(fù):“?? ,我在下個 布局幀( layout frame) 的時候通知你。”
下個幀開始時,根布局會通知所有的子 view:“重新 measure 自己,這是你們重新獲得 layout 的時刻?!泵總€子 view 將會遞歸地 measure 它們自身。現(xiàn)在,假如沒有 view 請求布局,所有的這些 measure 的尺寸將會被緩存。
現(xiàn)在所有的 view 的層次結(jié)構(gòu)趨于穩(wěn)定,這對于 RecyclerView 來說意味什么呢。
假如 Adapter::onBindViewHolder 有如下的邏輯:
onBindViewHolder(ViewHolder holder, int position) {
...
imageLoader.loadImage(holder.imageView, ImgUrl, R.drawable.placeHolder);
}
ImageLoader 異步地從網(wǎng)絡(luò)上下載圖片資源,將其轉(zhuǎn)化為 bitmap,然后調(diào)用 ImageView,設(shè)置 image bitmap。
上述步驟發(fā)生時,ImageView 會說:“我原來的數(shù)據(jù)已經(jīng)無效了,讓我請求一次布局吧”。消息被傳遞到 imageView 的父布局 itemView,itemView 會說:“?? ,看來我的子 view 已經(jīng)無效了,我來請求布局吧”。最終消息被傳遞到 RecyclerView,RecyclerView 會復(fù)位所有的子 view ,這是一個很 expensive 的操作。
還記得 RecyclerView::setHasFixedSize 嗎,可以在這里使用。如果 RecyclerView 擁有固定的尺寸,它知道不需要復(fù)位子 view,也就不會請求布局。

然而 RecyclerView::setHasFixedSize 僅會使 RecyclerView 不會調(diào)用 requestLayout();對于其子 view ,還是會經(jīng)歷完整的 requestLayout() 過程,這同樣是個 expensive 的操作。
好消息是從 2011 年開始,ImageView 的繪制邏輯做了一些優(yōu)化:
// ImageView.java Since 2011
void setImageDrawable(Drawable drawable) {
if (mDrawable != drawable) {
int oldWidth = mDrawableWidth;
int oldHeight = mDrawableHeight;
updateDrawable(drawable);
// 只有在 Drawable 尺寸變化時才請求布局
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
更新 Drawable 時,它會檢查 Drawable 大小是否有變化,如果不變,則不會調(diào)用 requestLayout(),只調(diào)用 invalidate(),invalidate 會在原位置上重新繪制視圖,這是一個 cheap 的操作。
對于 TextView 則不是這樣,即使你兩次設(shè)置相同的字符串,TextView 依然會調(diào)用 requestLayout()。

我們加載一些圖片,假如其中某張圖片不在內(nèi)存中會發(fā)生什么?在圖片加載完成之前,你添加了 placeholder。布局看起來很穩(wěn)健,但實際上并不是這樣。

理想狀態(tài)下 The Tall Bird 未加載完,placeholder 空白展示,The Watch 始終位于左側(cè),但實際上 StaggeredGridLayoutManager 會將 The Watch 置于右側(cè),因為 item 檢測到其 ImageView 的 height 為 0,其余的 item 都會依據(jù)上層的 item 安置自己 ImageView 。
解決這種問題可以通過自定義 ImageView:
// AspectRatioImageView.java
private float mAspectRatio;
@Override
protected void onMeasure(int wSpec, int hSpec) {
int width = MeasureSpec.getSize(wSpec);
int height = (int) (width * mAspectRatio);
setMeasureDimension(width, height);
}
自定義的 AspectRatioImageView 可以根據(jù)已知的 寬高比(aspect ratio) 調(diào)整自己的 size,然后復(fù)寫 onMeasure 方法,設(shè)置 measure 后的尺寸。依據(jù)之前提到的 setImageDrawable 機制,當(dāng)我們獲取到實際的圖片后,它并不會調(diào)用 requestLayout,而是直接更新 placeholder。
那么問題來了,如何設(shè)置寬高比呢?假設(shè)后端的 API 是這樣:
{
"user" : {
"name" : "Michael",
"photoUrl" : "https://..."
}
}
這無法解決我們的問題,如果 API 能提供圖片的 metadata 這樣會不會更好:
{
"user" : {
"name" : "Michael",
"photoUrl" : {
"width" : 300,
"height" : 500,
"url" : "https://...",
"palette" : {}
}
}
API 提供了寬和高,這樣我們方便計算寬高比,同時我們可以在下載實際圖片之前根據(jù)寬高比及 palette 設(shè)置好合適的 placeholder。
II. 數(shù)據(jù)更新 Data Updates
假設(shè)我們從服務(wù)端獲取到一組新的新聞數(shù)據(jù),然后刷新列表:
直接使用 Adapter::notifyDataSetChanged 會存在如下問題:
- RecyclerView 更新數(shù)據(jù)集時并不知道到底數(shù)據(jù)集的哪里發(fā)生了改變,于是只能非常低效地從位置 0 開始,重新綁定一遍數(shù)據(jù)。
解決這種問題的方法也比較簡單,使用 Adapter::getItemId:
long getItemId(int position) {
news.get(position).getId();
}
現(xiàn)在 RecyclerView 知道每個位置對應(yīng)的 ID,可以根據(jù) ID 判斷哪個位置該去綁定數(shù)據(jù),然而當(dāng)你使用 Adapter::notifyDataSetChanged 時,它同樣需要對其他的 item 進行多余的 measure 和 layout。針對這種場景,我們提供了 SortedList。
SortedList
通過簡單的幾個方法實現(xiàn)了所有元素的排序,并且有內(nèi)置的邏輯提供 RecyclerView 的數(shù)據(jù)更新。
SortedList<Item> mSortedList = new SortedList<>(Item.class, new SortedListAdapterCallback<Item>(mAdapter)) {
@Override
public int compare(Item oldItem, Item newItem) {
return oldItem.id - newItem.id;
}
@Override
public boolean areItemsTheSame(Item oldItem, Item newItem) {
return oldItem.id == newItem.id;
}
@Override
public boolean areContentsTheSame(Item oldItem, Item newItem) {
return oldItem.text.equals(newItem.text);
}
});
通過使用 SortedList,我們可以將服務(wù)端獲取新的數(shù)據(jù)直接添加到 SortedList 上,它會為我們通知 adapter 數(shù)據(jù)更新:
void onFetched(List<News> newsList) {
mSortedList.addAll(newsList);
}
SortedList 為我們添加 Item 提供了便捷的方法,但是假如我們獲取的新的 Item 數(shù)據(jù),是舊的 Item 的某項數(shù)值發(fā)生了變化,比如(add("To Rx or Not To Rx", 9)):
我們修改 compare 方法,根據(jù) votes 數(shù)值大小排列數(shù)據(jù):
SortedList<Item> mSortedList = new SortedList<>(Item.class, new SortedListAdapterCallback<Item>(mAdapter)) {
@Override
public int compare(Item oldItem, Item newItem) {
return newItem.votes - oldItem.votes;
}
...
});
得到的排序是這樣的:
當(dāng)你插入某項數(shù)據(jù)到 SortedList 中時,它會簡單的做二分查找。從中間的 item 開始,判斷是向上還是向下比較,因此無法判斷原先的 item 是否已經(jīng)存在【數(shù)據(jù)集】中。
如何解決這種問題呢?SortedList 提供了 SortedList::updateItemAt 方法:
Map<Integer, Item> items;
void inset(Item) {
Item existing = items.put(item.id, item);
if (existing == null) {
mSortedList.add(item);
} else {
int index = mSortedList.indexOf(existing);
mSortedList.updateItemAt(index, item);
}
}
SortedList 會對插入的 Item 進行判斷,假如存在數(shù)據(jù)集中,進行更新;假如不存在,直接添加。這個效果不錯,不過我們提供了更便捷的工具: DiffUtil。
DiffUtil
先看看使用的 API:
DiffResult result = DiffUtil.calculateDiff(new MyDiffCallback(oldList, newList));
mAdapter.setItems(newList);
result.dispatchUpdateTo(mAdapter);
DiffUtil::calculateDiff 接收 DiffUtil.Callback 回調(diào)方法,返回 DiffResult,設(shè)置新的數(shù)據(jù)集,然后調(diào)用DiffResult::dispatchUpdateTo 方法更新數(shù)據(jù)。
DiffUtil.Callback 與 SortedListAdapterCallback 實現(xiàn)的方法比較類似:
class MyCallback extends DiffUtil.Callback {
@Override
public int getOldListSize() {
return mOld.size();
}
@Override
public int getNewListSize() {
return mNew.size();
}
// 判斷兩個數(shù)據(jù)集是否相同
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOld.get(oldItemPosition).id == mNew.get(newItemPosition).id;
}
// 判斷兩個數(shù)據(jù)集中的元素是否相同
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return mOld.get(oldItemPosition).equals(mNew.get(newItemPosition));
}
}
III. 資源管理 Resource management
RecyclerView 的生命周期:
ViewHolder 通過 onCreate 方法創(chuàng)建,然后綁定到某個位置,綁定后可以認(rèn)為 ViewHolder 快速 attach 到視圖中,接收到 onViewAttachedToWindow 回調(diào)后,ViewHolder 出現(xiàn)在屏幕中,對用戶可見。
當(dāng)用戶滑動或其他操作時,item 也許被 detach,比如當(dāng)你把某個 item 滑動到屏幕之外時,Layout Manager 決定移除這個條目,這個時候會收到 onViewDetachedFromWindow 回調(diào)。
例如我們有一個視頻訂閱列表,滑動列表,當(dāng)視頻滑出屏幕時停止播放,滑入屏幕時開始播放,這是一個很好的方式去改善視頻隱藏和出現(xiàn)時的用戶體驗。
RecyclerView 是異步的
RecyclerView 是異步的是什么意思?
這里并不是表示 RecyclerView 是多線程的,這里表示的是它處理事務(wù)是異步的。
當(dāng)新幀出現(xiàn)時,RecyclerView 會向 View 應(yīng)用所有待處理的修改并進行視圖更新。
例如你調(diào)用 scrollToPosition(15)。當(dāng)下個幀出現(xiàn)時,RecyclerView 會應(yīng)用這些修改:
recyclerView.scrollToPosition(15);
int x = layoutManager.getFirstVisibleItemPosition();
然后你通過 LayoutManager 獲取第一個可見的位置,它會返回15嗎?實際上不會,RecyclerView 并不會立即執(zhí)行滑動,而是等待下一幀開始才去執(zhí)行。
void onCreate(SavedInstanceState state) {
...
// 兩個都是在下一幀執(zhí)行,所以它們看上去像是同步的。
mRecyclerView.scrollToPosition(selectedPosition);
mRecyclerView.setAdapter(mAdapter);
}
下面的方法也會執(zhí)行:
void onCreate(SavedInstanceState state) {
...
mRecyclerView.scrollToPosition(selectedPosition);
model.loadItems(items ->
mRecyclerView.setAdapter(
new ItemAdapter(items));
);
}
為什么在下一幀開始時,我們還沒有 adapter,設(shè)置的滑動還能奏效呢?
原因其實很簡單,當(dāng)你有一個 RecyclerView,在沒設(shè)置 adapter 和 Layout Manager 之前,它將忽略所有的 layout 調(diào)用。
ViewHolder艸
填充數(shù)據(jù)的最佳實踐:
class ViewHolder {
...
// ViewHolder 代表一個 item,更加抽象,便于移植到 Presenter 等位置。
public bindTo(Item item, ImageLoader loader) {
titleView.setText(item.getTitle());
bodyView.setText(item.getBody());
loader.loadImage(iconView, item.getIconUrl());
}
}
void onBindViewHolder(ViewHolder holder, int position) {
holder.bindTo(items.get(position), mImageLoader);
}
ViewType 的最佳實踐:
在 RecyclerView::getItemViewType 方法直接返回 layout:
@Override
public int getItemViewType(int position) {
User user = mItems.get(position);
if (user.isPremium()) {
return R.layout.premium;
}
return R.layout.basic;
}
生成 RecyclerView::ViewHolder:
public RecyclerView.ViewHoder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mLayoutInflater.inflate(viewType, parent, false);
return new ViewHolder(view);
}
Item Click Listener 的最佳實踐:
class MyAdapter {
interface ItemClickListener {
void onClick(Item item);
}
ItemClickListener itemClickListener;
void setItemClickListener(ItemClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
public onCreateViewHolder(...) {
final ViewHolder holder = ...;
holder.itemView.setOnClickListener({
// 獲取 view holder 在 adapter 中的位置
int position = holder.getAdapterPosition();
if (position != NO_POSITION) {
itemClickListener.onClick(items[position]);
}
});
}
}