Activity/Fragment結(jié)束時(shí)處理異步回調(diào)的解決方案
頭疼的IllegalArgumentException
在Android開發(fā)的過程中,涉及到與UI相關(guān)的操作只能在主線程執(zhí)行,否則就會(huì)拋出以下異常:
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
當(dāng)然這屬于基本常識(shí),也不是本文討論的重點(diǎn),但后續(xù)的所有討論都圍繞這一基本常識(shí)進(jìn)行。在開發(fā)Android應(yīng)用時(shí),如果所有的代碼都在主線程執(zhí)行,很容易就會(huì)出現(xiàn)ANR,并且Android在4.0以后已經(jīng)禁止在主線程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,因此或多或少地需要與多線程打交道。無(wú)論是使用當(dāng)前熱火朝天的OkHttp(Retrofit),還是使用過時(shí)的Volley或者Android-Async-Http,它們都支持異步請(qǐng)求。這些異步請(qǐng)求的請(qǐng)求流程一般如下:
主線程發(fā)起請(qǐng)求
->網(wǎng)絡(luò)框架開啟工作線程進(jìn)行網(wǎng)絡(luò)請(qǐng)求
->工作線程拿到請(qǐng)求結(jié)果
->將請(qǐng)求結(jié)果通過Handler返回主線程
->主線程更新UI,完成一次網(wǎng)絡(luò)請(qǐng)求
這個(gè)流程看似正常,實(shí)則暗含危機(jī)。下面的崩潰就是其中一個(gè)例子。
java.lang.IllegalArgumentException: View=com.android.internal.policy.impl.PhoneWindow$DecorView{24e9c19a V.E..... R......D 0,0-1026,348} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:403)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:322)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:84)
at android.app.Dialog.dismissDialog(Dialog.java:368)
at android.app.Dialog.dismiss(Dialog.java:351)
at com.kaola.spring.ui.kaola.AvatarNicknameSetActivity.e(Unknown Source)
at com.kaola.spring.ui.kaola.AvatarNicknameSetActivity.c(Unknown Source)
at com.kaola.spring.ui.kaola.d.a(Unknown Source)
at com.kaola.common.c.d$b.handleMessage(Unknown Source)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5539)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:960)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
這個(gè)崩潰是怎么發(fā)生的呢?原因很簡(jiǎn)單,工作線程執(zhí)行網(wǎng)絡(luò)請(qǐng)求,如果這個(gè)請(qǐng)求執(zhí)行的時(shí)間過久(由于網(wǎng)絡(luò)延遲等原因),Activity或者Fragment已經(jīng)不存在了(被銷毀了),而線程并不知道這件事,這時(shí)候請(qǐng)求數(shù)據(jù)結(jié)果回來以后,將數(shù)據(jù)通過Handler拋給了主線程,在異步回調(diào)里一般都會(huì)執(zhí)行數(shù)據(jù)的更新或者進(jìn)度條的更新等操作,但頁(yè)面已經(jīng)不存在了,所以就gg了。。。
目前的解決方案
有人會(huì)問,框架設(shè)計(jì)者怎么會(huì)沒有考慮到這個(gè)問題?實(shí)際上他們確實(shí)考慮了這個(gè)問題。目前來說有兩種解決方案:
- 在Activity結(jié)束時(shí)取消請(qǐng)求
- 在異步回調(diào)的時(shí)候,通過
Activity.isFinishing()方法判斷Activity是否已經(jīng)被銷毀
這兩種方案本質(zhì)是一樣的,就是判斷Activity是否被銷毀,如果銷毀,則要么取消回調(diào),要么不執(zhí)行回調(diào)。
在Activity結(jié)束時(shí)取消請(qǐng)求
以Volley為例,在RequestQueue這個(gè)類中,提供了通過tag來取消與之相關(guān)的網(wǎng)絡(luò)請(qǐng)求。
/**
* Cancels all requests in this queue with the given tag. Tag must be non-null
* and equality is by identity.
*/
public void cancelAll(final Object tag) {
if (tag == null) {
throw new IllegalArgumentException("Cannot cancelAll with a null tag");
}
cancelAll(new RequestFilter() {
@Override
public boolean apply(Request<?> request) {
return request.getTag() == tag;
}
});
}
這個(gè)tag是在主線程調(diào)起網(wǎng)絡(luò)請(qǐng)求時(shí),通過Request.setTag(Object)傳進(jìn)去的。tag可以是任意類型,通常來說可以使用下面兩種類型:
- Context
- PATH
Context
將Context作為tag帶入請(qǐng)求中,當(dāng)持有Context的對(duì)象銷毀時(shí),可以通知該請(qǐng)求線程取消。一個(gè)典型的使用場(chǎng)景就是在Activity的onDestroy()方法調(diào)用Request.cancelAll(this)來取消與該Context相關(guān)的所有請(qǐng)求。就Volley來說,它會(huì)在請(qǐng)求發(fā)出之前以及數(shù)據(jù)回來之后這兩個(gè)時(shí)間段判斷請(qǐng)求是否被cancel。
但是作為Android開發(fā)者,應(yīng)該知道,持有Context引用的線程是危險(xiǎn)的,如果線程發(fā)生死鎖,Context引用會(huì)被線程一直持有,導(dǎo)致該Context得不到釋放,容易引起內(nèi)存泄漏。如果該Context的實(shí)例是一個(gè)Activity,那么這個(gè)結(jié)果是災(zāi)難性的。
有沒有解決辦法?有。線程通過弱引用持有Context。當(dāng)系統(tǒng)內(nèi)存不足需要GC時(shí),會(huì)優(yōu)先回收持有弱引用的對(duì)象。然而這樣做還是存在隱患,有內(nèi)存泄漏的風(fēng)險(xiǎn)。
PATH
既然持有Context的問題比較嚴(yán)重,那么我們可以根據(jù)請(qǐng)求路徑來唯一識(shí)別一個(gè)請(qǐng)求。發(fā)起請(qǐng)求的對(duì)象需要維持一個(gè)列表,記錄當(dāng)前發(fā)出的請(qǐng)求路徑,在請(qǐng)求回來時(shí)再?gòu)脑摿斜硗ㄟ^路徑來刪除該請(qǐng)求。在Activity結(jié)束但請(qǐng)求未發(fā)出或者未返回時(shí),再將于這個(gè)Activity綁定的列表中的所有請(qǐng)求取消。
看起來方案不錯(cuò),但執(zhí)行起來如何呢?每個(gè)Activity都需要維持一個(gè)當(dāng)前頁(yè)面發(fā)出的請(qǐng)求列表,在Activity結(jié)束時(shí)再取消列表中的請(qǐng)求。面對(duì)一個(gè)應(yīng)用幾十上百個(gè)Activity,這樣的實(shí)現(xiàn)無(wú)疑是蛋疼的。
有沒有解決辦法?有。通過良好的設(shè)計(jì),可以避免這個(gè)問題??梢允褂靡粋€(gè)單例的路徑管理類來管理所有的請(qǐng)求。所有發(fā)出的請(qǐng)求需要在這個(gè)管理類里注冊(cè),請(qǐng)求可以與當(dāng)前發(fā)出的頁(yè)面的類名(Class)進(jìn)行綁定,從而在頁(yè)面銷毀時(shí),注銷所有與該頁(yè)面關(guān)聯(lián)的請(qǐng)求。
Activity.isFinishing()
在異步請(qǐng)求的流程中,我們注意到最后兩步:
->將請(qǐng)求結(jié)果通過Handler返回主線程
->主線程更新UI,完成一次網(wǎng)絡(luò)請(qǐng)求
在這最后兩步執(zhí)行的過程中,我們可以加入判斷,如果頁(yè)面被銷毀了,那么直接返回,不通知主線程更新UI了,這樣就可以完美解決問題了。
類似的一個(gè)例子如下:
mMessageManager.getBoxList(new BaseManager.UIDataCallBack<JSONObject>() {
@Override
public void onSuccess(JSONObject object) {
if(isFinishing()){
return;
}
do what you want...
}
@Override
public void onFail(int code, String msg) {
if(isFinishing()){
return;
}
do what you want...
}
});
盡管解決了問題,但是麻煩又來了,如果只有一個(gè)人開發(fā)還好,需要時(shí)刻記住每個(gè)網(wǎng)絡(luò)回調(diào)執(zhí)行的時(shí)候,都需要提前判斷Activity.isFinishing()。然而一個(gè)App往往有多個(gè)開發(fā)者一起協(xié)作完成,如果有一個(gè)開發(fā)者沒有按照規(guī)定判斷,那么這個(gè)App就有可能存在上述隱患,并且,在原有的網(wǎng)絡(luò)基礎(chǔ)框架上修改這么多的網(wǎng)絡(luò)回調(diào)是不太現(xiàn)實(shí)的。
有沒有更好的解決方案?請(qǐng)看下文。
基于Lifeful接口的異步回調(diào)框架
Lifeful接口設(shè)計(jì)
我們定義Lifeful,一個(gè)不依賴于Context、也不依賴于PATH的接口。
/**
* Created by xingli on 9/21/16.
*
* 判斷生命周期是否已經(jīng)結(jié)束的一個(gè)接口。
*/
public interface Lifeful {
/**
* 判斷某一個(gè)組件生命周期是否已經(jīng)走到最后。一般用于異步回調(diào)時(shí)判斷Activity或Fragment生命周期是否已經(jīng)結(jié)束。
*
* @return
*/
boolean isAlive();
}
實(shí)際上,我們只需要讓具有生命周期的類(一般是Activity或Fragment)實(shí)現(xiàn)這個(gè)接口,然后再通過這個(gè)接口來判斷這個(gè)實(shí)現(xiàn)類是否還存在,就可以與Context解耦了。
接下來定義一個(gè)接口生成器,通過弱引用包裝Lifeful接口的實(shí)現(xiàn)類,并返回所需要的相關(guān)信息。
/**
* Created by xingli on 9/22/16.
*
* 生命周期具體對(duì)象生成器。
*/
public interface LifefulGenerator<Callback> {
/**
* @return 返回回調(diào)接口。
*/
Callback getCallback();
/**
* 獲取與生命周期綁定的弱引用,一般為Context,使用一層WeakReference包裝。
*
* @return 返回與生命周期綁定的弱引用。
*/
WeakReference<Lifeful> getLifefulWeakReference();
/**
* 傳入的引用是否為Null。
*
* @return true if {@link Lifeful} is null.
*/
boolean isLifefulNull();
}
提供一個(gè)該接口的默認(rèn)實(shí)現(xiàn):
/**
* Created by xingli on 9/22/16.
*
* 默認(rèn)生命周期管理包裝生成器。
*/
public class DefaultLifefulGenerator<Callback> implements LifefulGenerator<Callback> {
private WeakReference<Lifeful> mLifefulWeakReference;
private boolean mLifefulIsNull;
private Callback mCallback;
public DefaultLifefulGenerator(Callback callback, Lifeful lifeful) {
mCallback = callback;
mLifefulWeakReference = new WeakReference<>(lifeful);
mLifefulIsNull = lifeful == null;
}
@Override
public Callback getCallback() {
return mCallback;
}
public WeakReference<Lifeful> getLifefulWeakReference() {
return mLifefulWeakReference;
}
@Override
public boolean isLifefulNull() {
return mLifefulIsNull;
}
}
接著通過一個(gè)靜態(tài)方法判斷是否對(duì)象的生命周期:
/**
* Created by xingli on 9/22/16.
*
* 生命周期相關(guān)幫助類。
*/
public class LifefulUtils {
private static final String TAG = LifefulUtils.class.getSimpleName();
public static boolean shouldGoHome(WeakReference<Lifeful> lifefulWeakReference, boolean objectIsNull) {
if (lifefulWeakReference == null) {
Log.e(TAG, "Go home, lifefulWeakReference == null");
return true;
}
Lifeful lifeful = lifefulWeakReference.get();
/**
* 如果傳入的Lifeful不為null,但弱引用為null,則這個(gè)對(duì)象被回收了。
*/
if (null == lifeful && !objectIsNull) {
Log.e(TAG, "Go home, null == lifeful && !objectIsNull");
return true;
}
/**
* 對(duì)象的生命周期結(jié)束
*/
if (null != lifeful && !lifeful.isAlive()) {
Log.e(TAG, "Go home, null != lifeful && !lifeful.isAlive()");
return true;
}
return false;
}
public static <T> boolean shouldGoHome(LifefulGenerator<T> lifefulGenerator) {
if (null == lifefulGenerator) {
Log.e(TAG, "Go home, null == lifefulGenerator");
return true;
} if (null == lifefulGenerator.getCallback()) {
Log.e(TAG, "Go home, null == lifefulGenerator.getCallback()");
return true;
}
return shouldGoHome(lifefulGenerator.getLifefulWeakReference(), lifefulGenerator.isLifefulNull());
}
}
具有生命周期的Runnable
具體到跟線程打交道的異步類,只有Runnable(Thread也是其子類),因此只需要處理Runnable就可以了。我們可以通過Wrapper包裝器模式,在處理真正的Runnable類之前,先通過Lifeful接口判斷對(duì)象是否還存在,如果不存在則直接返回。對(duì)于Runnable:
/**
* Created by xingli on 9/21/16.
*
* 與周期相關(guān)的異步線程回調(diào)類。
*/
public class LifefulRunnable implements Runnable {
private LifefulGenerator<Runnable> mLifefulGenerator;
public LifefulRunnable(Runnable runnable, Lifeful lifeful) {
mLifefulGenerator = new DefaultLifefulGenerator<>(runnable, lifeful);
}
@Override
public void run() {
if (LifefulUtils.shouldGoHome(mLifefulGenerator)) {
return;
}
mLifefulGenerator.getCallback().run();
}
}
Lifeful的實(shí)現(xiàn)類
最后說一下Lifeful類的實(shí)現(xiàn)類,主要包括Activity和Fragment,
public class BaseActivity extends Activity implements Lifeful {
@Override
public boolean isAlive() {
return activityIsAlive();
}
public boolean activityIsAlive() {
if (currentActivity == null) return false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !(currentActivity.isDestroyed() || currentActivity.isFinishing());
} else {
return !currentActivity.isFinishing();
}
}
}
public class BaseFragment extends Fragment implements Lifeful {
@Override
public boolean isAlive() {
return activityIsAlive();
}
public boolean activityIsAlive() {
Activity currentActivity = getActivity();
if (currentActivity == null) return false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !(currentActivity.isDestroyed() || currentActivity.isFinishing());
} else {
return !currentActivity.isFinishing();
}
}
}
除了這兩個(gè)類以外,別的類如果有生命周期,或者包含生命周期的引用,也可以實(shí)現(xiàn)Lifeful接口(如View,可以通過onAttachedToWindow()和onDetachedToWindow() ) 。
包含生命周期的異步調(diào)用
對(duì)于需要用到異步的地方,調(diào)用也很方便。
// ThreadCore是一個(gè)用于線程調(diào)度的ThreadPoolExecutor封裝類,也用于主線程和工作線程之間的切換
ThreadCore.getInstance().postOnMainLooper(new LifefulRunnable(new Runnable() {
@Override
public void run() {
// 實(shí)現(xiàn)真正的邏輯。
}
}, this));
總結(jié)
本文主要針對(duì)Android中具有生命周期的對(duì)象在已經(jīng)被銷毀時(shí)對(duì)應(yīng)的異步線程的處理方式進(jìn)行解耦的過程。通過定義Lifeful接口,實(shí)現(xiàn)了不依賴于Context或其他容易造成內(nèi)存泄漏的對(duì)象,卻又能與對(duì)象的生命周期進(jìn)行綁定的方法。好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)各位Android開發(fā)們能帶來一定的幫助,如果有疑問大家可以留言交流。
相關(guān)文章
安卓(Android)開發(fā)之統(tǒng)計(jì)App啟動(dòng)時(shí)間
當(dāng)大家要改善APP啟動(dòng)速度優(yōu)化的時(shí)候,首先要知道App的啟動(dòng)時(shí)間,那么改如何統(tǒng)計(jì)時(shí)間呢,下面我們一起來看看。2016-08-08
Android中自定義ImageView添加文字設(shè)置按下效果詳解
這篇文章主要給大家介紹了關(guān)于Android中自定義ImageView添加文字設(shè)置按下效果的相關(guān)資料,實(shí)現(xiàn)后的效果非常利用用戶的體驗(yàn),文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)下吧。2017-08-08
ImageView點(diǎn)擊可變暗的實(shí)例代碼(android代碼技巧)
本文給大家分享一段實(shí)例代碼給大家介紹ImageView點(diǎn)擊可變暗的實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的的朋友參考下吧2017-02-02
Android使用viewpager實(shí)現(xiàn)自動(dòng)無(wú)限輪播圖
這篇文章主要介紹了Android使用viewpager實(shí)現(xiàn)自動(dòng)無(wú)限輪播圖效果,實(shí)現(xiàn)方法大概有兩種,一種是viewpager+作為游標(biāo)的點(diǎn) 。另外一種是重寫viewpager,具體實(shí)現(xiàn)過程大家參考下本文2018-06-06
詳解React Native監(jiān)聽Android回退按鍵與程序化退出應(yīng)用
這篇文章主要介紹了詳解React Native監(jiān)聽Android回退按鍵與程序化退出應(yīng)用的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09
Android開發(fā)之開發(fā)者頭條(一)啟動(dòng)頁(yè)實(shí)現(xiàn)
這篇文章主要介紹了Android開發(fā)之開發(fā)者頭條(一)啟動(dòng)頁(yè)實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2016-04-04
Android拼圖游戲 玩轉(zhuǎn)從基礎(chǔ)到應(yīng)用手勢(shì)變化
這篇文章主要介紹了Android拼圖游戲的實(shí)現(xiàn)方法,教大家玩轉(zhuǎn)從基礎(chǔ)到應(yīng)用手勢(shì)變化,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
Android 中RecyclerView通用適配器的實(shí)現(xiàn)
這篇文章主要介紹了Android 中RecyclerView通用適配器的實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2017-03-03
Android代碼實(shí)現(xiàn)圖片和文字上下布局
在Android開發(fā)中經(jīng)常會(huì)需要用到帶文字和圖片的button,下面來給大家介紹使用radiobutton實(shí)現(xiàn)圖片和文字上下布局或左右布局。感興趣的朋友一起學(xué)習(xí)吧2015-11-11

