在Android开发中,通常会将耗时的任务放到子线程中执行,以保证UI线程的流畅性。不过,系统要开启一个线程会消耗一定的资源,并且,对于多线程还有额外的工作要处理,比如:线程安全、死锁、内存消耗、对象生命周期管理等。因此,在开发过程中,如果随意开启线程,不但会加重开发任务,还会过多消耗系统资源,达不到令UI线程流畅的效果。
进程优先级
线程是进程的一部分,因此,线程的生命周期直接被进程所影响,而进程的的存活又与其优先级直接相关,所以,在使用线程的时候,通常需要关注进程的优先级。
通常来说,Android系统会系统资源不足的情况下,根据进程优先级杀死进程,优先级越低的进程越容易被杀死。Android系统进程优先级由高到底排列如下:
- 前台进程(Foreground Process):表明用户正在与该进程进行交互操作。
1.进程持有一个正在与用户交互的Activity(Activity处于Resumed状态)。
2.进程持有一个Service,并且这个Service与一个用户正在交互的Activity进行绑定。
3.进程持有一个前台运行模式的Service(Service调用了startForegroud()方法)。
4.进程持有一个正在执行生命周期方法的Service(Service正在执行onCreate()、onDestroy()等方法)。
5.进程持有一个正在执行onReceive()方法的BroadcastReceiver。
- 可见进程(Visible Process):表明虽然该进程没有持有任何前台组件,但是它能够影响用户可见的界面。
1.进程持有一个非前台Activity,但这个Activity对用户可见(Activity处于Paused状态)。
2.进程持有一个与可见Activity绑定的Service。
服务进程(Service Process):除了符合前台进程和可见进程条件的Service,其它的Service都会被归类为服务进程。
后台进程(Background Process):持有不可见Activity(Activity处于Stopped状态)的进程。
通常情况下会有很多后台进程,当内存不足的时候,在所有的后台进程里面,会按照LRU(最近使用)规则,优先回收最长时间没有使用过的进程。
- 空进程(Empty Process):不持有任何活动组件的进程(Activity处于Destroyed状态)。
保持这种进程只有一个目的,就是为了缓存,以便下一次启动该进程中的组件时能够更快响应。当资源紧张的时候,系统会平衡进程缓存和底层的内核缓存情况进行回收。
线程调度
线程优先级
进程的优先级与系统回收资源有关,而线程的优先级与线程调度有关,一般来说,优先级越高的线程能获取到更多的CPU时间片去执行。
但是,在Linux系统中,调度器在分配时间片时,采用的CFS(completely fair scheduler)策略,这种策略不但会参考单个线程的优先级,还会追踪每个线程已经获取到的时间片数量,如果高优先级的线程已经执行了很长时间,但低优先级的线程一直在等待,后续系统会保证低优先级的线程也能获取更多的CPU时间。显然使用这种调度策略的话,优先级高的线程并不一定能在争取时间片上有绝对的优势,所以,Android系统在线程调度上使用了cgroups的概念,cgroups能更好的凸显某些线程的重要性,使得优先级更高的线程明确的获取到更多的时间片。
Android中的线程调度
Android系统将线程分为多个group,其中两类group尤其重要。一类是default group,UI线程属于这一类。另一类是background group,工作线程应该归属到这一类。background group当中所有的线程加起来总共也只能分配到5~10%的时间片,剩下的全部分配给default group,这样设计能保证UI线程的流畅性。
在开启线程时,可以手动将线程归于background group,这样可以保证UI线程的流程性:
1 | new Thread(new Runnable() { |
因此,在开启新线程时,需要考虑这个线程是否需要和UI线程争夺CPU资源:如果不是,降低线程优先级将其归于background group;如果是,则需要进一步的profile看这个线程是否造成UI线程的卡顿。
虽然,Android系统在任务调度上是以线程为基础单位,设置单个Thread的优先级可以改变其所属的control groups,从而影响CPU时间片的分配,但是,进程的状态的变化也会影响到线程的调度。当一个App进入后台的时候,该App所属的整个进程都将归于background group,以确保处于前台的可见进程能获取到尽可能多的CPU资源。用adb可以查看不同进程的当前调度策略。
1 | $ adb shell ps -P |
当App重新被用户切换到前台的时候,进程当中所属的线程又会回归到原来的group。在用户频繁切换的过程当中,Thread的优先级并不会发生变化,但系统在时间片的分配上却在不停的调整。
Android中的线程形态
在Android中,不同应用场景需要使用不同形态的线程,接下来介绍各种形态的线程的使用方式。
Thread
- 优点
使用最简单,适用于一些单一的只需要执行耗时任务的场景。
- 缺点
1.仅仅启动了一个新线程,没有任务的概念,不能做状态管理,任何开始后,会一直执行完毕后才结束,无法中途取消。
2.如果Runnable匿名内部类持有了外部类的引用,在线程结束前,该引用会一直存在,这样可能会阻碍外部类对象被GC回收,造成内存泄露。
3.没有通信接口,如果要与UI进行交互,需要手动实现消息机制。
- 使用方式
直接创建线程对象开启线程,代码如下所示:
1 | new Thread(new Runnable() { |
如果从UI线程启动,则该线程优先级默认为Default,归于default group,会平等的和UI线程争夺CPU资源。因此,在对UI性能要求高的场景下需要手动将其优先级设为Background:
1 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
AsyncTask
- 优点
1.将线程以任务的方式执行,可以管理任务的状态,比如,可以获取任务执行进度,以及终止任务执行。
2.内部封装了Handler,可以与UI线程直接交互。
3.开启的进程默认被设置为Background优先级,对UI线程的执行影响极小。
4.默认会在一个串行的线程池中执行任务,可以重复使用线程资源,避免了频繁开启和关闭线程带来的资源消耗。
- 缺点
1.只能对任务做粗略地控制。
2.只能与UI线程进行交互。
3.类似于直接开启Thread,也有隐式的持有外部类对象引用的问题,需要特别注意防止出现意外的内存泄漏。
- 使用方式
通过继承AsyncTask重写相应的方法:
1 | public static class MyAsyncTask extends AsyncTask<Void, Void, Void> { |
上面重写了使用AsyncTask常用的4个方法:
- onPreExecute():在主线程中执行,异步任务执行之前调用,可以做一些准备工作;
- doInBackground():在线程池中执行,并且线程的优先级会被设置设为Background;
- onPostExecute():在主线程中执行,异步任务执行完毕后调用,可以获取任务执行完毕的返回值;
- onProgressUpdate():在主线程中执行,用于获取任务执行的进度;
因为AsyncTask在内部使用了Handler(消息机制)与主进程交互, 所以异步任务定义完毕后,必须要在主线程中开启任务。开启任务的方式如下所示:
1 | new MyAsyncTask().execute(); |
- 注意事项
1.可以调用cancel()终止任务,不过,调用方法后,任务并不一定会被终止,要看方法的返回值来确定任务是否被终止。
2.Android3.0以后,为了避免并发错误,如果有多个AsyncTask任务,默认会在一个串行的线程池中执行这些任务,只有当一个任务执行完毕后才会去执行下一个任务;不过,也可以调用其executeOnExecutor()方法在并行的线程池中执行任务,但是,并行执行任务时需要处理线程安全的问题。
3.AsyncTask默认串行执行所有任务,最好不要创建了大量的AsyncTask任务,或者在AsyncTask任务中执行特别耗时的任务,这样会影响其它任务的执行。
4.不要直接调用onPreExecute()、doInBackground()、onPostExecute()和onProgressUpdate()方法。
5.一个MyAsyncTask对象只能执行一次,即只能调用一次execute()方法,否则会运行时异常。
HandlerThread
- 优点
1.可以对任务做比较精细的控制,适用于线程切换比较频繁的场景。
2.在Thread中封装了消息机制,其它线程都可以通过Handler与其进行交互。
3.类似于AsyncTask,串行执行多个任务,不存在线程安全的问题。
4.每个HandlerThread都有自己的消息队列,在一个HandlerThread中执行多个任务或比较耗时的任务,不会影响其它HandlerThread,这点与AsyncTask不同。
- 缺点
使用起来比AsyncTask复杂,需要编写更多代码。
- 使用方式
以下是使用HandlerThread的示例代码:
1 | public class MyHandlerThread<Token> extends HandlerThread { |
- 注意事项
1.HandlerThread默认被设置为Default优先级。
2.HandlerThread使用了消息机制,会一直循环处理消息,不会自动停止,如果不再使用HandlerThread时,可以通过quit()或则quitSafely()方法来终止线程的执行,避免资源消耗。
IntentService
- 优点
1.在Service组件中封装了HandlerThread,优先级(所属进程的优先级)比一般后台线程要高,不容易被系统杀死,可以用来执行优先级比较高的后台任务。
2.类似于HandlerThread,如果IntentService被开启了多次,会串行执行这些任务,不过,所有任务都执行完毕后,IntentService会自动停止。
- 使用方式
以下是使用IntentService的示例代码:
1 | public class MyIntentService extends IntentService { |
- 注意事项
停止IntentService一般使用stopSelfResult(int startId)和stopSelf()方法。其中stopSelfResult(int startId)相对比较安全,如果还有其它任务还未执行,那么会等待其它任务执行完毕后才停止服务;而stopSelf()直接停止当前服务,不管是否还有任务需要执行。
Android中的线程池
前面介绍线程的形态时,如果有多个线程,一般都是串行执行的。串行执行虽然没有线程同步的问题,但是,如果有大量的耗时任务需要执行,串行执行不是一个好的选择,这时,就需要并发的执行多个线程。在Android中,如果要并发执行线程,需要用的线程池,一般来说,线程池有以下优点:
- 重用线程池中的线程,避免了频繁创建和销毁线程带来的性能开销。
- 能够控制线程的最大并发数,避免了大量线程因相互抢占系统资源而导致的阻塞现象。
- 能对线程进行简单的管理,还能提供定制执行、或者间隔循环执行线程等功能。
不过,要特别注意处理线程安全的问题,因为多线程并发运行导致的Bug往往是偶现的,不方便调试。
ThreadPoolExecutor
Android中的线程池来源于JDK,使用Executor表示一个线程池,它是一个接口,线程池的实现类是ThreadPoolExecutor。这个实现类提供了一系列参数来配置线程池,下面是ThreadPoolExecutor的构造方法:
1 | public ThreadPoolExecutor(int corePoolSize, |
corePoolSize
线程池的核心线程数。默认情况下,核心线程会在线程池中一直存活,即使处于闲置状态。如果将线程池的allowsCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间由keepAliveTime决定,如果超时,核心线程也会被终止。maximumPoolSize
线程池所能容纳的最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞。keepAliveTime
非核心线程闲置的超时时间,如果超过这个时间,非核心线程就会被回收。如果将线程池的allowsCoreThreadTimeOut属性设置为true,这个超时也会作用于核心线程。unit
用于指定keepAliveTime的时间单位,是一个枚举量。workQueue
线程池中的任务队列,通过线程池的execute()方法提交的Runnable对象会被存储在这个队列中。threadFactory
线程工厂,为线程池提供创建新线程的能够。它是一个接口,只有Thread newThread(Runnable r)一个方法。RejectedExecutionHandler
这个参数不常用。当线程池无法执行新任务时(可能是任务队列已满,或者无法成功执行任务),线程池会调用参数的rejectedExecution()方法通知调用者,默认抛出RejectedExecutionException。
ThreadPoolExecutor执行任务一般遵循以下规则:
- 如果线程池中的线程数量未达到核心线程的数量,直接启动一个核心线程来执行任务。
- 如果线程池中的线程数量已经到达或超过核心线程的数量,那么任务会被插到任务队列中等待执行。
- 如果步骤2中的任务队列已满,但是线程数量没有达到线程池能容纳的最大线程数,那么会立刻启动一个非核心线程来执行任务。
- 如果步骤3中线程数量已达到线程池能容纳的最大线程数,那么拒绝执行此任务,调用rejectedExecution()方法通知调用者。
常用的线程池
如果直接使用ThreadPoolExecutor创建一个线程池会比较繁琐,可以使用Executors工厂类直接创建要使用的线程池实例,去实现特定功能的线程池。接下来介绍Android中常用的几种线程池。
- FixedThreadPool
通过Executors创建FixedThreadPool的源代码如下所示:
1 | public class Executors { |
它是一种线程数量固定的线程池,当其中的线程处于空闲状态时,也不会被回收,除非线程池被关闭。
由于FixedThreadPool只有核心线程并且这些核心线程都不会被回收,所以,它能够快速响应外界的请求。
- CachedThreadPool
通过Executors创建CachedThreadPool的源代码如下所示:
1 | public class Executors { |
它是一种线程数量不确定的线程池,并且只有非核心线程。当线程池中所有的线程都处于活动状态时,就立刻创建新线程处理新任务,否则,就会使用闲置的线程来处理新任务。如果某个线程空闲时间超过60s,这个闲置线程会被回收。因此,该线程池的任务队列相当于是一个空集合。
从CachedThreadPool的特性来看,它比较适合执行大量的、耗时较少的任务。
- ScheduledThreadPool
通过Executors创建ScheduledThreadPool的源代码如下所示:
1 | public class Executors { |
1 | public class ScheduledThreadPoolExecutor |
它的核心线程数量是固定的,而非核心线程数量没有限制。当非核心线程闲置时,会被回收。
ScheduledThreadPool主要用于执行定时任务,或者具有固定周期的重复任务。
- SingleThreadExecutor
通过Executors创建SingleThreadExecutor的源代码如下所示:
1 | public class Executors { |
它的线程池内只有一个核心线程,能确保所有的任务都在同一个线程中按顺序执行。
SingleThreadExecutor用于串行执行所有的任务,让这些任务之间不用考虑线程同步的问题。