FSAPMSDK
iOS APM (性能监控) - 数据采集实现调研。APM 监控,包含 系统信息、磁盘、CPU、内存、启动时间、FPS、卡顿、Crash、网络监控 等
Install / Use
/learn @Ericfengshi/FSAPMSDKREADME
一 概述
APM 的全称是 Application performance management,即应用性能管理,通过对应用的可靠性、稳定性等方面的监控,进而达到快速修复问题、提高用户体验的目的。将在以下几个方面对数据进行监控,包括 CPU 占有率,内存使用情况,磁盘使用情况,FPS,冷启动时间,卡顿,闪退,闪退防护,HTTPDNS,网络流量等。
此仓库为 APM 数据采集调研产物,主要是对性能监控实现进行了统一封装。其中 SDK 为实现代码,Demo 为调用详情。
二 系统
2.1 设备信息
实现功能:
- 设备型号
- 设备系统名称
- 设备系统版本
- 设备启动时间
- 设备信息
2.2 设备硬编码
通过设备机器名称(如“iPhone1,1”)硬编码方式输出相关数据。数据来自维基百科以及theiphonewiki。
实现功能:
- 设备名称
- CPU 名称
- CPU 频率
- 协处理器名称
- 电池容量
- 电池电压
- 屏幕尺寸
- 屏幕 PPI
三 磁盘
实现功能:
- 磁盘总空间
- 磁盘使用空间
- 磁盘可用空间
- 文件大小
- 目录大小
四 CPU
任务(task)是一种容器(container)对象,虚拟内存空间和其他资源都是通过这个容器对象管理的,这些资源包括设备和其他句柄。严格地说,Mach 的任务并不是其他操作系统中所谓的进程,因为 Mach 作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只是提供了最基本的实现。不过在 BSD 的模型中,这两个概念有1:1的简单映射,每一个 BSD 进程(也就是 OS X 进程)都在底层关联了一个 Mach 任务对象。
上面引用的是《OS X and iOS Kernel Programming》对 Mach task 的描述,Mach task 可以看作一个机器无关的 thread 执行环境的抽象一个 task 包含它的线程列表。内核提供了 task_threads API 调用获取指定 task 的线程列表,然后可以通过 thread_info API 调用来查询指定线程的信息。
task_threads 将 target_task 任务中的所有线程保存在 act_list 数组中,数组中包含 act_listCnt 个条目。
thread_info 查询 flavor 指定的 thread 信息,将信息返回到长度为 thread_info_outCnt 字节的 thread_info_out 缓存区中,
代码实现:
#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;
}
实现功能:
- CPU架构
- CPU核数
- 系统CPU使用率
- 当前应用CPU使用率
注意:由于安全性考虑,苹果已经禁止访问内核变量来获取 CPU 频率。现实现方法是通过硬编码方式获取 CPU 频率,新机发布需更新。
五 内存
mach_task_basic_info 结构体存储了 Mach task 的内存使用信息,其中 resident_size 就是应用使用的物理内存大小,virtual_size 是虚拟内存大小。
与获取 CPU 占用率类似,在调用 task_info API 时,target_task 参数传入的是 mach_task_self(),表示获取当前的 Mach task,另外 flavor 参数传的是 MACH_TASK_BASIC_INFO,使用这个类型会返回 mach_task_basic_info 结构体,表示返回 target_task 的基本信息,比如 task 的挂起次数和驻留页面数量。
代码实现:
+ (unsigned long long)getAppRAMUsage {
struct mach_task_basic_info info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
kern_return_t kr = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count);
if (kr != KERN_SUCCESS) {
return 0;
}
return info.resident_size;
}
+ (fs_system_ram_usage)getSystemRamUsageStruct {
vm_statistics64_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kr = host_statistics(mach_host_self(),
HOST_VM_INFO,
(host_info_t)&vmStats,
&infoCount);
fs_system_ram_usage system_memory_usage = {0, 0, 0};
if (kr != KERN_SUCCESS) {
return system_memory_usage;
}
system_memory_usage.used_size = (vmStats.active_count + vmStats.wire_count + vmStats.inactive_count) * vm_kernel_page_size;
system_memory_usage.available_size = (vmStats.free_count) * vm_kernel_page_size;
system_memory_usage.total_size = [NSProcessInfo processInfo].physicalMemory;
return system_memory_usage;
}
实现功能:
- 系统总内存
- 系统使用内存
- 系统可用内存
- 当前应用内存使用量
六 启动时间
6.1 冷启动
t(App总启动时间) = t1(main()之前的加载时间)+ t2(main()之后的加载时间)
6.1.1 main() 之前过程
t1 = 系统先读取App的可执行文件(Mach-O 文件),从里面获得 dyld 的路径,加载 dyld,dyld 去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步, runtime 被初始化。当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时 runtime 会对项目中所有类进行类结构初始化,然后调用所有的 load 方法。最后 dyld 返回 main 函数地址, main 函数被调用,我们便来到了熟悉的程序入口。可归纳以下几点:
-
系统先读取 App 的可执行文件(
Mach-O文件,自身 App 的所有.o文件的集合) -
加载dyld(动态链接编辑器)
-
Load dylibs: 加载动态库(包括所依赖的所有动态库)
-
Rebase && Bind
-
Objc SetUp: 初始化 Objective C Runtime
-
Initializers

6.1.2 main() 之后过程
t2 = main方法执行之后到
AppDelegate 类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。
因为类的
+ load方法在main函数执行之前调用,所以我们可以在+ load方法记录开始时间,同时监听UIApplicationDidFinishLaunchingNotification通知,收到通知时将时间相减作为应用启动时间,这样做有一个好处,不需要侵入到业务方的main函数去记录开始时间点。
代码实现:
static uint64_t loadTime;
static uint64_t applicationRespondedTime = -1;
static mach_timebase_info_data_t timebaseInfo;
static inline NSTimeInterval MachTimeToSeconds(uint64_t machTime) {
return ((machTime / 1e9) * timebaseInfo.numer) / timebaseInfo.denom;
}
@implementation XXStartupMeasurer
+ (void)load {
loadTime = mach_absolute_time();
mach_timebase_info(&timebaseInfo);
@autoreleasepool {
__block id<NSObject> obs;
obs = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
object:nil queue:nil
usingBlock:^(NSNotification *note) {
dispatch_async(dispatch_get_main_queue(), ^{
applicationRespondedTime = mach_absolute_time();
NSLog(@"StartupMeasurer: it took %f seconds until the app could respond to user interaction.", MachTimeToSeconds(applicationRespondedTime - loadTime));
});
[[NSNotificationCenter defaultCenter] removeObserver:obs];
}];
}
}
注意:由于 load 方法执行时机问题,通过以上方式计算出的结果并不十分准确
七 FPS
FPS 是测量用于保存、显示动态视频的信息数量,每秒钟帧数愈多,所显示的动作就会愈流畅,一般应用只要保持 FPS 在 50-60,应用就会给用户流畅的感觉,反之,用户则会感觉到卡顿。
代码实现:
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
}
- (id)init {
self = [super init];
if( self ){
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
return self;
}
- (void)dealloc {
[_link invalidate];
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;
}
由于 CADisplayLink 的
frameInterval值默认为 1,代表与帧刷新同步,这样可以通过 CADisplayLink 一个时间段内定时器方法刷新次数算出屏幕 FPS 值
注意:值得注意的是基于 CADisplayLink 实现的 FPS 在生产场景中只有指导意义,不能代表真实的 FPS,因为基于 CADisplayLink 实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop 的帧率。
八 卡顿
监控卡顿,最直接就是找到主线程。我们知道一个线程的消息事件处理都是依赖于 NSRunLoop 来驱动,所以要知道线程正在调用什么方法,就需要从 NSRunLoop 来入手。发现 NSRunLoop 调用方法主要就是在 kCFRunLoopBeforeSources 和kCFRunLoopBeforeWaiting 之间,还有kCFRunLoopAfterWaiting 之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

代码实现:
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MyClass *object = (__bridge MyClass*)info;
// 记录状态值
object->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
Related Skills
node-connect
344.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
96.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
344.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
344.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
