在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称:aozhimin/iOS-Monitor-Platform开源软件地址:https://github.com/aozhimin/iOS-Monitor-Platform开源编程语言:开源软件介绍:为了让这篇文章能够在公众号发表,所以将文章拆解成上下两篇:基础性能篇和网络篇 目录为什么写这篇文章?随着移动互联网向纵深发展,用户变得越来越关心应用的体验,开发者必须关注应用性能所带来的用户流失问题。据统计,有十种应用性能问题危害最大,分别为:连接超时、闪退、卡顿、崩溃、黑白屏、网络劫持、交互性能差、CPU 使用率问题、内存泄露、不良接口。开发者难以兼顾所有的性能问题,而在传统的开发流程中,我们解决性能问题的方式通常是在得到线上用户的反馈后,再由开发人员去分析引发问题的根源;显然,凭借用户的反馈来得知应用的性能问题这种方式很原始,也很不高效,它使得开发团队在应对应用性能问题上很被动;所以寻找一种更专业和高效的手段来保障应用的性能就变得势在必行。性能监控 SDK 的定位就是帮助开发团队快速精确地定位性能问题,进而推动应用的性能和用户体验的提升。 这篇文章是我在开发 iOS 性能监控平台 SDK 过程前期的调研和沉淀。主要会探讨在 iOS 平台下如何采集性能指标,如 CPU 占用率、内存使用情况、FPS、冷启动、热启动时间,网络,耗电量等,剖析每一项性能指标的具体实现方式,SDK 的实现会有一定的技术难度,这也是我为什么写这篇文章的原因,我希望能够将开发过程中的一些心得和体会记录下来,同时后续我会将实现 SDK 的详细细节开源出来,希望能对读者有所帮助。 项目名称的来源我们团队将这个项目命名为 Wedjat(华狄特),取自古埃及神话中鹰头神荷鲁斯的眼睛,荷鲁斯是古埃及神话中法老的守护神,他通常被描绘成“隼头人身”的形象,最常见的代表符号是一只眼睛,该眼也被称之为“荷鲁斯之眼”,象征着“正义之眼”,严厉、公正、铁面无私,一切公开或私人的行为,都逃不过他的法眼。他不但是光明和天堂的象征,最早还是一位生育万物的大神,每天在尼罗河上巡视他的子民。Wedjat 的寓意恰好与我们性能监控 SDK 的愿景相契合。
CPU
CPU 是移动设备最重要的计算资源,设计糟糕的应用可能会造成 CPU 持续以高负载运行,一方面会导致用户使用过程遭遇卡顿;另一方面也会导致手机发热发烫,电量被快速消耗完,严重影响用户体验。 APP 的 CPU 占用率如果想避免出现上述情况,可以通过监控应用的 CPU 占用率,那么在 iOS 中如何实现 CPU 占用率的监控呢?事实上,学习过操作系统课程的读者都了解线程是调度和分配的基本单位,而应用作为进程运行时,包含了多个不同的线程,显然如果我们能获取应用的所有线程占用 CPU 的情况,也就能知道应用的 CPU 占用率。
上图是权威著作《OS X Internal: A System Approach》给出的 Mac OS X 中进程子系统组成的概念图,与 Mac OS X 类似,iOS 的线程技术也是基于 Mach 线程技术实现的,在 Mach 层中 struct thread_basic_info {
time_value_t user_time; /* user run time */
time_value_t system_time; /* system run time */
integer_t cpu_usage; /* scaled cpu usage percentage */
policy_t policy; /* scheduling policy in effect */
integer_t run_state; /* run state (see below) */
integer_t flags; /* various flags (see below) */
integer_t suspend_count; /* suspend count for thread */
integer_t sleep_time; /* number of seconds that thread
has been sleeping */
};
上面引用的是《OS X and iOS Kernel Programming》对 Mach task 的描述,Mach task 可以看作一个机器无关的 thread 执行环境的抽象
一个 task 包含它的线程列表。内核提供了 kern_return_t task_threads
(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);
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
);
有了上面的铺垫后,得到获取当前应用的 CPU 占用率的实现如下: #import <mach/mach.h>
#import <assert.h>
+ (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;
} 在调用 注意方法最后要调用
下面是 GT 中获得 App 的 CPU 占用率的方法 - (float)getCpuUsage
{
kern_return_t kr;
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;
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
return -1;
}
cpu_usage = 0;
for (int i = 0; i < thread_count; i++)
{
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[i], 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))
{
cpu_usage += basic_info_th->cpu_usage;
}
}
cpu_usage = cpu_usage / (float)TH_USAGE_SCALE * 100.0;
vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
return cpu_usage;
} 总的 CPU 占用率而获取整个设备的 CPU 占用率如下: static NSUInteger const kMaxPercent = 100;
+ (CGFloat)cpuUsage {
CGFloat cpuUsage = 0;
processor_info_array_t _cpuInfo, _prevCPUInfo = nil;
mach_msg_type_number_t _numCPUInfo, _numPrevCPUInfo = 0;
unsigned _numCPUs;
NSLock *_cpuUsageLock;
int _mib[2U] = {CTL_HW, HW_NCPU};
size_t _sizeOfNumCPUs = sizeof(_numCPUs);
int _status = sysctl(_mib, 2U, &_numCPUs, &_sizeOfNumCPUs, NULL, 0U);
if (_status)
_numCPUs = 1;
_cpuUsageLock = [[NSLock alloc] init];
natural_t _numCPUsU = 0U;
kern_return_t err = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &_numCPUsU, &_cpuInfo, &_numCPUInfo);
if (err == KERN_SUCCESS) {
[_cpuUsageLock lock];
for (unsigned i = 0U; i < _numCPUs; ++i) {
CGFloat _inUse, _total = 0;
if (_prevCPUInfo) {
_inUse = (
(_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER])
+ (_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM])
+ (_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE])
);
_total = _inUse + (_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE]);
} else {
_inUse = _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER] + _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM] + _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE];
_total = _inUse + _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE];
}
if (_total != 0) {
cpuUsage += _inUse / _total;
}
}
[_cpuUsageLock unlock];
if (_prevCPUInfo) {
size_t prevCpuInfoSize = sizeof(integer_t) * _numPrevCPUInfo;
vm_deallocate(mach_task_self(), (vm_address_t)_prevCPUInfo, prevCpuInfoSize);
}
return cpuUsage * kMaxPercent ;
} else {
return -1;
}
}
网上有很多文章都是通过上述方式去获取设备的 CPU 占用率,包括 YYCategories 中 @implementation WDTDevice {
processor_info_array_t _cpuInfo, _prevCPUInfo;
mach_msg_type_number_t _numCPUInfo, _numPrevCPUInfo;
NSLock *_cpuUsageLock;
}
- (CGFloat)cpuUsage {
CGFloat cpuUsage = 0;
unsigned _numCPUs;
int _mib[2U] = {CTL_HW, HW_NCPU};
size_t _sizeOfNumCPUs = sizeof(_numCPUs);
int _status = sysctl(_mib, 2U, &_numCPUs, &_sizeOfNumCPUs, NULL, 0U);
if (_status)
_numCPUs = 1;
_cpuUsageLock = [[NSLock alloc] init];
natural_t _numCPUsU = 0U;
kern_return_t err = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &_numCPUsU, &_cpuInfo, &_numCPUInfo);
if (err == KERN_SUCCESS) {
[_cpuUsageLock lock];
for (unsigned i = 0U; i < _numCPUs; ++i) {
CGFloat _inUse, _total = 0;
if (_prevCPUInfo) {
_inUse = (
(_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER])
+ (_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM])
+ (_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE])
);
_total = _inUse + (_cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE] - _prevCPUInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE]);
} else {
_inUse = _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER] + _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM] + _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE];
_total = _inUse + _cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE];
}
if (_total != 0) {
cpuUsage += _inUse / _total;
}
}
[_cpuUsageLock unlock];
if (_prevCPUInfo) {
size_t prevCpuInfoSize = sizeof(integer_t) * _numPrevCPUInfo;
vm_deallocate(mach_task_self(), (vm_address_t)_prevCPUInfo, prevCpuInfoSize);
}
_prevCPUInfo = _cpuInfo;
_numPrevCPUInfo = _numCPUInfo;
_cpuInfo = NULL;
_numCPUInfo = 0U;
return cpuUsage * kMaxPercent ;
} else {
return -1;
}
}
改为这种写法之后发现结果几乎都在 100% 以上,所以这种写法依然存在问题。 于是寻找到另外一种 + (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;
} 上面代码通过计算 经测试发现这种计算总的 CPU 占用率的方式与 iOS 系统的 top 命令的值吻合(测试环境:iPhone 5s 的越狱机器),并且 App Store 中的几个性能工具的应用都是采用这种方式去计算设备的 CPU 占用率的,比如简易系统状态和 Battery Memory System Status Monitor 这两款应用。 CPU 核数+ (NSUInteger)cpuNumber {
return [NSProcessInfo processInfo].activeProcessorCount;
} CPU 频率CPU 频率,就是 CPU 的时钟频率, 是 CPU 运算时的工作的频率(1秒内发生的同步脉冲数)的简称。单位是 Hz,它决定移动设备的运行速度。 在 iOS 中与 CPU 频率相关的性能指标有三个:CPU 频率,CPU 最大频率 和 CPU 最小频率。 下面代码给出了获取 CPU 频率的实现,笔者通过反编译发现手淘,腾讯视频等应用也是通过这种方式获取 CPU 频率,反编译的截图如下。 上面反编译代码的实现效果和下面这段代码基本一致。 + (NSUInteger)getSysInfo:(uint)typeSpecifier {
size_t size = sizeof(int);
int results;
int mib[2] = {CTL_HW, typeSpecifier};
sysctl(mib, 2, &results, &size, NULL, 0);
return (NSUInteger)results;
}
+ (NSUInteger)getCpuFrequency {
return [self getSysInfo:HW_CPU_FREQ];
}
但是在真机测试会发现上述方式并不能正确获取到设备的 CPU 频率,如果你在网上搜索会发现有很多代码都是使用这种方式,猜测应该是早期版本还是能够获取到的,只不过出于安全性的考虑,主频这个内核变量也被禁止访问了。手淘等应用中代码估计应该是遗留代码。 既然上述方式已经被 Apple 堵死了,我们还有其他的方法可以获取到 CPU 主频吗?当然,其实我们还是可以通过一些变通的方式获取到的,主要有以下两种方式。 第一种方式是比较容易实现,我们通过硬编码的方式,建立一张机型和 CPU 主频的映射表,然后根据机型找到对应的 CPU 主频即可。
第二种方式实现起来较上一种方式更为复杂,可以通过计算来得出 CPU 频率,具体的代码如下
出于效率的考虑,代码中
当然这个文件的汇编指令只支持 armv7 和 armv7s ,也就是 32 位 Arch,64 位汇编指令有机会再补上,如果你使用的是 64 位机器调试,记得将 build archive architecture only 设置为 NO 如下图,否则会编译不通过, 我用一台 iPhone 6 测试了这种方法获得 CPU 频率,结果为 1391614727.725209 HZ,大约也就是 1400 MHZ,和上面那张表中主频一致。
要获取 CPU 最大频率 和 CPU 最小频率这两个性能指标也需要用到 static inline Boolean WDTCanGetSysCtlBySpecifier(char* specifier, size_t *size) {
if (!specifier || strlen(specifier) == 0 ||
sysctlbyname(specifier, NULL, size, NULL, 0) == -1 || size == -1) {
return false;
}
return
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论