简单的 iOS 性能监测工具

由于现在iOS这一块缺少性能测试,导致 app 性能优化方面工作并没有明确的工作目标。所以需要个必要的测试工具来数据化 app 的性能状况。

需要一个工具可以检测 app 的内存占用,CPU使用情况,FPS还有启动时间(可以通过 UIAutomation 来实现)
由于需要检测生产环境下各种友商 app,用于对比性能,所以决定用越狱后tweak来实现性能的检测。

Tweak 的一些实现

(示例代码:Github

tweak的主要功能是检测app启动后每一秒的内存使用量,CPU使用率,FPS。

内存使用量检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
// get current RAM usage
double usedMemory()
{
task_basic_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(),
TASK_BASIC_INFO, (task_info_t)&taskInfo, &infoCount);
if(kernReturn != KERN_SUCCESS) {
return NSNotFound;
}
return taskInfo.resident_size / 1024.0 / 1024.0;
}

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
55
56
57
58
59
60
61
// get current CPU usage
float cpu_usage()
{
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(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
if (kr != KERN_SUCCESS) {
return -1;
}
task_basic_info_t basic_info;
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;
uint32_t stat_thread = 0;
basic_info = (task_basic_info_t)tinfo;
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
return -1;
}
if (thread_count > 0)
stat_thread += thread_count;
long tot_sec = 0;
long tot_usec = 0;
float tot_cpu = 0;
int j;
for (j = 0; j < 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)) {
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 * 100.0;
}
}
kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);
return tot_cpu;
}

FPS 监测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)displayLinkTick:(CADisplayLink *)displayLink
{
static CFTimeInterval lastTimeInterval = 0.0;
CFTimeInterval cuTime = CACurrentMediaTime();
if (!lastTimeInterval) {
lastTimeInterval = cuTime;
return;
}
CFTimeInterval newInterval = cuTime - lastTimeInterval;
lastTimeInterval = cuTime;
if (newInterval > 1.0 || newInterval <= 0) {
return;
}
currentFPS = round(1.0/ newInterval);
}

由于想要做到通用性,所以选择在 UIApplicationdelegate 里面的 -application:didFinishLaunchingWithOptions: 来启动检测,由于是 delegate,正常的 Logos 语法没法准确 hook ,于是决定用运行时 Method Swizzling (参考:念茜 Objective-C的hook方案(一): Method Swizzling

自定义IMP用于替换之前的 -application:didFinishLaunchingWithOptions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id myDidFinishLaunchingWithOptions_imp(id self, SEL cmd, UIApplication *application, NSDictionary *launchOptions)
{
BOOL ret = [self myApplication:application didFinishLaunchingWithOptions:launchOptions];
// remove old log file
NSString *path = [NSString stringWithFormat:@"/tmp/%@.plist", [NSBundle mainBundle].bundleIdentifier];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
// creat a container for recording the log
logList = [[NSMutableArray alloc] init];
// get current FPS
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimer) userInfo:nil repeats:YES];
[timer fire];
return @(ret);
}

然后在App进入到后台的时候,进行检测记录的文件写入,替换原来的 -applicationDidEnterBackground:

1
2
3
4
5
6
id myApplicationDidEnterBackground_imp(id self, SEL cmd, UIApplication *application, NSDictionary *launchOptions)
{
[self myApplicationDidEnterBackground:application];
[self writeLogsToFile];
}

实现方法替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setDelegate:(id)delegate
{
%orig;
Method method1 = class_getInstanceMethod([delegate class], @selector(application:didFinishLaunchingWithOptions:));
class_addMethod([delegate class], @selector(myApplication:didFinishLaunchingWithOptions:), (IMP)myDidFinishLaunchingWithOptions_imp, method_getTypeEncoding(method1));
Method method2 = class_getInstanceMethod([delegate class], @selector(myApplication:didFinishLaunchingWithOptions:));
method_exchangeImplementations(method1, method2);
Method method3 = class_getInstanceMethod([delegate class], @selector(applicationDidEnterBackground:));
class_addMethod([delegate class], @selector(myApplicationDidEnterBackground:), (IMP)myApplicationDidEnterBackground_imp, method_getTypeEncoding(method3));
Method method4 = class_getInstanceMethod([delegate class], @selector(myApplicationDidEnterBackground:));
method_exchangeImplementations(method3, method4);
}

Tweak 的使用

和其他的越狱工具大体相当,在 .plist 文件里声明好你想要 hook 的 app(也就是你想要监测的)。利用 Theos 可以把你写好的工具生成一个 dylib ,然后通过已越狱的手机就可以运行了。当然了,如果你要想生成一些报告,还需要在工具中实现。