Android为我们提供了几种异步线程操作:AsyncTask
, HandlerThread
, IntentService
, ThreadPool
和Loader
。了解这几种操作的不同之处,有助于我们开发的时候使用正确的异步操作,避免不必要的坑。
AsyncTask
AsyncTask
是我们平时使用频率最高的,利用它,我们可以很容易的将工作扔到异步线程中去,然后在工作完成之后再在UI线程中处理结果或者处理过程中与UI线程交互。但另一方面,它也很容易造成一些潜在的问题,比如说内存泄露。
应用场景:
与UI相关的工作,不太耗时的工作。例如,下载一张图片,并且在下载过程中需要不断更新进度条,然后下载完成后显示出来。
需要注意的坑:
- 默认情况下,所有创建的
AsyncTask
会共享一个线程,换句话说,AsyncTask
是以串行的方式执行的。如果有某个任务耗时太长,将会影响后面的任务。解决方法是将任务放到线程池中去执行。AsyncTask.executeOnExecutor
,或者按照官方(性能优化视频)的建议,直接用线程池ThreadPoolExecutor
代替AsyncTask
。 - 异步任务的取消:虽然
AsyncTask
提供了cancel
方法,但是你直接调用该方法并不能让任务停止下来,因为线程没有办法直接停止正在运行的代码。所以我们还需要在doInBackground
方法中,在恰当的时机中去检查标志位。
doInBackground(...) {
while (!isCanceled()) {
// do your work.
}
}
设置完标志位后,调用cancel
方法才能使得线程中的任务提前终止。需要注意的一点是被取消掉的任务会回调onCancelled
而不是onPostExecute
3.很容易出现内存泄露:
public class MainActivity extends AppCompatActivity {
private class MyAsyncTask extends AsyncTask<Void, Integer, String> {
@Override
protected String doInBackground(Void... params) {
// ...
return null;
}
@Override
protected void onPostExecute(String s) {
// ...
}
}
这是典型的内部类引用外部类问题。内部类MyAsyncTask
隐形的持有了MainActivity
的引用。假如Activity退出后,AsyncTask
中的任务还没处理完,Activity
还会被AsyncTask
持有着,这将导致Activity
无法被释放,直到AsyncTask
中的任务处理完。并且,如果这个时候,你在onPostExecute
中去操作UI,还有可能会报错。
HandlerThread
HandlerThread
继承自Thread
,内部封装了一个属于该线程的Looper
,源码加上注释也就是150行左右。可以看出还是非常简单的一个类。有了Looper
之后,我们就可以利用Handler
发送消息到该线程中去执行了。
应用场景:
与UI无关的或者耗时的工作。例如,后台压缩一张大图片。与AsyncTask
对比可以看出,HandlerThread
是对后者的补充。
需要注意的坑:
- 需要为
HandlerThread
设置线程优先级,默认情况下,它是和UI线程拥有相同优先级的,根据需要降低优先级可以保证UI线程不被阻塞。 - 当
HandlerThread
使用完成之后记得调用quitSafely
或者quit
方法完成退出,否则由于Looper
是一个无限循环,会导致线程无法正常退出。
ThreadPool
线程池,顾名思义,一个池子里面躺着很多的线程。在开始的时候就将这个池里的线程初始化完,待到需要使用的时候,就从池子里取出线程。在Android中我们可以使用ThreadPoolExecutor
来实现线程池。
应用场景:
适合并发工作的。假设你有40张图片需要处理,每张图片需要4ms,放在HandlerThread
中的话需要160ms,而如果将这些图片处理放到5个线程中去同时处理的话,则只需要32ms,整整提高了5倍速度。
需要注意的坑:
理论上来说,你可以为一个线程池创建很多很多的线程,但是CPU只能同时执行一定数量的线程,当线程超过这个数量时,就需要进行线程调度了,即根据每个线程的优先级来分配时间片,所以线程并不是越多越好的。而且,创建线程也是有代价的,每一个线程至少要占用64k内存,还很容易就上升。所以我们需要为线程池找到一个合理的线程数。我们可以根据每个机器当前可用的CPU个数来确定线程数,如:
// 需要注意的是,该方法并不是返回你手机物理上的CPU个数,因为有些CPU可能处于未激活状态,这取决于系统的负载。
private static int NUMBER_OF_THREAD = Runtime.getRuntime().availableProcessors();
IntentService
IntentService
是Service
和HandlerThread
的结合体。虽然Service
是运行在后台的,但是实际上它还是运行在UI线程中的,所以它并不适合用来执行耗时的操作。好在Android为我们提供了IntentService
,它继承自Service
,但在内部封装了HandlerThread
。它在收到Intent
时,会把Intent
发送到HandlerThread
中,让它来完成相应的耗时任务。同时,IntentService
又拥有Service
的优点(其中之一是当系统资源紧张的时候,它被回收掉的可能性在处于前台的app与处于后台的app之间)。
应用场景:
假设你在UI线程收到了一个Intent
,但处理这个Intent
的工作又会比较耗时,这时候就可以利用IntentService
来处理。
需要注意的坑:
-
HandlerThread
内部是通过消息队列来处理任务的,这意味着如果有某个任务耗时太长的话,会影响其它任务的执行。因为Service
是可以接收很多的Intent
的,即消息队列很可能会有很多的消息等待着处理。这与我们直接使用HandlerThread
还是有点不同的(因为直接使用可能消息队列中就只有一个任务需要处理)。 - 用到
IntentService
的时候,我们经常会使用BroadcastReceiver
来将结果返回到UI线程中,建议使用LocalBroadcastReceiver
来代替,它比较轻量级。
Loader
Loader
是Android 3.0之后才出现的,也是官方推荐的异步操作:它的优点有
- 不用担心内存泄露,这是因为
Loader
会帮我们同步Activity或者Fragment的生命周期。这是其它异步线程操作所不具备的。 - 缓存结果,所以当结果已经获得之后再次查询可以立即返回结果。
应用场景
Activity和Fragment中需要异步操作的地方。
总结
这篇文章并没有对每一种异步操作进行详细的分析,只是为了对比几种操作的优缺点、应用场景以及需要注意的地方。了解这些异步操作的不同之处,有助于帮助我们在开发过程中正确的选择相应的异步操作,减少不必要的麻烦。
参考资料
来源于Android官方的性能优化视频(需要梯子):