第 11 章 Android 的线程和线程池
线程分为主线程和子线程,主线程主要处理和界面相关的事情,子线程用来执行耗时操作。
在 Android 中,如果主线程中执行耗时操作就会导致程序无法响应,因此耗时操作必须放在子线程中去执行。
除了传统的 Thread 外,AsyncTask、IntentService、HandlerThread 也是特殊的线程
AsyncTask 底层用到了线程池,IntentService 和 HandlerThread 底层则直接使用了线程
AsyncTask 封装了线程池和 Handler,为了方便开发者在子线程中更新 ui
HandlerThread 是一种具有消息循环的线程,它内部可以使用 Handler
IntentService 是一个服务,系统对其进行封装使其更方便地执行后台任务,内部采用 HandlerThread 来执行任务,执行完毕后 IntentService 会自动退出。(服务,不容易被系统杀死)
操作系统中,线程是操作系统调度的最小单元,同时又是一种受限的系统资源,不能无限制地产生,它的创建和销毁都有相应的开销。
系统中有大量线程时,系统通过时间片轮转
调度每个线程,因此线程不可能做到绝对并行,除非线程数小于等于 cpu 的核心数量。
用线程池来管理线程是一个高校的做法,它缓存一定数量的线程,避免频繁创建销毁带来的开销。
Android 线程池来源于 Java,主要通过 Excutor 来派生特定类型的线程池。
11.1 主线程和子线程
主线程是指进程所拥有的线程,在 Java 中默认情况下一个进程只有一个线程,这个线程就是主线程。处理界面交互的逻辑,用户随时都会和界面交互,所以主线程必须有较高的响应速度,否则就会产生一种界面卡顿的感觉。
为了保持较高的响应速度,这就要求主线程不能执行耗时任务,这个时候子线程就派上用场了。子线程也叫工作线程,除了主线程以外的线程都是子线程。
Android 沿用了 Java 的线程模型,主线程也叫 UI 线程。
主线程作用是运行四大组件以及处理他们和用户的交互
子线程的作用是处理耗时任务:网络请求、I/O操作等
Android 3.0 开始,网络访问必须在子线程中进行,否则会抛 NetworkOnMainThreadException
异常。
11.2 Android 中的线程形态
11.2.1 AsyncTask
轻量级异步任务类,可以在线程池中执行异步任务,执行结果传递给主线程并在主线程更新 ui。
实现上来说,它封装了 Thread 和 Handler,通过它可以很方便的执行后台任务以及在主线程中访问 ui,但它不适合进行特别耗时的任务。
AsyncTask 是一个抽象的泛型类,提供了 Params、Progress、Result 这三个泛型参数。Params
表示参数的类型,Progress
表示后台任务的执行进度类型,Result
表示后台任务的返回结果类型。如果不需要具体参数,可以传 Void 代替。
AsybcTask 提供了 4 个核心方法:
onPreExecute()
,在主线程中执行,异步任务执行之前,此方法会被调用,一般用于做一些准备工作doInBackground(Params...params)
,在线程池中执行,用于执行异步任务,params 参数表示异步任务的输入参数。在此方法中可以通过publishProgress
方法更新任务进度,publishProgress 方法会调用onProgressUpdate
方法。另外此方法需要返回计算结果给onPostExecute
方法onProgressUpdate(Progress...values)
,在主线程中执行,当后台任务的执行进度发生改变时此方法会被调用。onPostExecute(Result result)
,在主线程中执行,在异步任务结束之后会被调用,result 是后台任务的返回值,即 doInBackground 的返回值。
上边的方法, onPreExecute
限先执行,接着是 doInBackground
,最后才是 onPostExecute
。AsyncTask 还提供了 onCanceled()
方法,同样在主线程执行,当异步任务被取消时,onCanceled() 方法被调用,这时 onPostExecute 就不会被调用了。
AsyncTask 的使用限制:
- AsyncTask 类必须在主线程中加载,意味着第一次访问 AsyncTask 必须在主线程。从 android 4.1 开始这个操作已经被系统自动完成。在 ActivityThread 的 main 方法中,调用了 AsyncTask 的 init 方法。(android 5.0 源码是这样的,而 api-27 构造方法里会拿到需要的 Handler,已经没有了 init 方法)
- AsyncTask 的对象必须在主线程中创建。
- execute 方法必须在 ui 线程调用
- 不要在程序中直接使用 onPreExecute()、onPostExecute()、doInBackground() 和 onProgressUpdate() 方法。
- 一个 AsyncTask 对象只能执行一次,即只能调用一次 execute 方法,否则会报运行时异常。
- Android 1.6 之前,AsyncTask 是串行执行任务,Android 1.6 的时候 AsyncTask 开始采用线程池里处理并行任务;Android 3.0 开始,为了避免 AsyncTask 所带来的并发错误,AsyncTask 又采用一个线程来串行执行任务。Android 3.0 以后的版本可以通过 executeOnExecutor 方法并行执行任务
11.2.2 AsyncTask 的工作原理
看看构造方法做了啥
api-27AsyncTask#execute(Params... params)
api-27sDefaultExecutor
是SerialExecutor
这个线程池的实例,看一下它的源码。SerialExecutor 线程池
api-27任务执行的时候,会调用 FutureTask 的 run 方法,看一下源码
FutureTask.run()
api-27WorkerRunnable.call()
api-27接下来接着看 InternalHandler 是怎么处理消息的
InternalHandler
api-27InternalHandler 是在 AsyncTask 的构造方法中创建的,因为构造方法必须在主线程调用,所以这个 Handler 所用的 Looper 也是主线程的 Looper,所以它可以完成切换到主线程的任务
还看到 InternalHandler 处理了更新任务进度的消息,更新任务进度是用户在
doInBackground(Params... params)
方法里手动调用publishProgress(Progress... values)
方法,publishProgress 通过发消息给 InternalHandler 完成线程切换去在主线程更新进度 ui。
以上,AsyncTask 的整个工作流程就分析完毕了。
AsyncTask 在 Android 3.0 以上是默认串行执行的的,想要达到并行的效果的话,可以直接调用 executeOnExecutor(Executor exec, Params... params)
传入执行任务的线程池 AsyncTask.THREAD_POOL_EXECUTOR
。
11.2.3 HandlerThread
继承自 Thread,是一种可以使用 Handler 的 Thread,它的实现是在 run 方法里通过 Looper.prepare() 来创建消息队列,并通过 Looper.loop() 开启消息循环,这样在实际使用中就允许在 HandlerThread 中创建 Handler 了。
HandlerThread.run()
api-27@Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; }
内部创建了消息队列,外界需要通过 Handler 的消息方式来通知 HandlerThread 执行一个具体的任务。
在 Android 中的一个具体使用场景是 IntentService。
HandlerThread 的 run 方法是一个无限循环(因为里边创建了 Looper),因此当明确不再使用 HandlerThread 的时候,可以通过它的 quit 或 quitSafely 方法来终止线程的执行。
11.2.4 IntentService
是一种特殊的 Service,继承于 Service 并且是一个抽象类,所以必须创建它的子类才能使用它。
IntentService 可以执行后台耗时任务,任务执行完后它会自动停止。它是一个服务,优先级比较高不容易被系统杀死
IntentService#onCreate()
api-27每次启动 IntentService,它的 onStartCommand 方法就会调用一次,看一下源码
IntentService#onStartCommand()
api-27多次启动 IntentService 就会多次调用 onStartCommand 方法,把任务依次放入 HandlerThread 的消息队列中。
把启动 Service 的 Intent 交给了 ServiceHandler 去处理,看一下它的源码
ServiceHandler
api-27如果目前只存在一个后台任务,那 onHandleIntent 执行完后,stopSelf 就会立即停止服务;如果有多个后台任务,当 onHandleIntent 执行完最后一个任务时,stopSelf 才会直接停止服务。
IntentService 是顺序执行后台任务的,当有多个后台任务同时存在时,会按照外界发起的顺序排队执行。
11.3 Android 中的线程池
线程池的优点:
- 重用线程池里的线程,避免因为线程的创建和销毁所带来的性能开销
- 有效控制线程池最大并发数,避免大量线程之间互相抢占资源导致阻塞
- 对线程进行简单的管理,提供定时执行和指定间隔循环执行等功能
Android 中线程池概念来自 Java 中的 Executor,Executor 是一个接口,真正的线程池的实现为 ThreadPoolExecutor。
ThreadPoolExecutor 提供一系列参数来配置线程池。
11.3.1 ThreadPoolExecutor
它的常用的一个构造方法
api-27public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); }
corePoolSize
线程池的核心线程数。默认情况核心线程会在线程池中一直存活,即使他们处于闲置状态。如果将 ThreadPoolExecutor 的
allowCoreThreadTimeOut
属性设置为 true,那闲置的核心线程在等待新任务到来时会有超时策略,时间间隔由keepAliveTime
所指定,等待时间超过 keepAliveTime 所指定时间后,核心线程会被终止。maximumPoolSize
线程池所能容纳的最大线程数,当活动线程达到这个数值后,后续的新任务将会被阻塞。
keepAliveTime
非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当 ThreadPoolExecutor 的
allowCoreThreadTimeOut
属性设置为 true 时,keepAliveTime 同样会作用于核心线程。unit
用于指定 keepAliveTime 参数的时间单位,这是一个枚举,常用的有 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)以及 TimeUnit.MINUTES(分钟)等。
workQueue
线程池中的任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中。
threadFactory
线程工厂,为线程池提供创建新线程的功能。ThreadFactory 是一个接口,它只有一个方法:
Thread newThread(Runnable r)
除了以上参数外,还有一个不常用的参数
RejectedExecutionHandler handler
。当线程池无法执行新任务时,这可能是任务队列已满或者是无法成功执行任务,这个时候 ThreadPoolExecutor 会调用 handler 的 rejectedWxecution 方法来通知调用者,默认情况下 rejectedExecution 方法会直接抛出异常 RejectedExecutionException。ThreadPoolExecutor 为 RejectedExecutionHandler 提供了几个可选值:CallerRunsPolicy、AbortPolicy、DiscardPolicy 和 DiscardOldestPolicy
,其中 AbortPolicy 是默认值,它会直接抛出异常。
ThreadPoolExecutor 执行任务时大致遵循如下规则:
- 如果线程池中的线程数量
未达到核心线程的数量
,会直接启动一个核心线程
来执行任务。 - 若已经
达到或超过核心线程数量
,人物会被插入任务队列
。 - 如果上一步中无法插入任务队列,说明
任务队列已满
,这时如果线程数未达到线程池规定的最大值
,那么会立刻启动一个非核心线程
来执行任务。 如果上一步中
线程数量已经达到线程池规定的最大值
,就拒绝执行任务
,调用 RejectedExecutionHandler 的 rejectedExecution 方法来通知调用者。AsyncTask 中线程池的配置情况
api-27
11.3.2 线程池的分类
FixedThreadPool
通过 Executors 的 newFixedThreadPool 方法来创建
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }
看创建方法可知:核心线程数量和最大线程数相等,只有核心线程,核心线程空闲状态也不会被回收;任务队列没有大小限制,所有线程都处于活动状态时,新任务会等待。
它可以更加快速的响应外界的请求。
CachedThreadPool
通过 Executors 的 newCachedThreadPool 方法来创建。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
它只有非核心线程、线程数量是 int 的最大值、限制超时时间 60 秒、任务队列大小不限制(线程数量最大值,可能用不到任务队列)
任何任务都将立即被执行。
它比较适合执行大量的耗时较少的任务
,闲置状态时,所有线程超时都会被停止,几乎不占用系统资源ScheduledThreadPool
通过 Executors 的 newScheduledThreadPool 方法来创建
public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); } public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue(), threadFactory); }
核心线程数固定、最大线程数无限制、空闲的非核心线程立即回收。
用于执行定时任务和具有固定周期的重复任务。
SingleThreadExecutor
通过 Executors 的 newSingleThreadExecutor 方法来创建
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
只有一个核心线程,来任务等待,空闲不会超时回收。
意义在于统一所有的外界任务到一个线程中,在这些任务之间不需要处理线程同步的问题。