Android多线程方式
1、前言
在Android开发中经常会使用到多线程,这里主要是总结Android开发中常见的多线程实现方式,以及这些多线程实现方式的一些特点
多线程实现方式主要有:
实现Thread的run()方法或者实现Runable接口
HandlerThread
AsyncTask
LoaderManager
2、Thread方式
一般使用异步操作*常见的一种方式,我们可以继承Thread,并重写run()方法,如下所示:
Thread syncTask = new Thread() {
@Override
public void run() {
// 执行耗时操作
}
};
syncTask.start();
还有另外一种启动线程的方式,即在创建Thread对象时,传入一个实现了Runable接口的对象,如下所示:
Thread syncTask = new Thread(new Runnable() {
@Override
public void run() {
// 执行耗时操作
}
});
syncTask.start();
Thread类中有几个方法的作用有些模糊,这里给出说明:
interrupt( ):
我们一般会使用该方法中断线程的执行,但该方法并不会中断线程,它的作用只是设置一个中断标志位,我们还得在run( )方法中判断这个标志位,并决定是否继续执行,通过这样的方式来达到中断的效果。 但该方法根据线程状态的不同,会有不同的结果。结果如下:
当线程由于调用wait( )、join( )、sleep( )而阻塞时,中断标志位将会被清空,并接收到InterruptedException异常。
当线程由于正在进行InterruptibleChannel类型的I/O操作而阻塞时,中断标志位将会置位,并接收到ClosedByInterruptException异常(I/O流也会自动关闭)
当线程由于进行Selector操作而阻塞时,中断标志位将会置位,但不会接收到异常
interrupted( )和isInterrupted( )的区别:
两个方法都是判断当前线程的中断标志位是否被置位,但调用interrupted( )方法后,中断标志位将会重置,而isInterrupted()不会被重置。
join( )和sleep( )的区别:两个方法都会让线程暂停执行
join()方法是让出执行资源(如:CPU时间片),使得其它线程可以获得执行的资源。所以调用join()方法会使进入阻塞状态,该线程被唤醒后会进入runable状态,等待下一个时间片的到来才能再次执行。
sleep( )不会让出资源,只是处于睡眠状态(类似只执行空操作)。调用sleep()方法会使进入等待状态,当等待时间到后,如果还在时间片内,则直接进入运行状态,否则进入runable状态,等待下个时间片。
3、HandlerThread
有些需求需要子线程不断的从一个消息队列中取出消息,并进行处理,处理完毕以后继续取出下一个处理。对于这个需求我们可以使用*种方式,实现一个Thread对象,并创建一个消息队列,在Thread对象的run方法中不断的从消息队列中取出消息进行处理。多以该线程的这些特点有点像一个Looper线程,我们可复用Handler机制提供的消息队列MessageQueue,而无需自己重新创建。
HandlerThread的内部实现机制很简单,在创建新的线程后,使该线程成为一个Looper线程,让该线程不断的从MessageQueue取出消息并处理。我们看一下HandlerThread的实现:
public class HandlerThread extends Thread {
int mPriority;
Looper mLooper;
/**
* Constructs a HandlerThread.
*/
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}
@Override
public void run() {
// 要想让某个线程成为Looper线程,先调用Looper.prepare()为该线程创建一个Looper对象,并初始化MessageQueue对象
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
// 调用Looper.loop(),让该线程的Looper实例循环从MessageQueue中取出Message进行处理
Looper.loop();
}
}
4、AsyncTask
这是我们*经常使用的一种异步方式,在前面的两种多线程方式中,如果在子线程中进行了耗时的处理操作(如:网络请求、读写数据库等),当操作完毕后,我们需要更新UI上的显示状态,但在Android开发中我们是不能在子线程中更新UI界面的,所以还得在子线程中发送一个通知到主线程,让主线程去更新UI。这样的操作流程有些复杂,且都是重复性的工作。所以Android sdk中为我们抽象出AsyncTask这个类。
public class CustomAsyncTask extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute() {
// 在开始执行异步操作前回调,该方法在主线程中执行
}
@Override
protected String doInBackground(String… strings) {
// 在该方法中进行异步操作,参数strings是在启动异步任务时execute(…)传递进来的
// 该异步任务放回的结果类型为String
return null;
}
@Override
protected void onProgressUpdate(Integer… values) {
// 该方法用户通知用户doInBackground()方法的处理进度,在主线程中被回调,所以可在该方法中更新UI
// 参数values用于指示处理进度
}
@Override
protected void onPostExecute(String result) {
// 该方法是在异步操作doInBackground()处理完毕后回调,参数result是doInBackground()的处理结果
// 该方法在主线程中被回调,可直接更新UI
}
@Override
protected void onCancelled(String result) {
super.onCancelled(result);
// 当调用cancel(boolean), 则在doInBackground()完成后回调该方法
// 注意: 参数result可能为null,
}
}
AsyncTask的内部使用了两个线程池,我们大概看一下AsyncTask的内部实现
// 顺序执行任务的线程池,注意这个线程池是静态的,每个AsyncTask对象共用这个线程池
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
// 我们启动异步任务的三个方法,都是向SerialExecutor.execute(runable)传递一个runable对象
public final AsyncTask<Params, Progress, Result> execute(Params… params) {
return executeOnExecutor(sDefaultExecutor, params);
}
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params… params) {
…
exec.execute(mFuture);
…
return this;
}
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
看一下SerialExecutor的实现
private static class SerialExecutor implements Executor {
// 存储待执行的异步任务
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
// 其实并没有马上执行,而是添加到队列mTasks中, 进行一个排队
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
// 一个任务执行完后,再执行下一个
scheduleNext();
}
}
});
// 当前没有异步任务执行时,启动开始执行
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
// 使用另外一个线程池分配线程,并执行任务
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
所以在使用AsyncTask执行异步操作时,会先在SerialExecutor进行一个顺序排队, 后再用ThreadPoolExcutor线程池为你分配一个线程并执行。而整个应用的AsyncTask任务都在排同一条队,有可能等待排队的任务很多,所以一般不会使用AsyncTask执行一些优先级比较高的异步任务。
当然我们是可以跳过不需要进行排队,直接就通过线程池分配一个线程并执行异步任务,但需要注意同时执行太多的异步任务,会影响用户体验,我想Google就是为了限制同时创建太多的线程才会采用一个排队机制的
/** @hide */
public static void setDefaultExecutor(Executor exec) {
sDefaultExecutor = exec;
}
该方法是隐藏,但可使用反射,设置一个线程池。
5、Loader&LoaderManager
上面三种异步方式都可以用来加载一些耗时的数据,但有时我们加载数据的过程与Activity、Fragment的生命息息相关的。所以在使用上面说的那几种异步方式进行异步数据加载时,是需要去考虑Activity(Fragment)的生命周期是处于哪个阶段的。于是Android在Android 3.0以后引入了LoaderManager,主要用于执行一些耗时的异步数据加载操作,并根据Activity生命周期对异步处理进行调整,LoaderManager可以解决的问题包括:
加载的数据有变化时,会自动通知我们,而不自己监控数据的变化情况,如:用CursorLoader来加载数据库数据,当数据库数据有变化时,可是个展示变化的数据
数据的请求处理时机会结合Activity和Fragment的生命周期进行调整,如:若Acivity销毁了,那就不会再去请求新的数据
使用该方法加载数据涉及到两个类重要的类,Loader和LoaderManager:
Loader:该类用于数据的加载 ,类型参数D用于指定Loader加载的数据类型
public class Loader<D> {
}
一般我们不直接继承Loader,而是继承AsyncTaskLoader,因为Loader的加载工作并不是在异步线程中。而AsyncTaskLoader实现了异步线程,加载流程在子线程中执行。注意:对该类的调用应该在主线程中完成。
LoaderManager:
LoaderManager用于管理与Activity和Fragment关联的Loader实例,LoaderManager负责根据的Activity的生命周期对Loader的数据加载器进行调度,所以这里分工明确,Loader负责数据加载逻辑,LoaderManager
负责Loader的调度,开发者只需要自定义自己的Loader,实现数据的加载逻辑,而不再关注数据加载时由于Activity销毁引发的问题。
注意:其实AsyncTaskLoader内部实现异步的方式是使用AsyncTask完成的,上面我们说过AsyncTask的内部是有一个排队机制,但AsyncTaskLoader内部使用AsyncTask进行数据异步加载时,异步任务并不进行排队。而直接又线程池分配新线程来执行。
6、总结
我们来总结一下异步处理的方式,以及每种处理方式适合什么样的场景
直接使用Thread实现方式,这种方式简单,但不是很优雅。适合数量很少(偶尔一两次)的异步任务,但要处理的异步任务很多的话,使用该方式会导致创建大量的线程,这会影响用户交互。
HandlerThread,这种方式适合子线程有序的执行异步操作,异步任务的执行一个接着一个。
AsyncTask, 通常用于耗时的异步处理,且时效性要求不是非常高的那种异步操作。如果时效性要求非常高的操作,不建议使用这个方式,因为AsyncTask的默认实现是有内部排队机制,且是整个应用的AsyncTask的任务进行排队,所以不能保证异步任务能很快的被执行。
LoaderManager,当请求处理时机需要根据Activity的生命周期进行调整,或需要时刻监测数据的变化,那LoaderManager是很不错的解决方案。