冲天香阵透长安,满城尽带黄金甲。这篇文章主要讲述深入了解Android中的AsyncTask相关的知识,希望能为你提供帮助。
【深入了解Android中的AsyncTask】
AsyncTask,即异步任务,是android给我们提供的一个处理异步任务的类。通过此类,可以实现UI线程和后台线程进行通讯,后台线程执行异步任务,并把结果返回给UI线程。
我们知道,Android中只有UI线程,也就是主线程才能进行对UI的更新操作,而其他线程是不能直接操作UI的.这样的好处是保证了UI的稳定性和准确性,避免多个线程同时对UI进行操作而造成UI的混乱。
但Android是一个多线程的操作系统,我们总不能把所有的任务都放在主线程中进行实现,比如网络操作,文件读取等耗时操作,如果全部放到主线程去执行,就可能会造成后面任务的阻塞。Android会去检测这种阻塞,当阻塞时间太长的时候,就会抛出Application Not Responsed(ANR)错误.所以我们需要将这些耗时操作放在非主线程中去执行.这样既避免了Android的单线程模型,又避免了ANR。
虽说现在做网络请求有了Volley全家桶和OkHttp这样好用的库,但是在处理其他后台任务以及与UI交互上,还是需要用到AsyncTask。任何一个用户量上千万的产品绝对不会在代码里面使用系统原生的AsynTask,因为它蛋疼的兼容性以及极高的崩溃率实在让人不敢恭维。
AsyncTask到底是什么呢?很简单,它不过是对线程池和Handler的封装;用线程池来处理后台任务,用Handler来处理与UI的交互。线程池使用的是Executor接口,我们先了解一下线程池的特性。
JDK5带来的一大改进就是java的并发能力,它提供了三种并发武器:并发框架Executor,并发集合类型如ConcurrentHashMap,并发控制类如CountDownLatch等;尽量使用Exector而不是直接用Thread类进行并发编程。
AsyncTask内部也使用了线程池处理并发;线程池通过ThreadPoolExector类构造,这个构造函数参数比较多,它允许开发者对线程池进行定制,我们先看看这每个参数是什么意思,然后看看Android是以何种方式定制的。
ThreadPoolExecutor的其他构造函数最终都会调用如下的构造函数完成对象创建工作:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<
Runnable>
workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
corePoolSize: 核心线程数目,即使线程池没有任务,核心线程也不会终止(除非设置了allowCoreThreadTimeOut参数)可以理解为“常驻线程”
maximumPoolSize: 线程池中允许的最大线程数目;一般来说,线程越多,线程调度开销越大;因此一般都有这个限制。
keepAliveTime: 当线程池中的线程数目比核心线程多的时候,如果超过这个keepAliveTime的时间,多余的线程会被回收;这些与核心线程相对的线程通常被称为缓存线程
unit: keepAliveTime的时间单位
workQueue: 任务执行前保存任务的队列;这个队列仅保存由execute提交的Runnable任务
threadFactory: 用来构造线程池的工厂;一般都是使用默认的;
handler: 当线程池由于线程数目和队列限制而导致后续任务阻塞的时候,线程池的处理方式。
如果线程池中线程的数目少于corePoolSize,就算线程池中有其他的没事做的核心线程,线程池还是会重新创建一个核心线程;直到核心线程数目到达corePoolSize(常驻线程就位)。如果线程池中线程的数目大于或者等于corePoolSize,但是工作队列workQueue没有满,那么新的任务会放在队列workQueue中,按照FIFO的原则依次等待执行。当有核心线程处理完任务空闲出来后,会检查这个工作队列然后取出任务默默执行去,如果线程池中线程数目大于等于corePoolSize,并且工作队列workQueue满了,但是总线程数目小于maximumPoolSize,那么直接创建一个线程处理被添加的任务。如果工作队列满了,并且线程池中线程的数目到达了最大数目maximumPoolSize,那么就会用最后一个构造参数handler处理;**默认的处理方式是直接丢掉任务,然后抛出一个异常。总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于 corePoolSize,再看缓冲队列 workQueue 是否满,最后看线程池中的线程数量是否大于 maximumPoolSize。另外,当线程池中的线程数量大于 corePoolSize 时,如果里面有线程的空闲时间超过了 keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。
AsyncTask里面有“两个”线程池;一个THREAD_POOL_EXECUTOR一个SERIAL_EXECUTOR;之所以打引号,是因为其实SERIAL_EXECUTOR也使用THREAD_POOL_EXECUTOR实现的,只不过加了一个队列弄成了串行而已。AsyncTask里面线程池是一个核心线程数为CPU + 1,最大线程数为CPU * 2 + 1,工作队列长度为128的线程池;并且没有传递handler参数,那么使用的就是默认的Handler(拒绝执行)。如果任务过多,那么超过了工作队列以及线程数目的限制导致这个线程池发生阻塞,那么悲剧发生,默认的处理方式会直接抛出一个异常导致进程挂掉。假设你自己写一个异步图片加载的框架,然后用AsyncTask实现的话,当你快速滑动ListView的时候很容易发生这种异常;这也是为什么各大ImageLoader都是自己写线程池和Handlder的原因。这个线程池是一个静态变量;那么在同一个进程之内,所有地方使用到的AsyncTask默认构造函数构造出来的AsyncTask都使用的是同一个线程池,如果App模块比较多并且不加控制的话,很容易满足第一条的崩溃条件;如果你不幸在不同的AsyncTask的doInBackgroud里面访问了共享资源,那么就会发生各种并发编程问题。
在AsyncTask全部执行完毕之后,进程中还是会常驻corePoolSize个线程;在Android 4.4 (API 19)以下,这个corePoolSize是hardcode的,数值是5;API 19改成了cpu + 1;也就是说,在Android 4.4以前;如果你执行了超过五个AsyncTask;然后啥也不干了,进程中还是会有5个AsyncTask线程。
AsyncTask里面的handler很简单,如下(API 22代码):
private static final InternalHandler sHandler = new InternalHandler();
public InternalHandler() {
super(Looper.getMainLooper());
}
注意,这里直接用的主线程的Looper;如果去看API 22以下的代码,会发现它没有这个构造函数,而是使用默认的;默认情况下,Handler会使用当前线程的Looper,如果你的AsyncTask是在子线程创建的,那么很不幸,你的onPreExecute和onPostExecute并非在UI线程执行,而是被Handler post到创建它的那个线程执行;如果你在这两个线程更新了UI,那么直接导致崩溃。这也是大家口口相传的AsyncTask必须在主线程创建的原因。另外,AsyncTask里面的这个Handler是一个静态变量,也就是说它是在类加载的时候创建的;如果在你的APP进程里面,以前从来没有使用过AsyncTask,然后在子线程使用AsyncTask的相关变量,那么导致静态Handler初始化,如果在API 16以下,那么会出现上面同样的问题;这就是AsyncTask必须在主线程初始化
的原因。事实上,在Android 4.1(API 16)以后,在APP主线程ActivityThread的main函数里面,直接调用了AscynTask.init函数确保这个类是在主线程初始化的;另外,init这个函数里面获取了InternalHandler的Looper,由于是在主线程执行的,因此,AsyncTask的Handler用的也是主线程的Looper。这个问题从而得到彻底的解决。
AsyncTask的使用较为简单,只需要关注三个参数和四个方法即可。
AsyncTask<
Params,Progress,Result>
是一个抽象类,通常用于被继承.继承AsyncTask需要指定如下三个泛型参数:
Params:启动任务时输入的参数类型.
Progress:后台任务执行中返回进度值的类型.
Result:后台任务执行完成后返回结果的类型.
AsyncTask主要有如下几个方法:
doInBackground:必须重写,异步执行后台线程要完成的任务,耗时操作将在此方法中完成.
onPreExecute:执行后台耗时操作前被调用,通常用于进行初始化操作.
onPostExecute:当doInBackground方法完成后,系统将自动调用此方法,并将doInBackground方法返回的值传入此方法.通过此方法进行UI的更新.
onProgressUpdate:当在doInBackground方法中调用publishProgress方法更新任务执行进度后,将调用此方法.通过此方法我们可以知晓任务的完成进度.
下面通过代码演示一个典型的异步处理的实例--加载网络图片.网络操作作为一个不稳定的耗时操作,从4.0开始就被严禁放入主线程中.所以在显示一张网络图片时,我们需要在异步处理中下载图片,并在UI线程中设置图片。
MainActivity.java
import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class MainActivity extends Activity { private Button btn_image; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_image = (Button) findViewById(R.id.btn_image); btn_image.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this,ImageActivity.class)); } }); } }
ImageActivity.java
import android.app.Activity; import android.graphics.*; import android.os.*; import android.view.View; import android.widget.*; import java.io.*; import java.net.*; public class ImageActivity extends Activity { private ImageView imageView ; private ProgressBar progressBar ; private static String URL = "http://tupian.baike.com/a2_50_64_01300000432220134623642199335_jpg.html?prd=so_tupian"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.image); imageView = (ImageView) findViewById(R.id.image); progressBar = (ProgressBar) findViewById(R.id.progressBar); //通过调用execute方法开始处理异步任务.相当于线程中的start方法. new MyAsyncTask().execute(URL); }class MyAsyncTask extends AsyncTask< String,Void,Bitmap> {//onPreExecute用于异步处理前的操作 @Override protected void onPreExecute() { super.onPreExecute(); //此处将progressBar设置为可见. progressBar.setVisibility(View.VISIBLE); }//在doInBackground方法中进行异步任务的处理. @Override protected Bitmap doInBackground(String... params) { //获取传进来的参数 String url = params[0]; Bitmap bitmap = null; URLConnection connection ; InputStream is ; try { connection = new URL(url).openConnection(); is = connection.getInputStream(); //为了更清楚的看到加载图片的等待操作,将线程休眠3秒钟. Thread.sleep(3000); BufferedInputStream bis = new BufferedInputStream(is); //通过decodeStream方法解析输入流 bitmap = BitmapFactory.decodeStream(bis); is.close(); bis.close(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return bitmap; }//onPostExecute用于UI的更新.此方法的参数为doInBackground方法返回的值. @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); //隐藏progressBar progressBar.setVisibility(View.GONE); //更新imageView imageView.setImageBitmap(bitmap); } } }
推荐阅读
- 安卓笔记20170117
- 检查是否有可能以相等的总和划分k个子数组
- 可能的二叉搜索树和具有n个键的二叉树的总数
- 对称和非对称密钥加密之间有什么区别()
- C中char数据类型和char数组的大小
- Java计算list元素的出现次数
- 如何使用正则表达式检查字符串是否为字母数字()
- 算法题(2的出现次数(从0到n的数字))
- 什么是信息安全(有什么特征?)