問題描述
今天App的日誌捕獲中收到了一條這樣的crash日誌:
剛看到這個日誌的時候,分析了一下,復現的場景應該是這樣的:RecyclerView的Item中一個按鈕,點選了之後會發起一個非同步請求,開始前會彈出一個ProgressDialog等待,如果這個時候按home鍵回到了後臺,此時不巧被Activity被系統回收的話,就會出現這個問題。debug模式下,開啟不保留活動發現能夠穩定復現。原因是:非同步操作回來的時候,在執行ProgressDialog的dismisss方法的時候,由於Activity已經被回收之後,就相當於這個ProgressDialog(它持有了Activity的Context)所依附的window已經被銷燬了,所以會出現這個問題。
程式碼場景
具體到專案的場景中,我們的專案中一個RecyclerView中對應了很多種type型別,所以引用了MultiType的庫來簡潔的註冊多種型別,這並沒什麼問題。
問題是:當初為了簡單,按鈕的點選效果是直接放到了ViewHolder中來處理了。參考程式碼如下:
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方法並沒有被執行到。
後來在這篇文章中找到了一些說明,這裡選取部分要點如下:
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;
解決方案
知道了上述原因,想到兩種解決方案:
- 比較簡單的改法:在Actiivty的onDestroy方法中呼叫RecyclerView的setAdapter方法,把原來的Adapter設定為null;這樣可以保證onDetach的呼叫,實際測試發現可以解決問題;
- 把所有的點選事件移到Activity中去處理,不要再ViewHolder中處理點選事件。彈對話方塊的操作也放到Activity中去處理。
專案中為了簡單,採用了第一種改法:
@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();
}
}
複製程式碼