RecyclerView的Adapter中attach和detach探索

問題描述

今天App的日誌捕獲中收到了一條這樣的crash日誌: java

異常捕獲圖

剛看到這個日誌的時候,分析了一下,復現的場景應該是這樣的:RecyclerView的Item中一個按鈕,點擊了以後會發起一個異步請求,開始前會彈出一個ProgressDialog等待,若是這個時候按home鍵回到了後臺,此時不巧被Activity被系統回收的話,就會出現這個問題。debug模式下,開啓不保留活動發現可以穩定復現。緣由是:異步操做回來的時候,在執行ProgressDialog的dismisss方法的時候,因爲Activity已經被回收以後,就至關於這個ProgressDialog(它持有了Activity的Context)所依附的window已經被銷燬了,因此會出現這個問題。git

代碼場景

具體到項目的場景中,咱們的項目中一個RecyclerView中對應了不少種type類型,因此引用了MultiType的庫來簡潔的註冊多種類型,這並沒什麼問題。github

問題是:當初爲了簡單,按鈕的點擊效果是直接放到了ViewHolder中來處理了。參考代碼以下:app

public class SongBinder extends ItemViewBinder<SongInfo, SongBinder.ViewHolder> {
    @NonNull @Override protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        ...
    }

    @Override protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull SongInfo item) {
        holder.bind(item);
        ...
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        private View mView;
        private ProgressDialog mProgressDialog;
        private CompositeDisposable mDisposables = new CompositeDisposable();
        ...

        public ViewHolder(View itemView) {
            super(itemView);
            mView = itemView;
            ...
        }

        public void bind(SongInfo info) {
            mView.setOnClickListener(view -> {
                ...
                // Step1 顯示彈窗
                showProgress();
                mDisposables.add(PlayUtils.playSingleSong().subscribe(status -> {
                	// Step2 關閉彈窗
                    dismissProgress();
                    ...
                }, throwable -> {
                	// Step2 關閉彈窗
                    dismissProgress();
                    ...
                }));
            });
        }

        public void showProgress() {
            if (mProgressDialog == null) {
                mProgressDialog = new LoadingDialog(getContext());
            }
            mProgressDialog.show();
        }

        public void dismissProgress() {
            if (mProgressDialog != null && mProgressDialog.isShowing()) {
                mProgressDialog.dismiss();
            }
        }

        public Context getContext() {
            return mView.getContext();
        }
    }
}
複製代碼

如上面的代碼所示,當初爲了簡便,點擊事件的處理放到了ViewHolder中,因此出現了開頭所述的問題。由於RecyclerView的Apdater有attach和detach的方法,因此看到這個問題,第一反應是增長這兩個方法,而後在detach方法中執行異步任務取消的操做,代碼以下:異步

public class SongBinder extends ItemViewBinder<SongInfo, SongBinder.ViewHolder> {
	private CompositeDisposable mDisposables = new CompositeDisposable();
	...
	
    @Override protected void onViewAttachedToWindow(@NonNull ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
    }

    @Override protected void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        // 取消異步操做
        mDisposables.clear();
    }

   class ViewHolder extends RecyclerView.ViewHolder {
        private View mView;
        private ProgressDialog mProgressDialog;
        
        ...
    }
}
複製代碼

如代碼註釋,設置取消的異步任務的操做。本覺得這樣設置應該能夠解決這個問題,可是測試發現,仍是會出現上述問題。調試發現,attach方法會執行,可是detach方法並無被執行到。ide

後來在這篇文章中找到了一些說明,這裏選取部分要點以下:佈局

  • onAttachedToRecyclerView is called when the Adapter is set to RecyclerView, after a call to RecyclerView#setAdapter(Adapter) or RecyclerView#swapAdapter(Adapter, boolean). This is quite obvious.
  • onDetachedFromRecyclerView, on the other hand, is called when current Adapter if going to be replaced by another Adapter (this another ‘Adapter’ can be Null). What is the point here: if you don’t replace the Adapter, this method will never be called. And what happens if an Adapter is never be 「detached」 from a RecyclerView? Let’s see after I explain about the other couples.
  • onViewAttachedToWindow is called once RecyclerView or its LayoutManager add a View into RecyclerView (hint: go to RecyclerView source code and search for the following keywords: dispatchChildAttached).
  • onViewDetachedFromWindow, on opposite, is called when RecyclerView or its LayoutManager detach a View from current Window.

大體意思是說:onViewDetachedFromWindow只有當它的佈局管理把一個子的Item View從當前Window中分離的時候纔會調用。總結來講,在如下兩種狀況下會被調用:測試

  • 顯式的調用Adapter的remove方法;
  • 從新設置RecyclerView的Adapter;

解決方案

知道了上述緣由,想到兩種解決方案:ui

  1. 比較簡單的改法:在Actiivty的onDestroy方法中調用RecyclerView的setAdapter方法,把原來的Adapter設置爲null;這樣能夠保證onDetach的調用,實際測試發現能夠解決問題;
  2. 把全部的點擊事件移到Activity中去處理,不要再ViewHolder中處理點擊事件。彈對話框的操做也放到Activity中去處理。

項目中爲了簡單,採用了第一種改法:this

@Override protected void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
    super.onViewDetachedFromWindow(holder);
    // 避免窗體泄漏
    holder.dismissProgress();
    mDisposables.clear();
}

public class SearchFragment extends Fragment{
	...
    @Override public void onDestroyView() {
        mBinding.recyclerView.setAdapter(null);
        super.onDestroyView();
    }
}
複製代碼
相關文章
相關標籤/搜索