RecyclerView中 各種節(jié)能刷新 和重點(diǎn)DiffUtil

你瞅啥贡避?

RecyclerView的刷新基本分為以下兩種情況:

  • 1. 如果大量的數(shù)據(jù)被修改或者被修改數(shù)據(jù)的位置不確定继准,這個方法很消耗性能,不到萬不得已不要使用棘街,請盡量使用下面的刷新方法蟆盐。實(shí)現(xiàn)如下:
adapter.notifyDataSetChanged();
  • 2. 刷新某一項(xiàng),定點(diǎn)刷新(常用)遭殉,消耗性能很少石挂,但是會有定位的問題,可能在過程中需要遍歷集合獲取操作下標(biāo)
//刷新某Item中的所有組件
adapter.notifyItemChanged(position);
//刷新某Item中的部分組件
adapter.notifyItemChanged(position, payloads);
//插入Item
adapter.notifyItemInserted(position);       
//刪除Item
adapter.notifyItemRemoved(position);  
//移動Item
adapter.notifyItemMoved(position, position + 1);

RecyclerView的刷新問題Google推出了DiffUtil這個解決方案:

  • DiffUtil的運(yùn)用邏輯非常簡單险污,大致如下:
    實(shí)現(xiàn)對比新舊數(shù)據(jù)的方法(類似比較器)痹愚,這樣DiffUtil便知道當(dāng)新數(shù)據(jù)來臨時,該不該更新某個item蛔糯。
    更新數(shù)據(jù)時拯腮,把新舊數(shù)據(jù)丟給DiffUtil,底層會根據(jù)你實(shí)現(xiàn)的對比方法蚁飒,利用一種差分算法自動計算出差異动壤,最后局部更新到UI。
  • DiffUtil的使用也很簡單:
    1淮逻、先實(shí)現(xiàn)比較新舊數(shù)據(jù)的回調(diào)琼懊,可以是一個獨(dú)立的類,也可以寫成Adapter的內(nèi)部類:
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
    // ...

    private class DiffCallback extends DiffUtil.Callback {
        private List<T> oldData, newData;

        DiffCallback(List<T> oldData, List<T> newData) {
            this.oldData = oldData;
            this.newData = newData;
        }

        @Override
        public int getOldListSize() {
            return oldData.size();
        }

        @Override
        public int getNewListSize() {
            return newData.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            T oldT = oldData.get(oldItemPosition);
            T newT = newData.get(newItemPosition);
            // 實(shí)際情況最好是在此處對比新舊數(shù)據(jù)的id(比如用戶uid)弦蹂,這里為了方便示例直接equals對象了
            // 若此處返回true肩碟,則DiffUtil會再調(diào)用下面的areContentsTheSame方法,進(jìn)一步對比UI是否有變化
            // 若此處返回false凸椿,則說明id都不同削祈,肯定不是一個item
            return Objects.equals(oldT, newT);
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            // TODO 比較新舊數(shù)據(jù)(主要是UI展示內(nèi)容)是否相同,這里為了方便示例直接返回true
            return true;
        }
    }
}

2脑漫、然后在Adapter內(nèi)部實(shí)現(xiàn)一個update數(shù)據(jù)的方法:

    @Override
    public void updateData(List<T> newData) {
        DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffCallback(getData(), newData));
        // 這里的getData即表示獲取整個列表的數(shù)據(jù)髓抑,自行實(shí)現(xiàn)即可
        getData().clear();
        getData().addAll(newData);
        result.dispatchUpdatesTo(this);
    }

3、重點(diǎn)還是 areItemsTheSame 和 areContentsTheSame 方法优幸,后者大部分時候只需要對比每個item上UI展示出來的數(shù)據(jù)即可吨拍,因?yàn)橛脩糁魂P(guān)心眼見的內(nèi)容。

解決使用后產(chǎn)生的問題:
我們會發(fā)現(xiàn)在上面的使用示例中网杆,updateData 方法內(nèi)部對原數(shù)據(jù)進(jìn)行了清除和添加的操作羹饰,這會導(dǎo)致一個問題便是:列表數(shù)據(jù)集合中的對象已經(jīng)變了伊滋,即使其某項(xiàng)對應(yīng)的UI內(nèi)容沒有發(fā)生變化。
舉個例子队秩,一個通訊錄列表里面有 [小明, 小紅] 兩個人笑旺,對應(yīng)內(nèi)存地址為 [a1, a2],現(xiàn)在通過上述 updateData 方法更新了通訊錄列表馍资,UI內(nèi)容變成了 [小王, 小紅]筒主,對應(yīng)內(nèi)存地址為 [b1, b2]。對用戶來說小紅這個item看上去沒有發(fā)生變化鸟蟹,但其實(shí)對應(yīng)的數(shù)據(jù)類對象已經(jīng)不同乌妙。而且此時 onBindViewHolder 方法只會觸發(fā)一次,將小明更新成小王建钥,而不會觸發(fā)小紅那個position對應(yīng)的 onBindViewHolder 藤韵。
上述細(xì)節(jié)很關(guān)鍵,如果開發(fā)過程中綁定(bind)數(shù)據(jù)不恰當(dāng)?shù)脑捫芫腿菀自斐筛鞣N奇異問題荠察,比如網(wǎng)上資料最多的DiffUtil導(dǎo)致item點(diǎn)擊事件數(shù)據(jù)錯位問題、數(shù)組越界崩潰問題等等奈搜。
這里的“不恰當(dāng)”,絕大部分情況下盯荤,總結(jié)出來:其實(shí)指的就是在 onBindViewHolder 方法中持有了某個位置(position)對應(yīng)數(shù)據(jù)的不可變對象馋吗。最常見的誤用示例就是在 onBindViewHolder 中設(shè)置某些控件的點(diǎn)擊事件并引用數(shù)據(jù)對象:

    // 此處假設(shè)item的數(shù)據(jù)類為User
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        MyItemViewHolder h = (MyItemViewHolder) holder;
        User user = getData().get(position);
        h.mNameTextView.setOnClickListener(v -> {
            // 第2種寫法:User user = getData().get(position);
            // 假設(shè)這里是點(diǎn)擊item跳轉(zhuǎn)到該User對應(yīng)的個人主頁界面
            startWebView(user.getHomePageUrl());
        });
    }

在不接入DiffUtil之前,上面這段代碼沒有任何問題秋秤,因?yàn)槲覀兌际鞘褂?notifyDataSetChanged 方法來更新UI宏粤,每次更新調(diào)用到 onBindViewHolder 時,點(diǎn)擊事件都會重新設(shè)置灼卢,get出來的user對象自然也是最新的绍哎。一旦我們使用了DiffUtil,就會出問題了鞋真。
回到上面小王綠了小明的例子崇堰,在我們的 updateData 方法執(zhí)行后,如果我們只對比了user的名字這個屬性(其實(shí)也只需要對比這個屬性)涩咖,那么小紅那一個item就不會觸發(fā)對應(yīng)的 onBindViewHolder 海诲,即小紅的點(diǎn)擊事件回調(diào)里,仍然持有著舊數(shù)據(jù)集的user對象(對應(yīng)那個內(nèi)存地址a2)檩互。但實(shí)際上小紅應(yīng)該對應(yīng) b2 那個內(nèi)存了特幔,這就造成 a2 內(nèi)存無法釋放,問題是不是顯得有點(diǎn)嚴(yán)重了闸昨。
有同學(xué)說無所謂呀蚯斯,反正點(diǎn)擊事件依然有效薄风。那如果我說網(wǎng)絡(luò)數(shù)據(jù)刷新下來小紅的 homePageUrl 變了呢?是不是還得把這個屬性加入DiffUtil的對比方法中拍嵌?這樣最終會導(dǎo)致小紅的 onBindViewHolder 方法也執(zhí)行遭赂,跟 notifyDataSetChanged 豈不是沒什么兩樣了?
此外撰茎,若get對象寫成注釋中的第2種寫法嵌牺,且列表第0個位置的item被刪了呢?小紅頂上去變成了第0個龄糊,此時由于小紅的UI內(nèi)容沒變逆粹,只是位置變了,所以 onBindViewHolder 依然不會執(zhí)行炫惩。以上面的示例代碼來看僻弹,當(dāng)再次點(diǎn)擊小紅時,就會直接出現(xiàn)數(shù)組越界的異常他嚷。因?yàn)閜osition還是之前的1蹋绽,而此時小紅的position已經(jīng)為0。

顯然上述出現(xiàn)的這些問題不符合谷歌的設(shè)計初衷筋蓖,也不符合我們使用DiffUtil的初衷卸耘。其實(shí)解決辦法很簡單,就是要對 onBindViewHolder 方法有一個正確的認(rèn)知粘咖,其原則就是:

  • onBindViewHolder 只做UI內(nèi)容的更新蚣抗,如 setText,setImageXXX 等方法瓮下。做到數(shù)據(jù)對象一次性使用翰铡。
  • 不要跨作用域持有與位置(position)相關(guān)的數(shù)據(jù),比如每個item的數(shù)據(jù)對象讽坏。尤其就是避免在 onBindViewHolder 中設(shè)置點(diǎn)擊事件監(jiān)聽锭魔。

正確的點(diǎn)擊事件監(jiān)聽還是參照如下形式比較好:

// 比如這是某個Base適配器類
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
    // ...
    private View.OnClickListener mOnClickListener;
    private View.OnLongClickListener mOnLongClickListener;
    private OnItemClickListener mOnItemClickListener;

    public interface OnItemClickListener {
        void onItemClick(View view, RecyclerView.ViewHolder holder, int position);

        void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position);
    }

    public BaseXXXAdapter(Context context) {
        // ...
        mOnClickListener = v -> {
            RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
            int pos = h.getAdapterPosition();
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemClick(v, h, pos);
            }
        };
        mOnLongClickListener = v -> {
            RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
            int pos = h.getAdapterPosition();
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemLongClick(v, h, pos);
            }
            return true;
        };
    }

    public void setOnItemClickListener(OnItemClickListener clickListener) {
        this.mOnItemClickListener = clickListener;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // ...省略holder實(shí)例化
        holder.itemView.setTag(holder); // 把holder當(dāng)tag存
        holder.itemView.setOnClickListener(mOnClickListener);
        holder.itemView.setOnLongClickListener(mOnLongClickListener);
        return holder;
    }
}

// 繼承實(shí)現(xiàn)的實(shí)際業(yè)務(wù)Adapter
public class XXXAdapter extends BaseXXXAdapter<User> {
    public XXXAdapter(Context context) {
        setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) {
                MyItemViewHolder h = (MyItemViewHolder) holder;
                // 每次點(diǎn)擊都保證了為對應(yīng)位置的數(shù)據(jù),再也不用擔(dān)心數(shù)據(jù)錯位問題了
                User user = getData().get(position);
            }

            @Override
            public void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) {
                // ...
            }
        });
    }
}

參考來源:掘金 作者:針葉
參考來源:簡書 作者:BruceBug

注:參考記錄目的是為了加深印象和自己以后方便查閱

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末路呜,一起剝皮案震驚了整個濱河市迷捧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胀葱,老刑警劉巖党涕,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異巡社,居然都是意外死亡膛堤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門晌该,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肥荔,“玉大人绿渣,你說我怎么就攤上這事⊙喙ⅲ” “怎么了中符?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長誉帅。 經(jīng)常有香客問我淀散,道長,這世上最難降的妖魔是什么蚜锨? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任档插,我火速辦了婚禮,結(jié)果婚禮上亚再,老公的妹妹穿的比我還像新娘郭膛。我一直安慰自己,他們只是感情好氛悬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布则剃。 她就那樣靜靜地躺著,像睡著了一般如捅。 火紅的嫁衣襯著肌膚如雪棍现。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天镜遣,我揣著相機(jī)與錄音轴咱,去河邊找鬼。 笑死烈涮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的窖剑。 我是一名探鬼主播坚洽,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼西土!你這毒婦竟也來了讶舰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤需了,失蹤者是張志新(化名)和其女友劉穎跳昼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肋乍,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鹅颊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墓造。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堪伍。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡锚烦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帝雇,到底是詐尸還是另有隱情涮俄,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布尸闸,位于F島的核電站彻亲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吮廉。R本人自食惡果不足惜苞尝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望茧痕。 院中可真熱鬧野来,春花似錦、人聲如沸踪旷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽令野。三九已至舀患,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間气破,已是汗流浹背聊浅。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留现使,地道東北人低匙。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像碳锈,于是被迫代替她去往敵國和親顽冶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容