前面讲过,使命调度是影响速度的实质因素之一,这一章咱们就来讲讲使命调度的优化。

针对使命调度,咱们很天然地就能想到:进步使命的优先级或许削减使命调度的耗时这两条优化方法论。削减调度耗时有不少优化计划,比方线程保活和运用协程等,这些咱们都在前面的章节中讲过了,也就不再重复了。

这一章,咱们就环绕怎么进步使命的优先级,来介绍 2 种优化计划:

  1. 进步中心线程的优先级;

  2. 中心线程绑定 cpu 大核。

进步中心线程优先级

想要进步线程的优先级,咱们需求先了解线程优先级这一概念的原理。第二章咱们讲过,Linux 中的进程分为实时进程和一般进程这两类。实时进程一般经过 RTPRI(RealTimeRriority) 值来描绘优先级,取值规模是 0 到 99。一般进程一般运用 Nice 值来描绘进程的优先级,取值规模是 -20 到 19。可是为了架构设计上的统一, Linux 体系会将 Nice 对齐成 Prio 值,即 Nice 取 -20 时,该进程的 Prio 值为 0 ,此刻它的优先级仍然比任何一个实时进程的优先级都要低。由于线程的实质便是进程,因而上述优先级规矩也适用于线程。

速度优化:任务调度优化

咱们能够经过进入手机的 shell 界面,履行 ps -p -t (高版别体系上这个指令或许失效了)检查一切进程的 Nice 值和 Prio 值。部分数据如下:

ps -p -t
USER      PID  PPID     VSIZE  RSS   PRIO  NICE  RTPRI SCHED   WCHAN    PC          NAME
root      393   1     1554500 5256    20    0     0     0     ffffffff 000 S       zygote
system    762   328   338336  9844    12    -8    0     0     ffffffff 00000000 S surfaceflinger
……
//测验demo的主线程和烘托线程
u0_a45    16632 393   2401604 60140   20    0     0     0     ffffffff 00000000 S com.example.test
u0_a45    16725 16632 2401604 60140   16    -4    0     0     ffffffff 00000000 S RenderThread
……

Android 中只要部分底层中心进程才是实时进程,如 SurfaceFlinger、Audio 等进程,大部分的进程都是一般进程,从上面数据也能够看到,咱们 Demo 主线程的 Nice 值默以为 0,烘托线程的 Nice 值默许值为 -4。咱们无法将一般进程调整成实时进程,也无法将实时进程调整成一般进程,只要操作体系有这个权限。但有一个破例,在Root 手机中,将 /system 目录下的 build.prop 文件中的 sys.use_fifo_ui 字段修正成 1 , 就能 将运用的主线程和烘托线程调整成实时进程,不过这需求 Root 设备才干操作,正常设备这个值都是 0,计划不具备通用性,就不打开讲了。

运用中的一切线程都归于一般进程的等级,所以针对线程优先级这一点,咱们仅有能操作的便是修正线程的 Nice 值了,而且咱们有两种方法来调整线程的 Nice 值。

调整线程优先级的方法

咱们有 2 种方法改变线程的 nice 值:

  1. Process.setThreadPriority(int priority) / Process.setThreadPriority(int pid,int priority);

  2. Thread.setPriority(int priority)。

第一种方法是 Android 体系中供给的 API 接口。入参 pid 便是线程 id,也能够不传,会默以为当时线程,入参 priority 能够传 -20 到 19 之间的任何一个值,但主张直接运用 Android 供给的 Priority 界说常量,这样咱们的代码具有更高的可读性,假如直接传咱们自界说的数字进去,不利于代码的了解。

体系常量 nice值 运用场景
Process.THREAD_PRIORITY_DEFAULT 0 默许优先级
Process.THREAD_PRIORITY_LOWEST 19 最低优先级
Process.THREAD_PRIORITY_BACKGROUND 10 后台线程主张优先级
Process.THREAD_PRIORITY_LESS_FAVORABLE 1 比默许略低
Process.THREAD_PRIORITY_MORE_FAVORABLE -1 比默许略高
Process.THREAD_PRIORITY_FOREGROUND -2 前台线程优先级
Process.THREAD_PRIORITY_DISPLAY -4 显现线程主张优先级
Process.THREAD_PRIORITY_URGENT_DISPLAY -8 显现线程的最高等级
Process.THREAD_PRIORITY_AUDIO -16 音频线程主张优先级
Process.THREAD_PRIORITY_URGENT_AUDIO -19 音频线程最高优先级

在不进行调整前,咱们主线程的 Nice 值默以为 0,烘托线程的默许 Nice 值为 -4。音频线程主张是最高等级优先级,由于假如音频线程优先级太低,就会呈现音频播放卡顿的情况。

第二种方法是 Java 供给的 API 接口,Java 有自己对线程优先级的界说和规矩,可是最终都会将这些规矩转换成对应的 Nice 值巨细。Java 线程供给的优先级以及转换成 Nice 值的规矩如下:

常量值 nice值 对应 Android API
Thread.MAX_PRIORITY 10 -8 THREAD_PRIORITY_URGENT_DISPLAY
Thread.MIN_PRIORITY 0 19 THREAD_PRIORITY_LOWEST
Thread.NORM_PRIORITY 5 0 THREAD_PRIORITY_DEFAULT

第二种方法能设置的优先级较少,不太灵敏,而且由于体系的一个时序问题 Bug,在设置子线程的优先级时,或许由于子线程没创立成功而设置成了主线程的,会导致优先级设置异常,所以这儿主张运用第一种方法来设置线程的优先级,避免运用第二种方法。

需求调整优先级的线程

了解了调整线程优先级的方法,咱们再看看哪些线程需求调整,首要有两类:主线程和烘托线程(RenderThread)

为什么要调整这两个线程呢?由于这两个线程对任何运用来说都非常重要。从Android5 开始,主线程只负责布局文件的 measure 和 layout 作业,烘托的作业放到了烘托ls线程,这两个线程合作作业,才让咱们运用的界面能正常显现出来。所以经过进步这两个线程的优先级,便能让这两个线程取得更多的 CPU 时间,页面显现的速度天然也就更快了

主线程的优先级好调整,咱们直接在 Application 的 attach 生命周期中,调用 Process.setThreadPriority(-19),将主线程设置为最高等级的优先级即可。可是 render 线程怎样调整呢?这时咱们需求知道 render 线程的线程 id,然后仍然调用 Process.setThreadPriority 就能够了。下面咱们就一起看一下怎么找到 render 线程的线程 pid。

运用中线程的信息记载在 /proc/pid/task 的文件中,能够看到 task 文件中记载了当时运用的一切线程。以 11548 这个进程的数据为例,数据如下:

/proc/11548/task $ ls
11548  11554  11556  11558  11560  11564  11566  12879  12883  12890  12917  14501  14617  15596  15598  15600  15602  15614
11553  11555  11557  11559  11562  11565  12878  12881  12884  12894  12920  14555  15585  15597  15599  15601  15613  15617

咱们接着检查该目录里线程的 stat 节点,就能详细检查到线程的详细信息,如 Name、pid 等等。11548 进程的主线程 id 便是 11548,它的 stat 数据如下:

blueline:/proc/11548/task $ cat 11548/stat
11548 (ndroid.settings) S 1271 1271 0 0 -1 1077952832 12835 0 1617 0 52 19 0 0 10 -10 36 0 59569858 15359959040 23690 18446744073709551615 1 1 0 0 0 0 4612 1 1073775864 0 0 0 17 4 0 0 0 0 0 0 0 0 0 0 0 0 0

在第十章中,咱们现已详细介绍了 stat 数据中每个参数的含义,假如记不清了能够看看前面的知识点。上面的数据中,第一个参数是 pid,第二个参数是 name。

所以咱们只需求遍历这个文件,查找名称为 “render” 的线程,就能找到烘托线程的 pid 了。那么下面就看一下详细的代码怎么完成吧。

public static int getRenderThreadTid() {
    File taskParent = new File("/proc/" + Process.myPid() + "/task/");
    if (taskParent.isDirectory()) {
        File[] taskFiles = taskParent.listFiles();
        if (taskFiles != null) {
            for (File taskFile : taskFiles) {
                //读线程名
                BufferedReader br = null;
                String cpuRate = "";
                try {
                    br = new BufferedReader(new FileReader(taskFile.getPath() + "/stat"), 100);
                    cpuRate = br.readLine();
                } catch (Throwable throwable) {
                    //ignore
                } finally {
                    if (br != null) {
                        br.close();
                    }
                }
                if (!cpuRate.isEmpty()) {
                    String param[] = cpuRate.split(" ");
                    if (param.length < 2) {
                        continue;
                    }
                    String threadName = param[1];
                    //找到name为RenderThread的线程,则返回第0个数据便是 tid
                    if (threadName.equals("(RenderThread)")) {
                        return Integer.parseInt(param[0]);
                    }
                }
            }
        }
    }
    return -1;
}

当咱们拿到烘托线程的 pid 后,相同调用 Process.setThreadPriority(pid,-19) 将烘托线程设置成最高优先级即可。

当然,咱们要进步的优先级线程并非只要这两个,咱们能够依据事务需求,来进步中心线程的优先级,同时降低其他非中心线程的优先级,该操作能够在线程池中经过线程工厂来统一调整。进步中心线程优先级,降低非中心线程优先级,两者合作运用,才干更高效地进步运用的速度。

中心绑定 CPU 大核

接着,咱们来看第二种优化计划:绑定 CPU 大核。这种计划尽管和操作体系的使命调度关系不大,但也归于一种进步线程优先级的计划,只不过它进步的是线程运转在功能更好的 CPU 上的优先级。

目前手机设备的 CPU 都是多核的,如下图的骁龙 888 这款 CPU 就有 8 个核,其中大核的功能是最好的,时钟周期频率为 2.84GHZ,其他的核功能都要差很多。

速度优化:任务调度优化

最差的核只要 1.8GHZ 的时钟周期频率,假如用它来履行咱们中心线程的使命,功能就会差很多。这首要体现在主线程和烘托线程上,页面的显现速度会变慢。所以,假如咱们能将中心线程绑定在大核上,那么运用的速度就会进步很多

线程绑核计划

线程绑核并不是很杂乱的事情,由于 Linux 体系有供给相应的 API 接口,体系供给的 pthread_setaffinity_np 和 sched_setaffinity 这儿两个函数,都能完成线程绑核。可是在 Android 体系中,约束了 pthread_setaffinity_np 函数的运用,所以咱们只能经过 sched_setaffinity 函数来进行绑核操作。

#include <sched.h>

int sched_setaffinity(pid_t pid , size_t cpusetsize **,cpu_set_t *** mask );

第一个入参是线程的 pid,假如pid的值为0,则表示指定的是主线程

第二个入参 cpusetsize 是 mask 所指定的数的长度

第三个入参是需求绑定的 cpu 序列的掩码

下面,咱们就一起来看一下,怎么经过这个函数完成线程绑核的操作。

void bindCore(){
    cpu_set_t mask;     //CPU核的集合     
    CPU_ZERO(&mask);     //将mask置空     
    CPU_SET(0,&mask);    //将需求绑定的cpu核设置给mask,核为序列0,1,2,3……     
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1){     //将线程绑核
         printf("bind core fail");
    }
}

咱们能够看到,将主线程绑定序列为 0 的操作只要数行代码就能完成了。完成起来很简单,只需求在 native 层指定需求绑定线程的 pid,核序列的掩码 mask,作为入参调用 sched_setaffinity 函数即可。

可咱们需求的是将主线程和烘托线程绑定大核,所以上面的代码并不完好,咱们还需求传入正确的 pid 和核序列。主线程的 pid 入参传 0,烘托线程的 pid 也知道怎么获取了,现在就差找到那个功能最高的大核了。

获取大核序列

咱们能够经过 /sys/devices/system/cpu/ 目录下的文件,检查当时设备有几个 CPU 。用来测验的是一台 Pixel3,能够看到有 8 个 CPU,也便是 8 核。

/sys/devices/system/cpu $ ls
core_ctl_isolated  cpu1  cpu3  cpu5  cpu7     cpuidle                hang_detect_gold    hotplug   kernel_max  offline  possible  present
cpu0               cpu2  cpu4  cpu6  cpufreq  gladiator_hang_detect  hang_detect_silver  isolated  modalias    online   power     uevent

然后进入到 cpuX/cpufreq 文件,检查详细序列的 CPU 详情。

/sys/devices/system/cpu/cpu0/cpufreq $ ls
affected_cpus     cpuinfo_max_freq  cpuinfo_transition_latency  scaling_available_frequencies  scaling_boost_frequencies  scaling_driver    scaling_max_freq  scaling_setspeed  stats
cpuinfo_cur_freq  cpuinfo_min_freq  related_cpus                scaling_available_governors    scaling_cur_freq           scaling_governor  scaling_min_freq  schedutil

这个文件中的 cpuinfo_max_freq 节点便是当时 CPU 的时钟周期频率。下面便是 piexl3 骁龙 845 芯片的每个核的时钟周期频率。

/sys/devices/system/cpu $ cat cpu0/cpufreq/cpuinfo_max_freq
1766400
/sys/devices/system/cpu $ cat cpu1/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu2/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu3/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
1766400
/sys/devices/system/cpu $ cat cpu4/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200
/sys/devices/system/cpu $ cat cpu5/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200
/sys/devices/system/cpu $ cat cpu6/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200
/sys/devices/system/cpu $ cat cpu7/cpufreq/cpuinfo_max_freq                                                                                                                                                                   
2803200

能够看到,4、5、6、7 序列都是大核。假如检查 845 的参数,也能够发现是符合这个特性的。

速度优化:任务调度优化

所以咱们在代码完成中,只需求遍历 /sys/devices/system/cpu/ 目录下的 cpu 节点,然后读取节点下的 cpuinfo_max_freq 就能找到大核了。下面就看一下怎么完成吧。

  1. 统计该设备 CPU 有多少个核。
public static int getNumberOfCPUCores() {
    int cores = new File("/sys/devices/system/cpu/").listFiles((file) -> {
        String path = file.getName();
        if (path.startsWith("cpu")) {
            for (int i = 3; i < path.length(); i++) {
                if (path.charAt(i) < '0' || path.charAt(i) > '9') {
                    return false;
                }
            }
            return true;
        }
        return false;
    }).length;
    return cores;
}
  1. 遍历每个核,找出时钟频率最高的那个核。
public static int getMaxFreqCPU() {
    int maxFreq = -1;
    try {
        for (int i = 0; i < getNumberOfCPUCores(); i++) {
            String filename = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq";
            File cpuInfoMaxFreqFile = new File(filename);
            if (cpuInfoMaxFreqFile.exists()) {
                byte[] buffer = new byte[128];
                FileInputStream stream = new FileInputStream(cpuInfoMaxFreqFile);
                try {
                    stream.read(buffer);
                    int endIndex = 0;
                    //Trim the first number out of the byte buffer.
                    while (buffer[endIndex] >= '0' && buffer[endIndex] <= '9'
                            && endIndex < buffer.length) endIndex++;
                    String str = new String(buffer, 0, endIndex);
                    Integer freqBound = Integer.parseInt(str);
                    if (freqBound > maxFreq) maxFreq = freqBound;
                } catch (NumberFormatException e) {
                } finally {
                    stream.close();
                }
            }
        }
    } catch (IOException e) {
    }
    return maxFreq;
}

至此,咱们便找出了大核的序列,然后将大核序列以及线程 pid 传入 Native 层,调用 sched_setaffinity 进行绑核即可。当然,咱们也能够直接在 native 经过 C++ 代码来解析文件获取烘托线程和大核,这样效率会更好一些,详细代码就不在这儿完成了。除了主线程和烘托线程,咱们也能够依据事务需求,将其他中心线程绑定大核,比方上面说到的骁龙 845 有四个大核,咱们就能够每个大核都绑定一个中心线程。

当咱们经过上面的逻辑将主线程和烘托线程绑定大核后,能够经过 sched_getaffinity 函数或许经过 ps -p -t 等指令检查线程运转在在哪个核上,以此来确认是否绑定成功。到这儿,你就学会了怎么将线程绑定大核这一优化计划了,能够在课后试一试,看看绑定大核后,运用的发动速度和页面的打开速度进步了多少。

小结

这一章介绍的两种计划的代码完成都不难,很简单落地。可是咱们真实需求把握的不仅仅是这两种计划的完成,还有能诞生出这两种优化计划的方法论,即进步使命调度优先级的方法论。

那除了能想到本章中说到的两种计划,咱们能够想想还有哪些计划能进步优先级。比方:替换或许优化调度算法,将一般进程变成实时进程,等等。只要咱们能从原理出发,自下而上地考虑,就一定能发生连绵不断的创意和思路!