前言
近来空闲的时候阅读了 Google官方文档中的 性能章节,该章节为我们讲解了很多性能方面的基础知识 以及 如何从多个维度对我们的应用进行性能优化。
不过官方文档中很多地方讲得比较"生硬",有点Google翻译软件直译那味,而且很多知识都是一笔带过,例如文档讲了当组件(Activity/Service)被销毁时我们应该管理好Thread的存亡,但并没告诉我们如何管理;讲了我们使用线程时应该设置线程的优先级,但没告诉我们如何使用好线程优先级,Android线程优先级与Java线程优先级有什么区别等等,于是有了这篇文章~
本文基于 Google官方文档中的 性能章节 中的 通过线程提升性能,对其内容进行解读和演绎,有些知识虽然基础,但很多人未必能很好得区分他们的应用场景,所以有必要对它们进行深入的学习。
主线程的概念
应用启动的时候会创建一个Linux进程和一个主线程,主线程的唯一工作就是从线程安全工作队列获取工作块并执行,直到应用被终止,框架会从多个位置生成部分工作块。这些位置包括与生命周期信息、用户事件(例如输入)或来自其他应用和进程的事件相关的回调
ANR的简单概念
- 系统每16ms会去执行一次刷新,当主线程无法在16ms内完成一个工作块的时候,系统就会出现卡顿,如阻塞5s会出现ANR,所以这些会超时的工作块应该放到子线程中去处理。本文重点关注线程相关的知识,ANR不展开讲,后续有时间会写写ANR相关的原理分析。
线程和界面对象引用
为什么子线程不能刷新界面?两个原因:显示引用 与 隐式引用
显示引用如
- 子线程中操作View对象,而该View对象持有Activity对象的引用,则当Activity被销毁时会因为仍被引用而无法被正常回收
- 子线程持有Activity引用,此时发生Activity的生命周期时间,如屏幕旋转,而Activity重新创建的过程中由于无法被及时回收而可能出现两个Activity对象
所以我们不该在子线程中包含对界面对象的显式引用,否则容易发生内存泄漏 以及 线程处理争用(多线程操作同个View对象的属性导致属性不一致问题)
隐式引用如
- 在Activity中使用非静态内部类如
AsyncTask 或 Handler
,而非静态内部类会隐式持有外部内的实例(这也是为什么内部类能直接使用外部类方法 与 外部类成员变量的原因),这个时候如果Activity发生销毁,若内部类还有事务在处理,则会导致外部类回收失败,解决方法是使用静态内部类,因为静态内部类是应用级别的,不隐式持有外部类实例
基于以上两种引用类型可能导致的问题,如果允许在多个线程中进行界面刷新操作,很容易因为并发带来界面效果不一致 或者 因为状态不同步而带来奔溃问题,所以 Android 对界面刷新线程做了统一,即只允许在主线程做刷新,这个限制是合理的。
线程和应用 Activity 生命周期
Activity销毁时是否该保留线程?
提出该问题是因为,当我们的Activity被销毁的时候,仍在运行的子线程中的工作不一定立刻就变成无意义的,想象两种情况:
- 1.如果我们的子线程中的任务的最终目的是得出一个结果,然后根据该结果刷新UI界面,这个时候由于界面已经都不存在了,自然也不需要刷新,所以这个结果是没用的,该子线程可被同时销毁;
- 2.如果我们的子线程做的是一个图片下载的操作,而我们希望在图片下载完成的时候将该图片缓存起来,这个时候就不应该立即销毁子线程,而应该待其下载完成并缓存结束后再销毁。
综上,子线程的销毁与否需要我们根据任务的目的,进行手动管理。
拓展:如何手动管理子线程的存亡?
先思考这样一个问题:Activity 中构建了一个 Thread 去执行一个耗时任务,假定 Thread 对象未对 Activity 进行持有,并在该耗时任务未执行完之前,Activity 就被销毁了,此时 Thread 对象会被销毁吗?
答案是不会,为什么呢?Activity
的销毁其实就是在主线程上触发Activity
的onDestroy
方法的执行,而 new
出来的Thread
是运行在非主线程上的,并不受主线程上Activity
的生命周期的影响。只有当线程任务执行完了,或者当前进程被销毁了,该线程才会销毁。
解答了这个问题,我们就知道如何手动管理子线程的存亡了,针对上面两种情况:
- 1.**子线程可被同时销毁的情况:**这种情况下我们可以在
Activity
的onDestroy
中主动销毁线程,主动销毁线程通常有以下三种方式,使用退出标志 / 使用interrupt() / 使用Thread.stop() -- 有风险,慎用!可参考Java结束线程的三种方法 - 2.**子线程不随组件声明周期存亡的:**这种我们就不需要手动去销毁线程了,待
Thread.run()
方法执行完线程对象便会被销毁。
可以看到,手动管理线程的存亡是复杂的,很可能会遇到内存争用和性能问题,这时可以使用 ViewModel + LiveData
加载数据并在数据发生更改时发出通知,简单说就是将数据处理都放到ViewModel
中去,但是ViewModel
中不持有View相关的引用,然后View通过观察者模式观察数据的变化,解耦数据处理与组件生命周期事件,这样能让我们的应用更稳健,详细的使用可以参考 应用架构指南
线程优先级
拓展:Java线程优先级 与 Android线程优先级 的区别
我们都知道Thread是可以设置优先级的,0最低,5默认,10最大,但这其实并不是我们下面要讲的线程优先级,因为这是Java的线程优先级,而 Android 为了对线程优先级有更细粒度的控制,提供了一套Android API 来控制线程的优先级,而我们 开发的时候应该选择 Android API 而不是 Java 接口来设置线程的优先级。
那这两者有什么区别吗?答案是,这两者是不相关的,意味着你通过Thread.setPriority()
来设置线程优先级并不影响Process.getThreadPriority()
拿到的当前线程的优先级。
其实我们平时分析 ANR 文件的时候,经常会看到这两个优先级:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72436488 self=0x73beec2c00
| sysTid=19465 nice=-10 cgrp=default sched=0/0 handle=0x73c044aed0
| state=S schedstat=( 4806740796 1696513035 8394 ) utm=279 stm=201 core=5 HZ=100
| stack=0x7ff19de000-0x7ff19e0000 stackSize=8192KB
| held mutexes=
上面 traces 文件中,prio=5
即是Java API的线程优先级,而nice=-10
则是Android API的线程优先级
有了上面的预备知识,接下来看看文档内容,首先是线程的默认优先级,一个线程被创建时,未被干预的情况下其默认优先级为创建它的线程的优先级。
系统的线程调度程序会优先考虑优先级较高的线程,当我们在应用中创建和管理线程时,我们应该通过Process.setThreadPriority(Process.xxx)
为线程设置一个合理的优先级。
为什么说要设置一个合理的优先级呢?遇事max它不香?NO NO NO,如果子线程的优先级设置得太高,则主线程和RenderThread
的正常运行会受影响,对用户体验而言就是应用掉帧;而如果设置得过低,则任务的完成时间可能会过长,这显然也是不行的。
**拓展:**Android API 线程优先级范围、如何选择、线程优先级增量器的使用 以及 如何提升某段代码的执行优先级
- Android API 线程优先级范围
Process
类设置了一组常量供开发者设置线程优先级,Android 10 上数值的范围为 [-20, 19],数值越大优先级越低,我们看看这组优先级常量
// 最低优先级,表明若当前有任何其他任务在处理,则不要运行该线程(有点IdleHandler的意思),支持开发者设置
public static final int THREAD_PRIORITY_LOWEST = 19;
// 后台线程优先级,稍微比默认优先级低,当我们的任务可能会影响用户界面的流畅性的时候就应该使用该优先级,如耗时IO操作,支持开发者设置
public static final int THREAD_PRIORITY_BACKGROUND = 10;
// 比 THREAD_PRIORITY_DEFAULT 略低,支持开发者设置
public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1;
// 默认线程优先级,比前台线程低,比后台线程高,支持开发者设置
public static final int THREAD_PRIORITY_DEFAULT = 0;
// 比 THREAD_PRIORITY_DEFAULT 略高,支持开发者设置
public static final int THREAD_PRIORITY_MORE_FAVORABLE = -1;
// 前台线程优先级,表示当前处于一个正与用户进行交互的界面,不支持开发者设置,当我们的页面来到前台时系统会自动去帮我们设置该优先级
public static final int THREAD_PRIORITY_FOREGROUND = -2;
// 显示线程,优先级高于前台线程,同样不支持开发者设置,系统会视情况调整
public static final int THREAD_PRIORITY_DISPLAY = -4;
// 显示线程,不同于上一个的是这个用于处理紧急的显示事件,同样不支持开发者设置,系统会视情况调整
public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;
// 视频线程,同样不支持开发者设置,系统会视情况调整
public static final int THREAD_PRIORITY_VIDEO = -10;
// 音频线程,同样不支持开发者设置,系统会视情况调整
public static final int THREAD_PRIORITY_AUDIO = -16;
// 音频线程,最高优先级,同样不支持开发者设置,系统会视情况调整
public static final int THREAD_PRIORITY_URGENT_AUDIO = -19;
首先可以看到,Android API提供的这些线程优先级中,-1 以下的值是不允许开发者使用的,这是由系统线程调度程序去使用的,我们能用的就是 -1 以上的这些优先级,总共有5个(开发者可控制的实际范围并不只有5,只是Android提供了五个常量供我们选择,见下文THREAD_PRIORITY_LESS_FAVORABLE
的相关讲解),如何选择呢?
- 如何选择
1.THREAD_PRIORITY_LOWEST:
任务极不重要,我们希望系统"闲时处理"的,就可以使用该级别,这个很像消息机制中的IdleHandler
2.THREAD_PRIORITY_BACKGROUND:
任务不紧急又耗时的,可以放到这种类型的线程中去处理
3.THREAD_PRIORITY_DEFAULT:
默认优先级,如果希望任务以正常的速度处理,可以设置该优先级
4.THREAD_PRIORITY_LESS_FAVORABLE / THREAD_PRIORITY_MORE_FAVORABLE:
这两个是增量器,我们看看源码一般是怎么用这两个的:
/*frameworks/base/media/java/android/media/MediaPlayer.java*/
// process each subtitle in its own thread
final HandlerThread thread = new HandlerThread("SubtitleReadThread",
Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
/*external/apache-http/android/src/android/net/http/ConnectionThread.java*/
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_DEFAULT +
android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
/*frameworks/rs/support/java/src/androidx/renderscript/RenderScript.java*/
public enum Priority {
LOW (Process.THREAD_PRIORITY_BACKGROUND + (5 * Process.THREAD_PRIORITY_LESS_FAVORABLE)),
NORMAL (Process.THREAD_PRIORITY_DISPLAY);
...
}
整个Android 10源码只看到这三处应用,从这些应用我们也可以知道,通过Android API设置线程优先级不只有上面列出的那几个数值,在系统允许的范围内([-1, 19])我们是可以通过THREAD_PRIORITY_LESS_FAVORABLE / THREAD_PRIORITY_MORE_FAVORABLE
这两个增量器来进行数值调整的。
- 如何提升某段代码的执行优先级
有些场景下我们希望某线程中的某段代码的执行优先级高一些,其他代码的优先级保持不变,这个时候就可以用到Process
类提供的两个接口:
Process.getThreadPriority(int tid)
Process.setThreadPriority(int priority)
直接看个源码应用例子吧:
/*frameworks/base/services/core/java/com/android/server/net/NetworkPolicyManagerService.java*/
private void initService(CountDownLatch initCompleteSignal) {
final int oldPriority = Process.getThreadPriority(Process.myTid());
try {
// Boost thread's priority during system server init
Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
......
} finally {
// Restore the default priority after init is done
Process.setThreadPriority(oldPriority);
}
}
这个用法很好理解也很实用,在这个例子中就是 将这段代码的线程优先级暂时提升到前台线程级别,很像之前写过的 这篇文章 中提到的 Binder.clearCallingIdentity() 与 Binder.restoreCallingIdentity(id)
这两个接口的用法对吧
线程处理的辅助类
Android 封装了几个类AsyncTask、HandlerThread、ThreadPoolExecutor
,用于让开发者方便得执行线程相关的操作,相信大家都或多或少使用过,下面来看看:
AsyncTask 类
当我们的业务中有 快速将任务从主线程移动到工作线程的场景时,例如用户的操作触发了图片的下载操作,该操作可能是耗时和阻塞的,所以我们应该将下载操作放在子线程中去处理,当下载完成后,我们又需要回到主线程去显示图片,这个时候AsyncTask
是个很好的选择。
AsyncTask
的具体用法很简单,这里不展开见,不熟悉的童鞋可以看看 Carson_Ho 的 Android 多线程:这是一份详细的AsyncTask使用教程,需要注意的一点是 AsyncTask
的多个回调方法中,只有doInBackground
是在子线程中运行的,其他回调都是发生在UI线程。
本文关注AsyncTask
的一些注意项:
- 默认情况下,应用会将其创建的所有 AsyncTask 对象推送到单个线程中串行执行,这意味着当某个 Task 特别耗时时就会发生队列阻塞的情况。对此Google的建议是:仅使用 AsyncTask 处理持续时间短于 5ms 的工作。所以实际应用中,当我们的任务较为复杂时,可以通过使用工具systrace来查看任务的执行时长,从而决定是否使用
AsyncTask
- 潜在的隐式引用问题:前文我们讲隐式引用的时候就说到了这个问题,当我们在组件内创建非静态
AsyncTask
内部类时,容易出现隐式持有View对象导致回收问题,可以通过使用静态内部类来解决这个问题 - 潜在的显式引用问题:由于
AsyncTask
中多个回调都是发生在UI线程的,一般我们会想在这些回调中做界面刷新操作,这个时候就容易出现显式引用问题,也就是持有View对象时可能带来的内存泄漏 以及 线程处理争用问题,该问题可以通过使用弱引用WeakReference
来解决
HandlerThread
AsyncTask
很好用,因为它帮我们做了那些重复的工作,线程创建、Handler和消息队列的处理、线程切换等,但它不是万能的,上面分析了,AsyncTask
是在一个线程中串行工作的,这限制了任务的处理时长,这个时候我们就可以考虑使用HandlerThread
了。
HandlerThread
帮我们封装了Thread类 + Handler类机制
,内部原理为:
- 1.通过继承Thread类,快速地创建1个带有Looper对象的新工作线程
- 2.通过封装Handler类,快速创建Handler & 与其他线程进行通信
使用也很简单,可参考 Carson_Ho 的 Android多线程:HandlerThread详细使用手册
同样的,我们关注HandlerThread
的注意项:
- 使用
HandlerThread
创建线程时,最好根据线程正在执行的工作类型设置其优先级。因为 CPU 只能并行处理少量线程,设置优先级有助于系统 了解我们的任务的紧急程度 从而做出更优的处理优选级选择,相关接口为:
/*frameworks/base/core/java/android/os/HandlerThread.java*/
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}
ThreadPoolExecutor
有些时候我们需要使用大量的线程来处理复杂的任务,例如,为 800 万像素图片的每个 8x8 块计算滤镜,这个时候使用多线程能加快任务处理,但很显然AsyncTask 和 HandlerThread
都是不合适的,AsyncTask
的串行处理并不能加快任务处理,而使用HandlerThread
则需要我们在一组线程之间手动实现负载平衡,这个时候ThreadPoolExecutor
是个好的选择。
ThreadPoolExecutor
类可用于管理一组线程的创建,设置其优先级,并管理工作在这些线程之间的分布情况。随着工作负载的增减,该类会创建或销毁更多线程以适应工作负载。
具体的用法和使用常见可以看下 Carson_Ho 的 Android 多线程: 完全解析线程池ThreadPool原理&使用,讲得非常详细,这里借用他的一张图总结下Java内置的4种常见线程池,基本能满足绝大多数的应用场景了:

应该创建多少线程?
尽管在软件层面上,您的代码可以创建数百个线程,但这样做会导致性能问题。您的应用与后台服务、渲染程序、音频引擎、网络等共享有限的 CPU 资源。CPU 实际上只能并行处理少量线程;一旦超限便会遇到优先级和调度问题。因此,务必要根据工作负载需求创建合适数量的线程
如何得知什么是合适的数量呢?
Google的建议是采用试错法,比如为你的复杂任务分配 4 个线程,然后使用 Systrace 查看该任务的运行时间,进而我们就可以知道 4 这个数量合不合适了,前面我们讲 AsyncTask
的时候说过,5ms 是一个线程处理时长的一个比较合适的最大值,所以我们可以以 5ms 为单线程最大处理时长标准,利用 Systrace, 不断试错得出一个合适的数值。