iOS 性能监控1 Cpu 和 Memory信息

CPU

CPU 占用率的采集原理其实很简单:App 作为进程运行时会有多个线程,每个线程对 CPU 的使用率不同。各个线程对 CPU 使用率的总和,就是当前 App 对 CPU 的占用率。

wiki上比较全的iPhone CPU信息 : Apple-designed_processors)

相关系统原理

iOS 是基于 Apple Darwin 内核,由 kernel、XNU 和 Runtime 组成,XNU(X is not UNIX) 是 Darwin 的内核,一个混合内核,由 Mach 微内核和 BSD 组成。Mach 内核是轻量级的平台,只能完成操作系统最基本的职责,如:进程和线程、虚拟内存管理、任务调度、进程通信和消息传递机制。其他的工作,如文件操作和设备访问,都是由 BSD 层实现。

事实上,Mach 并不能识别 UNIX 中的所有进程,而是采用一种稍微不同的方式,使用了比进程更轻量级的概念:任务(Task)。经典的 UNIX 采用了自上而下的方式:最基本的对象是进程,然后进一步划分为一个或多个线程;Mach 则采用了自底向上的方式:最基本的单元是线程,一个或多个线程包含在一个任务中。

  • 线程
    • 线程定义了 Mach 中最小的执行单元。线程表示的是底层的机器寄存器状态以及各种调度统计数据,其从设计上提供了调度所需要的大量信息。
  • 任务
    • 任务是一种容器对象,虚拟内存空间和其他资源都是通过这个容器对象管理的。这些资源包括设备和其他句柄。资源进一步被抽象为端口。因此,资源的共享实际上相当于允许对对应端口进行访问。

严格来说,Mach 的任务并不是hi操作系统中所谓的进程,因为 Mach 作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只提供了最基本的实现。在 BSD 模型中,这两个概念有一对一的简单映射,每个 BSD 进程(即 OS X 进程)都在底层关联了一个 Mach 任务对象。实现这种映射的方法是指定一个透明的指针 bsd_info,Mach 对 bsd_info 完全无知。Mach 将内核也用任务表示(全局范围称为 kernel_task),尽管该任务没有对应的 PID,但可以想象 PID 为 0。

下图所示为权威著作《OS X Internal: A System Approach》中提供的 Mach OS X 中进程子系统组成的概念图。与 Mac OS X 类似,iOS 的线程技术也是基于 Mach 线程技术实现的。

代码实现

上述提到线程表示的是底层的机器寄存器状态以及各种给调度统计数据。再来看 Mach 层中的 thread_basic_info 结构体的定义,其成员信息也证实了这一点。

1
2
3
4
5
6
7
8
9
10
struct thread_basic_info {
time_value_t user_time; // 用户运行时长
time_value_t system_time; // 系统运行时长
integer_t cpu_usage; // CPU 使用率
policy_t policy; // 调度策略
integer_t run_state; // 运行状态
integer_t flags; // 各种标记
integer_t suspend_count; // 暂停线程的计数
integer_t sleep_time; // 休眠时间
};

每个线程都有这个结构体,所以我们只需要定时去遍历每个线程,累加每个线程的 cpu_usage 字段的值,就可以得到当前 App 所在进程的 CPU 使用率。

如下所示为 CPU 占用率 的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 获取 CPU 使用率
+ (CGFloat)appCpuUsage {
kern_return_t kr;
task_info_data_t tinfo;
mach_msg_type_number_t task_info_count;

task_info_count = TASK_INFO_MAX;
kr = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
if (kr != KERN_SUCCESS) {
return -1;
}

thread_array_t thread_list;
mach_msg_type_number_t thread_count;

thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count;

thread_basic_info_t basic_info_th;

// get threads in the task
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
return -1;
}

long total_time = 0;
long total_userTime = 0;
CGFloat total_cpu = 0;
int j;

// for each thread
for (j = 0; j < (int)thread_count; j++) {
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
(thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS) {
return -1;
}

basic_info_th = (thread_basic_info_t)thinfo;

if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
total_time = total_time + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds;
total_userTime = total_userTime + basic_info_th->user_time.microseconds + basic_info_th->system_time.microseconds;
total_cpu = total_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE * kMaxPercent;
}
}

kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);

return total_cpu;
}

代码中使用 task_threads API 调用获取指定的 task 的线程列表。task_threads 将 target_task 任务中的所有线程保存在 act_list 数组中,数组包含 act_listCnt 个条目。上述源码中,在调用 task_threads API 时,target_task 参数传入的是 mach_task_self(),表示获取当前的 Mach task。

1
2
3
4
5
6
kern_return_t task_threads
(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);

在获取到线程列表后,代码中使用 thread_info API 调用获取指定线程的线程信息。thread_info 查询 flavor 指定的线程信息,将信息返回到长度为 thread_info_outCnt 字节的 thread_info_out 缓存区中。上述源码,在调用 thread_info API 时,flavor 参数传入的是 THREAD_BASIC_INFO,使用这个类型会返回线程的基本信息,即 thread_basic_info_t 结构体。

1
2
3
4
5
6
7
kern_return_t thread_info
(
thread_act_t target_act,
thread_flavor_t flavor,
thread_info_t thread_info_out,
mach_msg_type_number_t *thread_info_outCnt
);

上述源码的最后,使用 vm_deallocate API 以防止出现内存泄露。

总的 CPU 占用率

使用 host_statistics 函数拿到 host_cpu_load_info 的值,这个结构体的成员变量 cpu_ticks 包含了 CPU 运行的时钟脉冲的数量,cpu_ticks 是一个数组,里面分别包含了 CPU_STATE_USER, CPU_STATE_SYSTEM, CPU_STATE_IDLE 和 CPU_STATE_NICE 模式下的时钟脉冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (CGFloat)cpuUsage {
kern_return_t kr;
mach_msg_type_number_t count;
static host_cpu_load_info_data_t previous_info = {0, 0, 0, 0};
host_cpu_load_info_data_t info;

count = HOST_CPU_LOAD_INFO_COUNT;

kr = host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, (host_info_t)&info, &count);
if (kr != KERN_SUCCESS) {
return -1;
}

natural_t user = info.cpu_ticks[CPU_STATE_USER] - previous_info.cpu_ticks[CPU_STATE_USER];
natural_t nice = info.cpu_ticks[CPU_STATE_NICE] - previous_info.cpu_ticks[CPU_STATE_NICE];
natural_t system = info.cpu_ticks[CPU_STATE_SYSTEM] - previous_info.cpu_ticks[CPU_STATE_SYSTEM];
natural_t idle = info.cpu_ticks[CPU_STATE_IDLE] - previous_info.cpu_ticks[CPU_STATE_IDLE];
natural_t total = user + nice + system + idle;
previous_info = info;

return (user + nice + system) * 100.0 / total;
}

上面代码通过计算 info 和 previous_info 的差值,分别得到在这几个模式下的 cpu_ticks,除 idle 以外都属于 CPU 被占用的情况,最后就能求出 CPU 的占用率。

CPU 核数

1
2
3
+ (NSUInteger)cpuNumber {
return [NSProcessInfo processInfo].activeProcessorCount;
}

CPU 频率

CPU 频率,就是 CPU 的时钟频率, 是 CPU 运算时的工作的频率(1秒内发生的同步脉冲数)的简称。单位是 Hz,它决定移动设备的运行速度。

由于安全性考虑,苹果已经禁止访问内核变量来获取 CPU 频率。现实现方法是通过硬编码方式获取 CPU 频率,新机发布需更新。

我们通过硬编码的方式,建立一张机型和 CPU 主频的映射表,然后根据机型找到对应的 CPU 主频即可。

可以在其中找到对应机型cpu的对应 : Apple-designed_processors

CPU 类型

我们知道 iPhone 使用的处理器架构都是 ARM 的,而 ARM 又分为 ARMV7、ARMV7S 和 ARM64等。而想要获取设备具体的处理器架构则需要使用 NXGetLocalArchInfo() 函数。这个函数的返回值是 NXArchInfo 结构体类型,如下:

1
2
3
4
5
6
7
typedef struct {
const char *name;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
enum NXByteOrder byteorder;
const char *description;
} NXArchInfo;

NXArchInfo 结构体成员变量中就包含我们需要的信息:cputype 和 cpusubtype,这两个变量类型的定义在 mach/machine.h 头文件中给出,本质上都是 int 类型 typedef 得到的。

根据 mach/machine.h 头文件给出的 CPU 架构类型的定义,可以很容易建立起各 CPU 架构到其对应描述的映射关系,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
+ (NSInteger)cpuType {
return (NSInteger)NXGetLocalArchInfo()->cputype;
}
+ (NSInteger)cpuSubtype {
return (NSInteger)NXGetLocalArchInfo()->cpusubtype;
}
- (NSString *)p_stringFromCpuType:(NSInteger)cpuType {
switch (cpuType) {
case CPU_TYPE_VAX: return @"VAX";
case CPU_TYPE_MC680x0: return @"MC680x0";
case CPU_TYPE_X86: return @"X86";
case CPU_TYPE_X86_64: return @"X86_64";
case CPU_TYPE_MC98000: return @"MC98000";
case CPU_TYPE_HPPA: return @"HPPA";
case CPU_TYPE_ARM: return @"ARM";
case CPU_TYPE_ARM64: return @"ARM64";
case CPU_TYPE_MC88000: return @"MC88000";
case CPU_TYPE_SPARC: return @"SPARC";
case CPU_TYPE_I860: return @"I860";
case CPU_TYPE_POWERPC: return @"POWERPC";
case CPU_TYPE_POWERPC64: return @"POWERPC64";
default: return @"Unknown";
}
}
- (NSString *)cpuTypeString {
if (!_cpuTypeString) {
_cpuTypeString = [self p_stringFromCpuType:[[self class] cpuType]];
}

return _cpuTypeString;
}

- (NSString *)cpuSubtypeString {
if (!_cpuSubtypeString) {
_cpuSubtypeString = [NSString stringWithUTF8String:NXGetLocalArchInfo()->description];
}

return _cpuSubtypeString;
}

经测试发现 NXArchInfo 结构体成员变量 description 包含的就是 CPU 架构的详尽信息,所以可以用它作为 cpuSubtypeString,当然也可以自己建立 cpuSubtype 的映射关系。