前言
线程优化一直是启动优化中的一个必不可少的项目。作为一个 Android 程序员,你肯定希望应用启动的时候,火力全开,线程池拉满,每一个 CPU 核心满载而行。
可你把线程池拉满的时候,启动时长就一定会降低吗?
结果显然是否定的,之前我在进行启动优化的时候,就遇到了类似的问题。我引入了有向无环图类似的启动库后,又将线程池的数量设置为:
CPU核心数 * 2 + 1看似没什么问题,后续启动时长居然还增长了一点点。
文章图片
为什么会出现这样的问题?我们今天就好好聊聊。
一、做个实验 先做个实验,在应用启动过程中,主要做了两步:
- 主线程循环 10w 次,做一些简单的计算
- 线程池做一些异步任务,读取文件,然后将读取到的数据写入数据库,这个异步任务提交了 1000 次
2 * CPU 核心数 + 1
,变量最大线程数:- 实验一:最大线程数 =
2 * CPU 核心数 + 1
- 实验二:最大线程数 = Int.MAX_VALUE
二、基础知识 在启动流程中基础知识必不可少,从上往下讲就是线程、线程池、内核 和 CPU,这些知识都是老生常谈了。
1. 线程
线程是操作系统进行运算调度的最小单位,可以理解为它就是系统执行的任务。
作为任务,它会有各种状态:
- NEW(新建):新创建的线程,还没有启动
- RUNNABLE(可运行):可以运行的线程
- BLOCKED(阻塞):阻塞状态的线程
- WAITTING(等待):等待状态
- TIME_WAITTING(计时等待)
- TERMINATED(终止)
文章图片
处于可运行状态的线程不一定处于运行中,如果 CPU 核心数 < 线程数量,在某个时间点,处于运行中的线程数量最多也只能等于 CPU 核心数。
除此以外,只有处于可运行状态的线程才有机会获取 CPU 的青睐,从而分到时间片,得以执行。
2. 线程池
线程池的知识都很熟悉了,简单了解一下。
2.1 核心线程 简单来说,我们想了解的部分就是线程池的核心线程和非核心线程:
- 核心线程:核心线程会一直存在
- 非核心线程:当非核心线程闲置超过指定的时间,就会被销毁
- 降低资源消耗:重复利用线程,降低资源消耗
- 提供响应速度:任务一来就执行
- 管理好线程资源:避免无节制的使用线程,引发性能问题
2.2 任务划分 我们经常将任务分为 I/O密集型 和 CPU密集型 任务,那么这两种有什么区别呢?
I/O 密集型任务指的是该任务的大部分时间用来提交 I/O 请求或者等待 I/O 请求。这类任务常常运行很短暂的一会儿,然后进入阻塞状态,等待更多的 I/O 请求。常见的如数据库操作、网络操作、键盘事件、屏幕操作等。
CPU 密集型任务指的是任务的大部分代码用来执行代码。该类任务常常会一直运行并占用着 CPU,直到时间片用完。常见的如数据计算、无限循环等。
那线程数如何设置?我们下面再去讲。
3. 内核
哪个线程先运行?什么时间运行?运行多久?这些都是调度程序说了算!
3.1 调度程序 调度程序是一个内核子系统,它是多任务操作系统的基础。多任务操作系统就是能够同时并发地交互执行多个进程的操作系统。
即使是单核处理器,它也可以并发的处理多个任务,只不过在一个时间点,只有一个正在执行的任务。
就好比安卓开发小王,身背几个需求,被产品要求同一天上线,虽然也能够完成,但他在某个时间点,只能写一个需求,如果想一个时间点同时进行两个需求,那得加人,也就是我们通常说的双核处理器,这就具备了并行的能力。
文章图片
3.2 抢占式和非抢占式 多任务操作系统可以分为两种类型:非抢占式多任务和抢占式多任务。
Android 使用的是抢占式多任务,在这种模式下,每个任务都会被分配到一定的时间用来执行,一旦时间片用完,就会自动切换到下一个任务,分配的时间我们称之为时间片。
还拿小王来举例,小王身背三个需求,每天的计划中,上午需求 A,下午需求 B,晚上需求C。到了下午,即使需求 A 没做完,也要去做需求 B,这样可以保证了每个需求每天都会有进度。
从启动的角度来说,我们肯定不希望主线程和子线程分得同样的时间片,这可能会让我们的应用看着很慢。
为了给主线程分得更长的时间片,每个进程都有一个 nice 值,它会影响时间片的分配,但我们改不了这个,我们能够处理的就是给线程设置优先级,Android 中线程的优先级从 -19 到 19,值越低代表优先级越高,分得的时间片也就越长。
3.3 线程多了会怎么分配 上面的这些东西看似和我们应用层开发没关系,实则不然。
比如线程数量多了以后,我们先拿小王举例:
原先小王手里有 5 个需求,每个 2 天工时,做完一个再做下一个,10天能搞定。
现经理要求他同时开发 5 个需求,保证 5 个需求每天都有进度,那可就麻烦了,先不算 10 天开发时间,还得加上如下时间:
- 每天切其他四个项目时间成本
- 思考时间:每次切到下一个项目,都会想上次开发到哪,上次的思路是什么
线程多了,也会有这样的问题,每次切换时间片都是成本。另外,线程的闲置率会上升,像这样运行 14ms 要等 185 ms:
文章图片
还拿小王来看,原先五个需求,桉顺序做,每个需求的生命周期就 2 天,但是并行开发后,每个需求的生命周期都拉长了,到了 12 天左右。对于启动的主线程来讲可不是好事!
理想的情况应该是量力而行,当小王开发一个需求遇到问题需要等产品回复而停滞,在等待的这段时间内,开发另外一个需求,知道产品回复完,再找一个合适的时间切回来,这样,反而会提升效率,将工作时间缩短到 9 天。
4. CPU
在2022年发布的 Android 低端机上,也都标配了 8 核心的 CPU,核心数越多,就意味着并行能力越强。
注意,这里用的是并行,而不是并发。
文章图片
一个核心,就代表着团队只有一个开发,8 核代表着团队有八个开发,意味着一个时间点最高可以有8个需求同时进行。
二、线程数如何设置 上面说了那么多,大家最想知道的就是线程数如何设置。
一般而言,核心线程数和最大线程数都设置为 CPU核心数 * 2 +1 ,阻塞队列使用
LinkedBlockingDeque
。1. 任务因素
但这个数字肯定不是绝对的,我们需要考虑到 CPU 密集型任务 和 IO 密集型任务的区别。
如果我们使用子线程都是处理网络、数据库、读文件等操作,这个数字就可以设置大一点;如果子线程仅执行一些耗时的计算代码,这个数字就可以设置小一点。
2. 任务闲置
即使我们自己设置的线程池没什么问题,但程序一启动,任务执行时候的线程闲置率一看就知道还有问题,比如这张图:
文章图片
为什么会出现这种闲置率太高的情况,原因可能如下:
- 过多使用
New Thread
或者不节制的使用线程池 - 很多第三方 SDK 都使用自身的线程池或者线程
推荐大家使用 Profiler,好处可太多了:
- 可以查看线程总数
- 可以查看CPU的负载情况
- 可以查看每个任务的闲置率
- ...
public void test{
Trace.beginSection("名称");
//... 代码省略
Trace.endSection();
}
对每个方法做上述过程确实太麻烦,所以都是配合函数插桩使用。
另外一个就是使用 Shell 命令,我们可以在 Android Studio 中 Logcat 窗口看到应用的进程 Id,进入 adb shell 后,就可以通过输入命令
cat /proc/{进程ID}/schedstat
查看:emulator64_x86_64_arm64:/ $ cat /proc/7775/schedstat
5511910111 2055599424 6712
// 参数一 CPU运行时间
// 参数二 该进程等待时间
// 参数三 主动切换和被动切换的次数
这些数据只能够我们查看大概的情况。
总结 关于线程我们能做的并不多,尽量去收敛线程:
- 禁止使用 New Thread 方式去创建线程
- 统一应用内线程池,并制定合适的核心线程和最大线程数量
- 编写公司库的时候,如需使用线程池,提供设置线程池的接口
- 可以设置自身线程池的第三方库,优先设置应用内线程池,比如 OkHttp
- Hook 第三方库使用
New Thread
,改为应用内线程池 - 能懒加载尽量懒加载第三方库,避免过早的竞争系统资源
本文由博客一文多发平台 OpenWrite 发布!
推荐阅读
- android|android studio 布局嵌套,Android Studio实战 - 设计布局之嵌套布局
- Android|第67篇 Android Studio实现聊天记录界面-ListView多形式界面
- Android-基础|Android基础教程之-----布局
- 【华为游戏多媒体】调用获取Token接口得到的Token值是null
- 移动开发|校友在美团 Android 岗的四面分享~
- android|记录JAVA中Calendar类的一个问题
- android逆向|第一次进行android逆向的过程记录
- Flutter项目开发|Flutter小技巧总结之SingleChildScrollView里面嵌套Column和ListView时候,ListView不显示
- Android开发|Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套