最近和几个朋友交流Android开发中的网络下载问题时,谈到了用Thread开启下载线程时会产生的Bug,其实直接用子线程开启下载任务的确是很Low的做法,那么原因究竟如何,而比较高大上的做法是怎样?于是用这篇博文详细分析记录一下。
一、概念介绍
Thread是指在CPU运行的一个程序中,可以有多个执行路径。运行的程序称作进程,而这个执行路径,就被称为线程(如果对这两个名词不太理解的同学可以参考一下操作系统方面的书籍)。Java中的多线程是指多个Thread可以在一段内同步执行,这样可以提高代码的运行效率,Java中允许一个进程有多个线程,可以无限多,但是必须要有一个线程,也就是当前进程的主线程。
必须要明白的一点是,Thread是Java语言下的一个底层类,而Android是使用并封装了Java语言的系统,所以Android中的AsyncTask只是使用了Java的多线程概念并优化封装之后的一个抽象类。所以Thread和AsyncTask完全是两个不同层次的概念,而不是简单的替换。
再说说AsyncTask异步任务,这个类是在Android中使用的,在编写该类时就已经明确说明,“AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs”,后边的不重要就不用粘贴了,可以看出异步任务进行长时间操作时使用的。因为Android中对每一个App的运行都看做一个进程,而这个进程中的主线程,就是UI线程,也就是我们打开一个App时可以看到界面的这个线程。而像下载这种耗时操作,如果放到UI线程执行,则会使得UI线程负荷过大产生ANR应用无响应异常,所以创建了AsyncTask类,用来专门进行一些耗时的非UI更新操作。
通过上面的介绍,很容易想到AsyncTask是使用了Java中的多线程技术的,但是他不是单纯的Thread,具体是怎么实现异步任务的,我们可以看源码比较。
Thread类是在java.lang包下的,所以他的使用不需要另外导包,而且Thread是实现Runnable接口的类,也就是说他可以实例化;由于Thread是底层代码,具体源码就不再分析了,所以主要说一下AsyncTask怎么用Thread实现的异步任务。
AsyncTask类是在android.os包下的抽象类,在使用之前必须导包。AsyncTask是使用线程工厂创建新的线程在后台执行异步任务的,之前我们说个Android中有一个UI线程作为主线程,那么再创建的线程都是子线程了,至于新创建的这些子线程做了什么事情,就要看我们的意愿了。
二、下载分析:
介绍了半天两个类的对比,感觉还是直接上Demo来的快一点。下边我分别用开启子线程和开启异步两种方式实现下载,同时简单分析一下这两种方式下的CPU执行顺序。
1. 在当前Activity中开启子Thread执行下载
(1)创建下载子线程:
1 /** 2 * 下载线程 3 */ 4 private Thread myThread =new Thread(new Runnable() { 5 @Override 6 public void run() { 7 Object data=download(PATH); 8 Message msg=Message.obtain(); 9 msg.what=101; 10 msg.obj=data; 11 handler.sendMessage(msg); 12 } 13 });
(2)在Handler中执行下载之后的任务:
1 private Handler handler=new Handler(new Handler.Callback() { 2 @Override 3 public boolean handleMessage(Message msg) { 4 if(msg.what==101){ 5 data=msg.obj; 6 //下面执行对data的操作 7 } 8 return false; 9 } 10 });
(3)在需要下载的地方开启当前下载线程:
1 myThread.start();
只需要上边三步就可以轻松完成下载网络请求,是不是看起来很简单?那么问题来了,下载任务是在myThread的子线程中执行的,如果下载任务还在执行的过程中时,用户执行了页面跳转的操作,也就是说当前Activity所在的UI线程已经销毁,但是并没有销毁myThread子线程吧,那么当myThread执行完下载任务download()这个方法之后,他接着调用handler来发送信息以执行data操作,而执行data操作的handler是在当前Activity中定义的,随着当前Activity的销毁,当前handler也跟随着销毁了,这样在myThread中就无法调用执行data的handler了,那么他必然会报NullPointException了吧。所以这样使用子线程进行下载任务是不安全的。
2. 使用异步任务AsyncTask执行下载任务
所以在Android中可以使用原生的AsyncTask进行像下载网络请求这样的耗时操作,具体方法就是创建一个下载任务继承AsyncTask抽象类,同时重写该类中的doInBackground(),这个方法是在要下载的子线程中执行的,点开AsyncTask的源码,我们可以看到在doInBackground()这个方法的前边有个注释@WorkThread,可以想到这个方法是在工作线程中执行的,那么有没有在主线程中执行的方法呢?当然是有的,我们还会看到有这样几个方法,他们的方法体内都没有执行语句,说明是可以用子类来重写这些方法的,有构造方法,execute(),onPreExecute(),onCancelled()等都是在MainThread中执行的。
那么可能有同学要提问了,这样还是在子线程中执行要下载的任务,难道这样再发生上边我们说到的那种临界事件,子线程下载结束之后就不会有空指针异常了吗?
当然可以很肯定的说,使用AsyncTask绝对不会发生上述Bug了,为什么呢?我们接着分析。
在工作线程中执行的除了当前执行下载的doInBackground()之外,就只有publishProgress()这一个方法了,而doInBackground()是个抽象方法,所以要想知道工作线程到底有什么门道,只能从publicProgress()找线索了。我们知道这个方法是发布进度的时候使用,下面是这个方法的源码,
1 @WorkerThread 2 protected final void publishProgress(Progress... values) { 3 if (!isCancelled()) { 4 getHandler().obtainMessage(MESSAGE_POST_PROGRESS, 5 new AsyncTaskResult<Progress>(this, values)).sendToTarget(); 6 } 7 }
很明显这个getHandler()就是获得当前AsyncTask类中的Handler对象,也就是说在工作线程中发布的进度会将信息发送到当前AsyncTask的Handler中处理,那么我们不管工作线程中具体怎么发布的进度,只需要看看在当前AsyncTask中怎么处理接收的信息就可以了。
1 private static Handler getHandler() { 2 synchronized (AsyncTask.class) { 3 if (sHandler == null) { 4 sHandler = new InternalHandler(); 5 } 6 return sHandler; 7 } 8 }
这个方法明显是在sHandler不为空的时候返回了一个InternalHandler对象,整个过程都是对AsyncTask加锁的,而这里加锁才是关键,毕竟要保证发送消息时的安全性,在获得一个InternalHandler对象后,整个AsyncTask都是加锁状态的。那么我们接着去看这个InternalHandler是干什么用的。
首先我们可以确定这是一个继承Handler类的子类,在他的handlerMessage()中只执行了下边几行代码,这里应该快要找到我们问题的根源了。
1 AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj; 2 switch (msg.what) { 3 case MESSAGE_POST_RESULT: 4 // There is only one result 5 result.mTask.finish(result.mData[0]); 6 break; 7 case MESSAGE_POST_PROGRESS: 8 result.mTask.onProgressUpdate(result.mData); 9 break; 10 }
发现这里对发送的信息类型进行了判断,只有两种类型,第二种MESSAGE_POST_PROGRESS,不正是刚刚发布进度的方法publicProgress()里边发送的信息类型吗。那么第一种MESSAGE_POST_RESULT,不难想到,这就是在工作线程中执行完doInBackground()之后的发送的信息类型了,而且人家已经有注释说明,“There is only one result”,“只有一个唯一的结果”,在得到这个信息也就是我们的下载任务执行完成之后,会调用下边那个方法,其实不用再往下找了,因为执行的这个方法就是当前AsyncTask自身的finish()这个方法。而这正是说明了在正常执行完工作线程的doInBackground()之后再在主线程中执行finish(),所以我们的思路也就理顺了。
好吧,也许看完上边的代码加我的分析,有些同学感觉更是云里雾里了,似乎这里边并没有解释中途跳转的问题啊。那你可要仔细想想了,在之前直接开启子线程下载之后的中途跳转发生空指针异常的根本原因在哪里?是在子线程中无法使用主线程中的handler对象才产生的空指针异常吧。那么我们的异步任务AsyncTask是怎么解决发送信息这个handler的?
在使用handler发送信息时,系统会先调用getHandler(),获得一个InternalHandler对象,如果之前没有就创建,如果有就用之前的,而且由于整个过程中当前异步任务AsyncTask都是加锁状态的,所以其他线程无法使用,同样的在使用AsyncTask的主线程中也无法随意销毁。这样再将得到的handler返回使用发送信息,就能顺利的跨过空指针异常了。
三、总结
这么解释,相信还在摸不着头脑的同学们应该明白一点了,下边我再简单做一下总结。
AsyncTask是作为异步任务,执行除了UI界面更新的任务之外的其他耗时操作的。UI界面的更新是在主线程,也就是UI线程中执行的,而在这个异步任务中,开启了一个工作线程来执行耗时操作。而这个工作线程和UI线程的执行顺序是不同步的,也就是说只有执行完工作线程中的下载之后,才会调用UI线程中的onPostExecute()执行后续UI操作,这样就实现了异步下载。如果UI线程销毁之后工作线程再发送下载结束的信息,由于工作线程再使用过程中是与AsyncTask绑定的,所以他也会随着当前AsyncTask的销毁而销毁,不会执行后续的下载操作,自然也不会执行发送下载结束的信息。
而简单的开启子线程执行下载,子线程与UI线程只是保持简单的同步关系,所以只是单纯的在子线程中执行下载耗时操作是不安全的。事实证明,尽管Java中的多线程是个很好的机制,但是在使用时要注意它的副作用,学会使用对他进行封装之后的类和方法。