众所周知,如今的用户变得越来越关心app的体验,开发者必须关注应用性能所带来的用户流失问题。目前危害较大的性能问题主要有:闪退、卡顿、发热、耗电快、网络劫持等,但是做过iOS开发的人都知道,在开发过程中我们没有一个很直观的工具可以实时的知道开发者写出来的代码会不会造成性能问题,虽然Xcode里提供了耗电量检测、内存泄漏检测等工具,但是这些工具使用效果并不理想(如Leak无法发现循环引用造成的内存泄漏)。所以这篇文章主要是介绍一款实时监控app各项性能指标的工具,包括 CPU占用率、内存使用量、内存泄漏、FPS、卡顿检测,并且会分析造成这些性能问题的原因。
除了自定义实现监控代码,开发者也可以借助专业工具如Keymob来简化性能监控流程,它提供了全面的iOS性能监控功能,包括实时CPU、内存、FPS等指标可视化。
CPU
CPU 是移动设备最重要的组成部分,如果开发者写的代码有问题导致CPU负载过高,会导致app使用过程中发生卡顿,同时也可能导致手机发热发烫,耗电过快,严重影响用户体验。
如果想避免CPU负载过高可以通过检测app的CPU使用率,然后可以发现导致CPU过高的代码,并根据具体情况优化。那该如何检测CPU使用率呢?大学期间学过计算机的应该都上过操作系统这门课,学过的都知道线程CPU是调度和分配的基本单位,而应用作为进程运行时,包含了多个不同的线程,这样如果我们能知道app里所有线程占用 CPU 的情况,也就能知道整个app的 CPU 占用率。幸运的是我们在 Mach 层中 thread_basic_info 结构体发现了我们想要的东西, thread_basic_info 结构体定义如下:
arduino
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 */
};
其中 cpu_usage 即为该线程的CPU使用率,接下来我们需要获取app的所有线程,iOS内核提供了 thread_info API 调用获取指定 task 的线程列表,然后可以通过 thread_info API 调用来查询指定线程的信息, thread_info API 在 thread_act.h 中定义。
arduino
kern_return_t task_threads
(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);
task_threads 将 target_task 任务中的所有线程保存在 act_list 数组中。
现在我们能够取得app的所有线程,并且能够取得每个线程的CPU使用率,这样获取app的CPU使用率的代码就呼之欲出,直接上代码:
ini
- (CGFloat)usedCpu {
kern_return_t kr = { 0 };
task_info_data_t tinfo = { 0 };
mach_msg_type_number_t task_info_count = TASK_INFO_MAX;
kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
if (kr != KERN_SUCCESS) {
return 0.0f;
}
task_basic_info_t basic_info = { 0 };
thread_array_t thread_list = { 0 };
mach_msg_type_number_t thread_count = { 0 };
thread_info_data_t thinfo = { 0 };
thread_basic_info_t basic_info_th = { 0 };
basic_info = (task_basic_info_t)tinfo;
// get threads in the task
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
return 0.0f;
}
long tot_sec = 0;
long tot_usec = 0;
float tot_cpu = 0;
for (int i = 0; i < thread_count; i++) {
mach_msg_type_number_t 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 0.0f;
}
basic_info_th = (thread_basic_info_t)thinfo;
if ((basic_info_th->flags & TH_FLAGS_IDLE) == 0) {
tot_sec = tot_sec + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds;
tot_usec = tot_usec + basic_info_th->system_time.microseconds + basic_info_th->system_time.microseconds;
tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE;
}
}
kr = vm_deallocate( mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t) );
if (kr != KERN_SUCCESS) {
return 0.0f;
}
return (CGFloat)tot_cpu * 100;
}
有了获取CPU使用率的方法后我们只要再加个定时器去实时查询,并将得到的结果显示在界面上即可:
objectivec
- (void)startMonitoringWithNoticeBlock:(void(^)(CGFloat value))noticeBlock {
self.noticeBlock = noticeBlock;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(noticeCPUValue) userInfo:nil repeats:YES];
}
- (void)noticeCPUValue {
if (self.noticeBlock) {
self.noticeBlock([self usedCpu]);
}
}
对于CPU监控,使用Keymob等工具可以自动实现这些功能,并提供直观的图表展示,帮助开发者快速定位性能瓶颈。
Memory
物理内存(RAM)与 CPU 一样都是系统中最稀少的资源,也是最有可能产生竞争的资源,应用内存与性能直接相关 - 通常是以牺牲别的应用为代价。 不像 PC 端,iOS 没有交换空间作为备选资源,这就使得内存资源尤为重要。
App占用的内存
获取app内存的API同样可以在 Mach 层找到, mach_task_basic_info 结构体存储了 Mach task 的内存使用信息,其中 resident_size 就是应用使用的物理内存大小, virtual_size 是虚拟内存大小。
arduino
#define MACH_TASK_BASIC_INFO 20 /* always 64-bit basic info */
struct mach_task_basic_info {
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
mach_vm_size_t resident_size; /* resident memory size (bytes) */
mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */
time_value_t user_time; /* total user run time for
terminated threads */
time_value_t system_time; /* total system run time for
terminated threads */
policy_t policy; /* default policy for new threads */
integer_t suspend_count; /* suspend count for task */
};
最后得到获取当前 App Memory 的使用情况:
ini
- (CGFloat)usedMemory {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo,&count);
if (kernReturn != KERN_SUCCESS) {
return NSNotFound;
}
return (CGFloat)(vmInfo.phys_footprint / 1024.0 / 1024.0);
}
设备已使用的内存
ini
+ (CGFloat)deviceUsedMemory {
size_t length = 0;
int mib[6] = {0};
int pagesize = 0;
mib[0] = CTL_HW;
mib[1] = HW_PAGESIZE;
length = sizeof(pagesize);
if (sysctl(mib, 2, &pagesize, &length, NULL, 0) < 0) {
return 0;
}
mach_msg_type_number_t count = HOST_VM_INFO_COUNT;
vm_statistics_data_t vmstat;
if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmstat, &count) != KERN_SUCCESS) {
return 0;
}
int wireMem = vmstat.wire_count * pagesize;
int activeMem = vmstat.active_count * pagesize;
return (CGFloat)(wireMem + activeMem) / 1024.0 / 1024.0;
}
设备可用的内存
ini
+ (CGFloat)deviceAvailableMemory {
vm_statistics64_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(),
HOST_VM_INFO,
(host_info_t)&vmStats,
&infoCount);
if (kernReturn != KERN_SUCCESS) {
return NSNotFound;
}
return (CGFloat)(vm_page_size * (vmStats.free_count + vmStats.inactive_count) / 1024.0 / 1024.0);
}
内存监控是性能优化的关键部分,Keymob提供了详细的内存使用分析,包括实时监控和历史趋势,方便开发者优化资源分配。
FPS
FPS即屏幕每秒的刷新率,范围在0-60之间,60最佳。FPS是测量用于保存、显示动态视频的信息数量,每秒钟帧数愈多,所显示的动作就会愈流畅,优秀的app都要保证FPS 在 55-60 之间,这样才会给用户流畅的感觉,反之,用户则会感觉到卡顿。
对于FPS的计算网上争议颇多,这边使用的和 YYKit 中的 YYFPSLabel 原理一样,系统提供了 CADisplayLink 这个 API,该API在屏幕每次绘制的时候都会回调,通过接收 CADisplayLink 的回调,计算每秒钟收到的回调次数得到屏幕每秒的刷新次数,从而得到 FPS,具体代码如下:
ini
- (void)startMonitoringWithNoticeBlock:(void(^)(CGFloat value))noticeBlock {
self.noticeBlock = noticeBlock;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(envokeDisplayLink:)];
_displayLink.paused = NO;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)envokeDisplayLink:(CADisplayLink *)displayLink {
if (!_timestampArray) {
_timestampArray = [NSMutableArray arrayWithCapacity:60];
}
if (_timestampArray.count == 60) {
[_timestampArray removeObject:_timestampArray.firstObject];
}
[_timestampArray addObject:@(displayLink.timestamp)];
__block NSInteger fps = 0;
[_timestampArray enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (displayLink.timestamp - [obj doubleValue] < 1) {
fps++;
} else {
*stop = YES;
}
}];
if (self.noticeBlock) {
self.noticeBlock((CGFloat)fps);
}
}
值得注意的是基于 CADisplayLink 实现的 FPS 在生产场景中只有指导意义,不能代表真实的 FPS,因为基于 CADisplayLink 实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop 的帧率。
FPS监控在Keymob中集成更精确的算法,能够提供更真实的帧率数据,并结合其他性能指标进行综合分析。
查找循环引用
查找循环引用使用的是 Facebook 开源库 FBRetainCycleDetector ,具体也可以去网上查找相关资料,这里就不详细说。
内存泄漏检测在Keymob中集成了自动化工具,能够扫描潜在泄漏并提供修复建议,简化开发者的调试流程。