线程优化是Android性能优化中一个非常重要的部分,作为进程中逻辑处理调度的基本单位,如果使用不当,非常容易造成系统资源的浪费,从而导致应用性能出问题。在日常开发中,最常出现的问题主要有两个方面,一是线程启动过多造成CPU和内存资源浪费,并且应用耗电过大;二是线程作为GCRoots,如果使用不当,容易直接或间接造成Activity无法销毁,导致内存泄漏。
本篇博文主要以这两点为基础,结合日常开发中遇到的场景,整理和分析下相应的优化策略。
一、无需使用线程的场景
虽然程序无时无刻不是运行在线程中,不过为了防止阻塞主线程(比如Android的UI线程),获得更加流畅的用户体验,像网络请求、数据库操作、IO操作、密集计算等,往往都需要开启一个异步工作线程,这是众所周知的道理。不过,往往因此导致了线程的滥用,下面我们来看一些无需使用线程却误用的场景。
1、延时任务
由于某些操作或者逻辑需要延时处理,比如延时2s设置一个按钮消失。大家都知道不能阻塞主线程,否则会导致页面无响应,然后就习惯性地开个线程。
new Thread() {
public void run() {
try {
Thread.sleep(3000);
} catch (Exception e) {
}
runOnUiThread(new Runnable() {
@Override
public void run() {
...
}
});
}
}.start();
当然,从功能运行和代码逻辑的角度上来看,这并没有问题。但是,如果这种操作过于频繁,或者sleep过长,很容易导致同时开启的线程过多。而且runOnUiThread里面基本都是视图相关的操作,而线程作为GCRoots之一,存活状态下关联的对象都是无法被回收的。所以像View、Activity等一系列对象销毁后内存都无法被回收导致内存泄漏,除非等到线程销毁后的下一次GC才能被回收掉。
那么,除了开线程sleep之外,还有什么方式执行延时任务呢?
推荐做法:利用主线程Handler消息机制的postDelay。如果延时前和延时后的执行环境都是在主线程中,那么利用利用Handler消息机制的delay功能,能够非常方便作延时处理。以上的代码,可以修改成:
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
...
}
}, 3000);
而如果你正好有一个任意的View可以使用,连mHandler都不需要创建了:
mView.postDelayed(new Runnable() {
@Override
public void run() {
...
}
}, 3000);
这两种方式原理其实是一模一样的,都是利用主线程的消息机制。注意,这两种方式的Runnable执行都是在主线程中的。
不过,需要小心的是,这是解决了乱开线程的问题,而没有解决内存泄漏的问题。如果仅仅是这样处理,内存泄漏的问题还在,具体请查阅Handler导致的内存泄漏。所以,我们还需要做相应延时任务移除的处理:
mView.removeCallbacks(Runnable action)
mHandler.removeCallbacks(Runnable action)
这样一来,既解决了乱开线程的问题,也解决了内存泄漏的问题(当然,严格地讲如果Runnable刚好正在执行,依然会有短暂的内存泄漏)。
2、计时任务
谈到计时,最容易想到的就是Timer类了,使用其schedule方法可以非常简单地发布一个计时任务。其内部原理会启动一个TimerImpl线程,通过wait与noitfy机制实现了计时功能。不过,如果要即时更新UI,比如按秒显示一个倒计时数据,需要通过主线程的Handler发送一个Message来处理,大概代码如下:
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
... // handle timer callback
};
};
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(...);
}
};
timer.schedule(task, 0, 1000);
如果需要在TimerTask中执行耗时操作,比如密集型计算,IO操作等,使用Timer确实是无比合适的。但是如果仅仅是为了计时更新UI,可以使用Android官方提供的CountDownTimer类,其原理同样是利用Handler消息机制的Delay功能。
推荐做法:
new CountDownTimer(30000, 1000) {
public void onTick(long millisUntilFinished) {
mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
}
public void onFinish() {
mTextField.setText("done!");
}
}.start();
CountDownTimer虽然简单实用,但是自身还是有缺陷的,观察仔细的童鞋会发现在最后一次onTick到倒计时结束的时候,最后一次计数会显得非常地长,所以这里建议手动矫正最后一次计时。
除了使用CountDownTimer的方式外,还可以使用消息机制的递归调用,比如:
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// handle timer callback
...
sendEmptyMessageDelayed(MSG, 1000);
};
};
handler.sendEmptyMessageDelayed(MSG, 1000);
很明显,以上两种方式API比使用Timer开线程来说,除了性能更好,代码也更加简洁。但是无论是使用哪一种,切记不能忘记在destroy的时候调用cancel方法,不然内存又会出现泄漏了。
二、AsyncTask异步线程的注意点
AsyncTask是Android中老生常谈的一个线程调用工具,为了不落窠臼,本文就不去讲解AsyncTask的具体用法,而是针对性地研究下使用AsyncTask容易忽略的几个点。
首先,关于其cancel(boolean mayInterruptIfRunning)方法,我们通常使用这个方法来取消当前Task任务以希望能够释放Activity等。如果mayInterruptIfRunning为true,会调用线程的interrupt()方法。而interrupt()只是改变中断状态,无法中断正在运行的线程。也就是说即使调用的cancel(true),当前Task任务也会执行完,只是不再调用onPostExecute而是调用onCancelled方法。
一般情况下内部类或者匿名AsyncTask回持有Activity或者View的引用,这容易造成内存泄漏,所以建议在使用AsyncTask的时候尽量使用静态内部类。
其次,AsyncTask具有完整一套生命周期PENDING->RUNNING->FINISHED,如果已经执行过了,便无法再次执行任务。
public final AsyncTask executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
...
}
最重要的,AsyncTask的默认execute实现是同时只存在一个后台线程,如果同时调用多个AsyncTask,任务会加入到队列中,等待前一个执行完才执行下一个。有趣的是,Android API 4之前就是这种单独线程队列执行,之后改成了多线并发执行,而在Android API 11又改回了单独线程队列执行,主要还是基于性能考虑。不过,如果希望使用多线程并发,可以使用executeOnExecutor方法以及内部提供的THREAD_POOL_EXECUTOR线程池执行并发任务。
AsyncTask内部提供的THREAD_POOL_EXECUTOR每个版本的代码都略有不用,有时候我们可能需要针对各版本做兼容处理,下面稍微整理了下(4.0及以后)关键的改动点:
Android API 15-18:
Android API 19-23:
Android API 24-25:
可以看出,在API 19及之后,线程池最大线程数量由128调整为CPU核数x2+1,主要是防止同时开启的线程数量膨胀影响性能,也就是意味着AsyncTask的THREAD_POOL_EXECUTOR不适合用来执行高并发的任务,而是更加适合用来执行CPU密集型运算。
另外,如果只是希望执行一些异步操作,而不需要回调到主线程,可以使用AsyncTask.execute(Runnable runnable) 静态方法,非常便捷。
三、HandlerThread的使用场景
以笔者几年的Android项目经验而言,实际里HandlerThread的使用场景并不多,而使用的地方是否合理也都是值得商榷的。
从一定角度上来说,HandlerThread是对AsyncTask的一种补充。
AsyncTask的使用场景是: 主线程->异步线程->主线程,而HandlerThread的使用场景则是:主线程->异步线程->主线程->异步线程。简而言之,AsyncTask表示一次异步任务,执行完成就结束了。而HandlerThread表示多次异步任务,可以由主线程多次启动。HandlerThread继承于Thread类,只需要创建和启动一个线程,就能完成多个异步耗时任务,相比于多个AsyncTask多个线程而言,性能的提高是很大的。
总结一下,HandlerThread适用于持续性异步任务或者不连续多次异步任务。如果非持续性或者非多次异步操作,直接使用AsyncTask就可以,使用HandlerThread就纯属杀鸡用牛刀了。
另外,HandlerThread在使用结束后,需要中断looper销毁线程:
@Override
protected void onDestroy(){
super.onDestroy();
mHandlerThread.quit();
}
四、线程池的设计
涉及到多线程的场景,我们会很容易想到使用线程池。在Android项目管理中,我们往往会遇到很多new Thread这种随意创建线程的不合理代码,非常容易出现性能问题,合理的解决方案就是设计一套公用的线程调度机制来取代直接创建线程的方式。然而,很多时候一个公用的线程池调度往往很难处理项目中各种复杂的行为,这时候针对不同行为模式使用不同的线程调度策略就很有必要了。
1、IO行为模式
常见的IO操作包括文件操作、网络操作、数据库操作等,这一类操作都有一个特性,就是堵塞时间长且CPU利用率低,所以最好设计成无上限且重复空闲线程的线程池。比如说:
Executors.newCachedThreadPool()
2、密集计算行为模式
密集型计算包括图片处理、大数据计算、时间复杂度高的算法等行为,这一类行为对CPU的利用率较高,所以我们可以充分利用多核CPU的特性来提升性能,固定大小为CPU核数的线程池就比较适用了。比如说:
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).
本博客不定期持续更新,欢迎关注和交流:
http://blog.csdn.net/megatronkings