Android AsyncListDiffer-RecyclerView最好的夥伴

自Android5.0以來,RecyclerView漸漸取代ListView成為Android開發中使用最多的列表控制元件,對於RecyclerView的使用相信大家都不陌生,但對於RecyclerView的高效重新整理,卻是很多人不知道的。
簡單粗暴的重新整理方式
Adapter.notifyDataSetChanged();
這種方式想必是大家曾經用的最多的一種重新整理Adapter的方式,它的缺點很明顯:
- 無腦重新整理整個RecyclerView可視區域,每個item重繪,如果你的onBindViewHolder邏輯處理稍微複雜一些,則容易造成卡頓
- 無法觸發RecyclerView的item動畫,使用者體驗極差。
區域性重新整理方式
為了解決上述問題,RecyclerView推出了局部重新整理的方式
Adapter.notifyItemChanged(int) Adapter.notifyItemInserted(int) Adapter.notifyItemRangeChanged(int, int) Adapter.notifyItemRangeInserted(int, int) Adapter.notifyItemRangeRemoved(int, int)
區域性重新整理只會重新整理指定position的item,這樣完美解決了上述簡單粗暴重新整理方式的缺點,但是:
- 區域性重新整理需要指定item的position,如果你只更新了一條資料,那麼你可以很容易知道position位置,但是如果你更新的是整個列表,你需要計算出所有你需要重新整理的position,那麼這將是一場災難
DiffUtil
Google似乎也注意到了這一點,因此在 support-recyclerview-v7:24.2.0 中,推出了一個用於計算哪些位置需要重新整理的工具類: DiffUtil。
使用 DiffUtil ,有3個步驟
1.自實現DiffUtil.callback
private DiffUtil.Callback diffCallback = new DiffUtil.Callback() { @Override public int getOldListSize() { // 返回舊資料的長度 return oldList == null ? 0 : oldList.size(); } @Override public int getNewListSize() { // 返回新資料的長度 return newList == null ? 0 : newList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { // 返回兩個item是否相同 // 例如:此處兩個item的資料實體是User類,所以以id作為兩個item是否相同的依據 // 即此處返回兩個user的id是否相同 return TextUtils.equals(oldList.get(oldItemPosition).getId(), newList.get(oldItemPosition).getId()); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { // 當areItemsTheSame返回true時,我們還需要判斷兩個item的內容是否相同 // 此處以User的age作為兩個item內容是否相同的依據 // 即返回兩個user的age是否相同 return oldList.get(oldItemPosition).getAge() == newList.get(newItemPosition).getAge(); } };
2.計算得到DiffResult
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
3.將DiffResult設定給Adapter
// 注意此處一定要將新資料設定給Adapter // 否則會造成ui重新整理了但資料未更新的bug mAdapter.setData(newList); diffResult.dispatchUpdatesTo(mAdapter);
這樣我們就實現了局部重新整理位置的計算和區域性重新整理的實現,相比notifyDataSetChanged(),效能大大提高。
本文到此結束?
不不不,還早著呢,咱們理智分析一下:
- 首先 DiffUtil.calculateDiff() 這個方法是執行在主執行緒的,如果新舊資料List比較大,那麼這個方法鐵定是會阻塞主執行緒的
- 計算出DiffResult後,咱們必須要將新資料設定給Adapter,然後才能呼叫 DiffResult.dispatchUpdatesTo(Adapter) 重新整理ui,然而很多人都會忘記這一步。
AsyncListDiff
DiffUtil已經很好用了,但是有上述兩個問題,想必Google的工程師也是看不下去的,雖然上述兩個問題不難解決,但是很容易遺漏。
因此Google又推出了一個新的類 AsyncListDiff
先來看一波 AsyncListDiff 的使用方式:
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHodler> { private AsyncListDiffer<User> mDiffer; private DiffUtil.ItemCallback<User> diffCallback = new DiffUtil.ItemCallback<User>() { @Override public boolean areItemsTheSame(User oldItem, User newItem) { return TextUtils.equals(oldItem.getId(), newItem.getId()); } @Override public boolean areContentsTheSame(User oldItem, User newItem) { return oldItem.getAge() == newItem.getAge(); } }; public UserAdapter() { mDiffer = new AsyncListDiffer<>(this, diffCallback); } @Override public int getItemCount() { return mDiffer.getCurrentList().size(); } public void submitList(List<User> data) { mDiffer.submitList(data); } public User getItem(int position) { return mDiffer.getCurrentList().get(position); } @NonNull @Override public UserAdapter.UserViewHodler onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_user_list, parent, false); return new UserViewHodler(itemView); } @Override public void onBindViewHolder(@NonNull UserAdapter.UserViewHodler holder, int position) { holder.setData(getItem(position)); } class UserViewHodler extends RecyclerView.ViewHolder { private TextView tvName; private TextView tvAge; public UserViewHodler(View itemView) { super(itemView); tvName = itemView.findViewById(R.id.tv_name); tvAge = itemView.findViewById(R.id.tv_age); } public void setData(User data) { tvName.setText(data.getName()); tvAge.setText(String.valueOf(data.getAge())); } } }
這裡使用了一個簡單的Adapter例子,不做封裝,是為了更好地說明 AsyncListDiffer 。
不難看出, AsyncListDiffer 的使用步驟:
- 自實現 DiffUtil.ItemCallback ,給出item差異性計算條件
- 將所有對資料的操作代理給 AsyncListDiffer ,可以看到這個Adapter是沒有List資料的
- 使用 submitList() 更新資料,並重新整理ui
ok,咱們看一下效果:
首先我們給Adapter設定資料
List<User> users = new ArrayList<>(); for (int i = 0; i < 10; i++) { users.add(new User(String.valueOf(i), "使用者" + i, i + 20)); } mAdapter.submitList(users);
然後修改資料
List<User> users = new ArrayList<>(); for (int i = 0; i < 10; i++) { users.add(new User(String.valueOf(i), "使用者" + i, i % 3 == 0 ? i + 10: i + 20)); } mAdapter.submitList(users);
跑起來看一哈


ok,我們看到只有被3整除的position被重新整理了,完美的區域性重新整理。
那麼問題來了, AsyncListDiffer 是如何解決我們上述的兩個問題的呢?
解惑
我們走進 AsyncListDiffer 的原始碼看一下:
public class AsyncListDiffer<T> { private final ListUpdateCallback mUpdateCallback; private final AsyncDifferConfig<T> mConfig; public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter, @NonNull DiffUtil.ItemCallback<T> diffCallback) { mUpdateCallback = new AdapterListUpdateCallback(adapter); mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build(); } private List<T> mList; private List<T> mReadOnlyList = Collections.emptyList(); private int mMaxScheduledGeneration; public List<T> getCurrentList() { return mReadOnlyList; } public void submitList(final List<T> newList) { if (newList == mList) { // 如果新舊資料相同,則啥事不做 return; } // 用於控制計算執行緒,防止在上一次submitList未完成時, // 又多次呼叫submitList,這裡只返回最後一個計算的DiffResult final int runGeneration = ++mMaxScheduledGeneration; if (newList == null) { // 如果新資料集為空,此種情況不需要計算diff // 直接清空資料即可 // 通知item remove mUpdateCallback.onRemoved(0, mList.size()); mList = null; mReadOnlyList = Collections.emptyList(); return; } if (mList == null) { // 如果舊資料集為空,此種情況不需要計算diff // 直接將新資料新增到舊資料集即可 // 通知item insert mUpdateCallback.onInserted(0, newList.size()); mList = newList; mReadOnlyList = Collections.unmodifiableList(newList); return; } final List<T> oldList = mList; // 在子執行緒中計算DiffResult mConfig.getBackgroundThreadExecutor().execute(new Runnable() { @Override public void run() { final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { return oldList.size(); } @Override public int getNewListSize() { return newList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return mConfig.getDiffCallback().areItemsTheSame( oldList.get(oldItemPosition), newList.get(newItemPosition)); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return mConfig.getDiffCallback().areContentsTheSame( oldList.get(oldItemPosition), newList.get(newItemPosition)); } }); // 在主執行緒中更新資料 mConfig.getMainThreadExecutor().execute(new Runnable() { @Override public void run() { if (mMaxScheduledGeneration == runGeneration) { latchList(newList, result); } } }); } }); } private void latchList(@NonNull List<T> newList, @NonNull DiffUtil.DiffResult diffResult) { diffResult.dispatchUpdatesTo(mUpdateCallback); mList = newList; mReadOnlyList = Collections.unmodifiableList(newList); } }
執行緒部分原始碼:
private static class MainThreadExecutor implements Executor { final Handler mHandler = new Handler(Looper.getMainLooper()); @Override public void execute(@NonNull Runnable command) { mHandler.post(command); } } @NonNull public AsyncDifferConfig<T> build() { if (mMainThreadExecutor == null) { mMainThreadExecutor = sMainThreadExecutor; } if (mBackgroundThreadExecutor == null) { synchronized (sExecutorLock) { if (sDiffExecutor == null) { sDiffExecutor = Executors.newFixedThreadPool(2); } } mBackgroundThreadExecutor = sDiffExecutor; } return new AsyncDifferConfig<>( mMainThreadExecutor, mBackgroundThreadExecutor, mDiffCallback); }
ui重新整理部分原始碼:
public final class AdapterListUpdateCallback implements ListUpdateCallback { @NonNull private final RecyclerView.Adapter mAdapter; public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) { mAdapter = adapter; } @Override public void onInserted(int position, int count) { mAdapter.notifyItemRangeInserted(position, count); } @Override public void onRemoved(int position, int count) { mAdapter.notifyItemRangeRemoved(position, count); } @Override public void onMoved(int fromPosition, int toPosition) { mAdapter.notifyItemMoved(fromPosition, toPosition); } @Override public void onChanged(int position, int count, Object payload) { mAdapter.notifyItemRangeChanged(position, count, payload); } }
原始碼實現很簡單,總結一下:
- 首先排除新舊資料為空的情況,這種情況不需要計算diff
- 在子執行緒中計算 DiffResult ,在主執行緒將 DiffResult 設定給Adapter,解決主執行緒阻塞問題
- 將Adapter的資料代理給 AsyncListDiffer ,解決Adapter與 DiffUtil 的資料一致性問題
完結,撒花
喜歡這篇文章記得給我一個小心心哦