Activity/Fragment结束时处理异步回调的解决方案

头疼的IllegalArgumentException

在Android开发的过程中,涉及到与UI相关的操作只能在主线程执行,否则就会抛出以下异常:

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

当然这属于基本常识,也不是本文讨论的重点,但后续的所有讨论都围绕这一基本常识进行。在开发Android应用时,如果所有的代码都在主线程执行,很容易就会出现ANR,并且Android在4.0以后已经禁止在主线程中执行网络请求,因此或多或少地需要与多线程打交道。无论是使用当前热火朝天的OkHttp(Retrofit),还是使用过时的Volley或者Android-Async-Http,它们都支持异步请求。这些异步请求的请求流程一般如下:

主线程发起请求

->网络框架开启工作线程进行网络请求

->工作线程拿到请求结果

->将请求结果通过Handler返回主线程

->主线程更新UI,完成一次网络请求

这个流程看似正常,实则暗含危机。下面的崩溃就是其中一个例子。

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)

这个崩溃是怎么发生的呢?原因很简单,工作线程执行网络请求,如果这个请求执行的时间过久(由于网络延迟等原因),Activity或者Fragment已经不存在了(被销毁了),而线程并不知道这件事,这时候请求数据结果回来以后,将数据通过Handler抛给了主线程,在异步回调里一般都会执行数据的更新或者进度条的更新等操作,但页面已经不存在了,所以就gg了。。。

目前的解决方案

有人会问,框架设计者怎么会没有考虑到这个问题?实际上他们确实考虑了这个问题。目前来说有两种解决方案:

  1. 在Activity结束时取消请求
  2. 在异步回调的时候,通过Activity.isFinishing()方法判断Activity是否已经被销毁

这两种方案本质是一样的,就是判断Activity是否被销毁,如果销毁,则要么取消回调,要么不执行回调。

在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() {
 @Override
 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.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...
 }
});

尽管解决了问题,但是麻烦又来了,如果只有一个人开发还好,需要时刻记住每个网络回调执行的时候,都需要提前判断Activity.isFinishing()。然而一个App往往有多个开发者一起协作完成,如果有一个开发者没有按照规定判断,那么这个App就有可能存在上述隐患,并且,在原有的网络基础框架上修改这么多的网络回调是不太现实的。

有没有更好的解决方案?请看下文。

基于Lifeful接口的异步回调框架

Lifeful接口设计

我们定义Lifeful,一个不依赖于Context、也不依赖于PATH的接口。

/**
 * Created by xingli on 9/21/16.
 *
 * 判断生命周期是否已经结束的一个接口。
 */
public interface Lifeful {
 /**
 * 判断某一个组件生命周期是否已经走到最后。一般用于异步回调时判断Activity或Fragment生命周期是否已经结束。
 *
 * @return
 */
 boolean isAlive();
}

实际上,我们只需要让具有生命周期的类(一般是Activity或Fragment)实现这个接口,然后再通过这个接口来判断这个实现类是否还存在,就可以与Context解耦了。

接下来定义一个接口生成器,通过弱引用包装Lifeful接口的实现类,并返回所需要的相关信息。

/**
 * Created by xingli on 9/22/16.
 *
 * 生命周期具体对象生成器。
 */
public interface LifefulGenerator<Callback> {

 /**
 * @return 返回回调接口。
 */
 Callback getCallback();

 /**
 * 获取与生命周期绑定的弱引用,一般为Context,使用一层WeakReference包装。
 *
 * @return 返回与生命周期绑定的弱引用。
 */
 WeakReference<Lifeful> getLifefulWeakReference();

 /**
 * 传入的引用是否为Null。
 *
 * @return true if {@link Lifeful} is null.
 */
 boolean isLifefulNull();
}

提供一个该接口的默认实现:

/**
 * Created by xingli on 9/22/16.
 *
 * 默认生命周期管理包装生成器。
 */

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;
 }
}

接着通过一个静态方法判断是否对象的生命周期:

/**
 * Created by xingli on 9/22/16.
 *
 * 生命周期相关帮助类。
 */

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,则这个对象被回收了。
  */
 if (null == lifeful && !objectIsNull) {
  Log.e(TAG, "Go home, null == lifeful && !objectIsNull");
  return true;
 }
 /**
  * 对象的生命周期结束
  */
 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接口判断对象是否还存在,如果不存在则直接返回。对于Runnable:

/**
 * Created by xingli on 9/21/16.
 *
 * 与周期相关的异步线程回调类。
 */
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的实现类

最后说一下Lifeful类的实现类,主要包括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();
  }
 }
}

除了这两个类以外,别的类如果有生命周期,或者包含生命周期的引用,也可以实现Lifeful接口(如View,可以通过onAttachedToWindow()onDetachedToWindow() ) 。

包含生命周期的异步调用

对于需要用到异步的地方,调用也很方便。

// ThreadCore是一个用于线程调度的ThreadPoolExecutor封装类,也用于主线程和工作线程之间的切换
ThreadCore.getInstance().postOnMainLooper(new LifefulRunnable(new Runnable() {
 @Override
 public void run() {
  // 实现真正的逻辑。
 }
}, this));

总结

本文主要针对Android中具有生命周期的对象在已经被销毁时对应的异步线程的处理方式进行解耦的过程。通过定义Lifeful接口,实现了不依赖于Context或其他容易造成内存泄漏的对象,却又能与对象的生命周期进行绑定的方法。好了,以上就是这篇文章的全部内容了,希望本文的内容对各位Android开发们能带来一定的帮助,如果有疑问大家可以留言交流。

(0)

相关推荐

  • C#实现异步编程的方法

    最近在我参与的几个.Net项目中都有用到异步编程,作为一名.Net小白,很有必要好好地学习一下C#异步编程. 什么是异步 异步指的就是不用阻塞当前线程来等待任务的完成,而是将任务扔到线程池中去执行,当前线程可以继续向下执行,直至其它线程将任务完成,并回调通知当前线程.整个任务从开始到结束都是异步完成的,不会阻塞当前线程.因此,异步很重要的一点就是,不会阻塞当前线程. 实现异步编程 在C#语言中,主要是通过委托来实现异步编程的,在委托类型中定义了两个方法BeginInvoke()和EndInvok

  • Java CountDownLatch完成异步回调实例详解

    Java CountDownLatch完成异步回调实例详解 实例代码: public class AsyncDemo { private static void doSomeTask() { System.out.println("Hello World"); } private static void onCompletion() { System.out.println("All tasks finished"); } public static void ma

  • C#编程总结(六)详解异步编程

    1.什么是异步? 异步操作通常用于执行完成时间可能较长的任务,如打开大文件.连接远程计算机或查询数据库.异步操作在主应用程序线程以外的线程中执行.应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行. 2.同步与异步的区别 同步(Synchronous):在执行某个操作时,应用程序必须等待该操作执行完成后才能继续执行. 异步(Asynchronous):在执行某个操作时,应用程序可在异步操作执行时继续执行.实质:异步操作,启动了新的线程,主线程与方法线程并行执行. 3.异

  • 浅谈JavaScript异步编程

    在一年前初学js的时候,看过很多关于异步编程的讲解.但是由于实践经验少,没有办法理解的太多,太理论的东西也往往是看完就忘. 经过公司的三两个项目的锻炼,终于对js异步编程有了比较具体的理解.但始终入门较浅,在这里就当是给自己一个阶段性的总结. 在异步编程中,一条语句的执行不能依赖上一条语句执行完毕的结果,因为无法预测一条语句什么时候执行完毕,它与代码顺序无关,语句是并发执行的. 例如以下代码: $.get($C.apiPath+'ucenter/padCharge/findMember',{id

  • 基于javascript的异步编程实例详解

    本文实例讲述了基于javascript的异步编程.分享给大家供大家参考,具体如下: 异步函数这个术语有点名不副实,调用一个函数后,程序只在该函数返回后才能继续.JavaScript程序员如果称一个函数为异步的,其意思就是这个函数会导致将来再运行另一个函数,后者取自于事件队列.如果后面这个函数是作为参数传递给前者的,则称其为回调函数. callback 回调函数是异步编程最基本的方式. 采用这种方式,我们把同步操作变成了异步操作,主函数不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟

  • js异步编程小技巧详解

    异步回调是js的一大特性,理解好用好这个特性可以写出很高质量的代码.分享一些实际用的一些异步编程技巧. 1.我们有些应用环境是需要等待两个http请求或IO操作返回后进行后续逻辑的处理.而这种情况使用回调嵌套代码会显得很难维护,而且也没有充分使用js的异步优势. 看下实例(为了大家容易理解使用了jq作为示例) $.get("获取数据1.html",function(data,status){ $.get("获取数据2.html",function(data1,sta

  • Java 异步回调机制实例分析

    Java 异步回调机制 一.什么是回调 回调,回调.要先有调用,才有调用者和被调用者之间的回调.所以在百度百科中是这样的: 软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用.回调和异步调用. 回调是一种特殊的调用,至于三种方式也有点不同. 1.同步回调,即阻塞,单向. 2.回调,即双向(类似自行车的两个齿轮). 3.异步调用,即通过异步消息进行通知. 二.CS中的异步回调(Java案例) 比如这里模拟个场景:客户端发送msg给服务端,服务端处理后(5秒),回调给客户端

  • 详解js的异步编程技术的方法

    基于浏览器的事件轮询机制(以及Node.js中的事件轮询机制),JavaScript常常会运行在异步环境中.由于JavaScript本身语言的特性(不需要程序员操控线程/进程),在js中解决异步化编程的方法就显得相当重要.可以说一个完整的项目中,js开发人员是不可能不面对异步操作的.本文将详细介绍几种经典JavaScript异步编程串行化方法,同时也将简单介绍一下ES6提供的Promise顺序执行方法. 一.回调函数 (1)经典回调函数方式:嵌套内联函数 假设我们有一个ajax()方法,他接收一

  • 使用Promise链式调用解决多个异步回调的问题

    介绍 所谓Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果. 缺少场景支撑,对于新手而言,很难理解Promise的意义. 在<你不知道的JavaScript中>有个场景介绍得很形象: 我走到快餐店的柜台,点了一个芝士汉堡.我交给收银员1.47美元.通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求.我已经启 动了一次交易. 但是,通常我不能马上就得到这个汉堡.收银员会交给我某个东西来代替汉堡:一张带有 订单号的收据.订单号就是一个

  • Java 异步编程实践_动力节点Java学院整理

    什么是异步?为什么要用它? 异步编程提供了一个非阻塞的,事件驱动的编程模型. 这种编程模型利用系统中多核执行任务来提供并行,因此提供了应用的吞吐率.此处吞吐率是指在单位时间内所做任务的数量. 在这种编程方式下, 一个工作单元将独立于主应用线程而执行, 并且会将它的状态通知调用线程:成功,处理中或者失败. 我们需要异步来消除阻塞模型.其实异步编程模型可以使用同样的线程来处理多个请求, 这些请求不会阻塞这个线程.想象一个应用正在使用的线程正在执行任务, 然后等待任务完成才进行下一步. log框架就是

随机推荐