大家都知道Android涉及到与UI相关的操作只能在主线程执行 android4.0以后就禁止在主线程进行网络请求了,在主线程里面执行Http请求都会报NetworkOnMainThreadException的异常. 于是乎,我们现在用的Volley,Android-Async-Http,Xutils,Okhttp,Retrofit..等网络框架都是支持异步网络请求的.(大致步骤: 子线程网络请求--得到结果(成功/失败) -- 通过Handler将结果返回主线程 -- 主线程更新ui )
目前常见的方法: 使用Thread ,Runnable,Handler启动一个子线程进行网络请求,然后再用handler通知主线程更新ui
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.activity_main); new Thread(runnable).start(); //启动子线程 } //handler 处理返回的请求结果 Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); Bundle data = msg.getData(); String val = data.getString(" data"); // // TODO: 更新页面 // } } //子线程进行网络请求 Runnable runnable = new Runnable(){ @Override public void run() { // // TODO: 执行网络请求. // Message msg = new Message(); Bundle data = new Bundle(); data.putString("data","请求结果"); msg.setData(data); handler.sendMessage(msg); } }
这个过程看似很正常,但是会出现一个常见的奔溃bug : IllegalArgumentException 如果这个请求执行的时间过久(由于网络延迟等原因),Activity或者Fragment已经不存在了(被销毁了),而线程并不知道这件事,这时候请求数据结果回来以后,将数据通过Handler抛给了主线程,在异步回调里一般都会执行数据的更新或者进度条的更新等操作,但页面已经不存在了,导致奔溃...
目前的解决方案: Activity : 1 在activity结束的时候结束请求 2 异步回调时通过Activity.isFinishing()判断activity是否已销毁 Fragment Fragment.isDeisDetached()
Activity举例: 以Volley
为例,在RequestQueue
这个类中,提供了通过tag
来取消与之相关的网络请求。
/*** 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() {public boolean apply(Request<?> request) {return request.getTag() == tag;}});}
tag
是在主线程调起网络请求时,通过Request.setTag(Object)
传进去的。tag
可以是任意类型(常用的 Context , Path ... )
Context
将Context
作为tag带入请求中,当持有Context
的对象销毁时,可以通知该请求线程取消。一个典型的使用场景就是在Activity的
onDestroy()
方法调用Request.cancelAll(this)
来取消与该Context
相关的所有请求。就Volley
来说,它会在请求发出之前以及数据回
来之后这两个时间段判断请求是否被cancel。但是作为Android开发者,应该知道,持有Context引用的线程是危险的,如果线程发
生死锁,Context引用会被线程一直持有,导致该Context得不到释放,容易引起内存泄漏。如果该Context的实例是一个Activity,
那么这个结果是灾难性的 ,所以线程通过弱引用持有Context。当系统内存不足需要GC时,会优先回收持有弱引用的对象。然而这样
做还是存在隐患,有内存泄漏的风险。
PATH
既然持有Context的问题比较严重,那么我们可以根据请求路径来唯一识别一个请求。发起请求的对象需要维持一个列表,记录当前
发出的请求路径,在请求回来时再从该列表通过路径来删除该请求。在Activity结束但请求未发出或者未返回时,再将于这个Activity
绑定的列表中的所有请求取消。这个方案执行起来如何呢?每个Activity都需要维持一个当前页面发出的请求列表,在Activity结束时
再取消列表中的请求。面对一个应用几十上百个Activity,这样的实现无疑是蛋疼的。有没有解决办法?有。通过良好的设计,可以
避免这个问题。可以使用一个单例的路径管理类来管理所有的请求,所有发出的请求需要在这个管理类里注册,请求可以与当前发出
的页面的类名(Class)进行绑定,从而在页面销毁时,注销所有与该页面关联的请求。
Activity.isFinishing()
大致步骤: 子线程网络请求--得到结果(成功/失败) -- 通过Handler将结果返回主线程 -- 主线程更新ui
最后两步
我们可以加入判断,如果页面被销毁了,那么直接返回,不通知主线程更新UI了,这样就可以完美解决问题了。
mMessageManager.getDataList(
new BaseManager.UIDataCallBack<JSONObject>() {
public void onSuccess(JSONObject object) {
if(isFinishing()){
return;
}
//TODO ...
}
public void onFail(int code, String msg) {
if(isFinishing()){
return;
}
//TODO ...
}
});
以上方案有一个弊端,在每个网络回调执行的时候,都需要提前判断Activity.isFinishing()
。如果有一个没有按照规定判断,那么这个App就有可能存在上述隐患,并且在原有的网络基础框架上修改这么多的网络回调是不太现实的。
在网上参考到了更好的解决方案:
基于Lifeful接口的异步回调框架
Lifeful接口设计
我们定义Lifeful,一个不依赖于Context、也不依赖于PATH的接口。
|
|
实际上,我们只需要让具有生命周期的类(一般是Activity或Fragment)实现这个接口,然后再通过这个接口来判断这个实现类是否还存在,就可以与Context解耦了。
接下来定义一个接口生成器,通过弱引用包装Lifeful接口的实现类,并返回所需要的相关信息。
|
|
提供一个该接口的默认实现:
|
|
接着通过一个静态方法判断是否对象的生命周期:
|
|
具有生命周期的Runnable
具体到跟线程打交道的异步类,只有Runnable
(Thread
也是其子类),因此只需要处理Runnable
就可以了。我们可以通过Wrapper
包装器模式,在处理真正的Runnable类之前,先通过Lifeful接口判断对象是否还存在,如果不存在则直接返回。对于Runnable
:
|
|
Lifeful的实现类
最后说一下Lifeful类的实现类,主要包括Activity和Fragment,
|
|
|
|
除了这两个类以外,别的类如果有生命周期,或者包含生命周期的引用,也可以实现Lifeful接口(如View,可以通过onAttachedToWindow()
和onDetachedToWindow()
)。
包含生命周期的异步调用
对于需要用到异步的地方,调用也很方便。
|
|
总结
本文主要针对Android中具有生命周期的对象在已经被销毁时对应的异步线程的处理方式进行解耦的过程。通过定义Lifeful接口,
实现了不依赖于Context或其他容易造成内存泄漏的对象,却又能与对象的生命周期进行绑定的方法。
参考以下文章:
2. http://www.jianshu.com/p/e7e1755d76b2