Android中使用Thread线程与AsyncTask异步任务的区别
最近和几个朋友交流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)创建下载子线程:
- /**
- * 下载线程
- */
- private Thread myThread =new Thread(new Runnable() {
- @Override
- public void run() {
- Object data=download(PATH);
- Message msg=Message.obtain();
- msg.what=101;
- msg.obj=data;
- handler.sendMessage(msg);
- }
- });
(2)在Handler中执行下载之后的任务:
- private Handler handler=new Handler(new Handler.Callback() {
- @Override
- public boolean handleMessage(Message msg) {
- if(msg.what==101){
- data=msg.obj;
- //下面执行对data的操作
- }
- return false;
- }
- });
(3)在需要下载的地方开启当前下载线程:
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()找线索了。我们知道这个方法是发布进度的时候使用,下面是这个方法的源码,
- @WorkerThread
- protected final void publishProgress(Progress... values) {
- if (!isCancelled()) {
- getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
- new AsyncTaskResult<Progress>(this, values)).sendToTarget();
- }
- }
很明显这个getHandler()就是获得当前AsyncTask类中的Handler对象,也就是说在工作线程中发布的进度会将信息发送到当前AsyncTask的Handler中处理,那么我们不管工作线程中具体怎么发布的进度,只需要看看在当前AsyncTask中怎么处理接收的信息就可以了。
- private static Handler getHandler() {
- synchronized (AsyncTask.class) {
- if (sHandler == null) {
- sHandler = new InternalHandler();
- }
- return sHandler;
- }
- }
这个方法明显是在sHandler不为空的时候返回了一个InternalHandler对象,整个过程都是对AsyncTask加锁的,而这里加锁才是关键,毕竟要保证发送消息时的安全性,在获得一个InternalHandler对象后,整个AsyncTask都是加锁状态的。那么我们接着去看这个InternalHandler是干什么用的。
首先我们可以确定这是一个继承Handler类的子类,在他的handlerMessage()中只执行了下边几行代码,这里应该快要找到我们问题的根源了。
- AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
- switch (msg.what) {
- case MESSAGE_POST_RESULT:
- // There is only one result
- result.mTask.finish(result.mData[0]);
- break;
- case MESSAGE_POST_PROGRESS:
- result.mTask.onProgressUpdate(result.mData);
- break;
- }
发现这里对发送的信息类型进行了判断,只有两种类型,第二种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中的多线程是个很好的机制,但是在使用时要注意它的副作用,学会使用对他进行封装之后的类和方法。
Android中使用Thread线程与AsyncTask异步任务的区别的更多相关文章
- Android多线程分析之五:使用AsyncTask异步下载图像
Android多线程分析之五:使用AsyncTask异步下载图像 罗朝辉 (http://www.cnblogs.com/kesalin) CC 许可,转载请注明出处 在本系列文章的第一篇<An ...
- Android中进程与线程
常说的主线程(UI线程)是什么? 当一个Android程序刚启动的时候,我们的android系统就会启动一个带有一个单一线程的linux进程.默认情况下,所有的组件比如Activity都运行在同样的一 ...
- Android中轻松使用线程
当你第一次启动一个Android程序的时候,一个被 称为"main"的线程就被自动创建了.它被称为主线程或者UI线程,它是非常重要的因为负责分发事件给对应的widget,还包含画 ...
- Android中进程与线程及如何在子线程中操作UI线程
1. Android进程 一个应用程序被启动时,系统默认创建执行一个叫做"main"的线程.这个线程也是你的应用与界面工具包(android.widget和android.view ...
- Android中,子线程使用主线程中的组件出现问题的解决方法
Android中,主线程中的组件,不能被子线程调用,否则就会出现异常. 这里所使用的方法就是利用Handler类中的Callback(),接受线程中的Message类发来的消息,然后把所要在线程中执行 ...
- 2017-11-29 由runnable说起Android中的子线程和主线程
1.首先纠正一个观点,就是runnable运行在子线程中是错误的观念.runnable只是创建了一个执行任务的对象,但是它本身并不会创建一个新的子线程,Runable只是给你接口让你实现工作线程的工作 ...
- 多线程——Java中继承Thread类与实现Runnable接口的区别
线程我只写过继承Thread类的,后来知道java多线程有三种方式,今天首先比较一下常用的继承Thread类和实现Runnable接口的区别. 按着Ctrl键进入Thread之后,发现Thread类也 ...
- android 中的 Handler 线程间通信
一. 在MainActivity中为什么只是类似的写一行如下代码就可以使用handler了呢? Handler handler = new Handler() { @Override public v ...
- RxJava 以及 Android 中的通用线程解决方案、并发与线程安全
关于RxJava如今是熟到发紫了,所以对于它底层的动作机制的了解是迫在眉睫了,费话不多说,直接开始. 这里还是以之前获取个人github仓库列表为例,用retrofit+rxjava,也是实际项目中用 ...
随机推荐
- Web开发——HTML基础(HTML表单/下拉列表/多行输入)
参考: 参考:http://www.w3school.com.cn/html/html_forms.asp 目录: 1.<form> 元素 1.1 <input> 元素(输入属 ...
- [redis] <<The little Redis book>>的读书笔记
<<The Little Redis Book>> 请右键点击在新窗口打开,可按原始大小查看.
- mimikaz常用命令
常用命令,留着自己使用的时候方便查找 mimikatz是一款功能强大的轻量级调试神器,通过它你可以提升进程权限注入进程读取进程内存,当然他最大的亮点也是让阿刚最感兴趣的就是他可以直接从 lsass中获 ...
- Ecplise通过Git将项目提交到GitHub
一.参考https://blog.csdn.net/bendanany/article/details/78891804 二.注意点: 1.仓库名必须和项目名相同: 2.若提交出现Can't conn ...
- bootstrap 中关于 HTML5 aria-* and role的用法
HTML5 aria-* and role 在bootstrap中看到role和aria-*,不知道干嘛的.google一下,发现aria的意思是Accessible Rich Internet Ap ...
- TZOJ :2731: 存钱计划(二)
描述 在TZC,WY存了钱,现在他要去买东西了.店很多,标记为1,2,3,4,5,6....但有的店之间有大路相连,而有的没有路.现在要由一个店到另一个店买东西,中途最少要经过多少个其它的店铺呢? 如 ...
- java框架之Spring(5)-注解驱动开发
准备 1.使用 maven 创建一个 java 项目,依赖如下: <dependency> <groupId>org.springframework</groupId&g ...
- git pull 冲突拉取不到新的代码
本地文件已经有冲突或者在pull的过程中拉取的文件和本地文件冲突时,拉取不到新的代码,git pull出现报错,如下: 这个时候,如果你有两种选择,如果你需要这些改动,那个你就需要手动解决冲突,然后a ...
- IO流的总结(二)
缓冲字节流: 我们先说一下缓存区的概念: 缓冲区就好比一辆车,一车一车的把数据拉走,这样就效率快多了 按照流的方向分类: 写入数据到流中,字节缓冲输出流 BufferedOutputStream 读取 ...
- POJ 2533 裸的LIS
A numeric sequence of ai is ordered if a1 < a2 < ... < aN. Let the subsequence of the given ...