iOS主线程耗时检测方案
栏目: Objective-C · 发布时间: 5年前
内容简介:主线程耗时是一个App性能的重要指标。主线程阻塞,立马会引起用户操作的卡顿,这是最直接的反应,所以是我们必须关注的一个性能点。Time Profiler模板使用Time Profiler工具对系统CPU上运行的进程执行低开销,基于时间的采样,显示App对多核CPU和线程的使用情况。随着时间的推移,使用多个核心和线程的效率越高,App的性能就越好。
主线程耗时是一个App性能的重要指标。主线程阻塞,立马会引起用户操作的卡顿,这是最直接的反应,所以是我们必须关注的一个性能点。
检测方案
Instrument - Time Profiler
Time Profiler模板使用Time Profiler工具对系统CPU上运行的进程执行低开销,基于时间的采样,显示App对多核CPU和线程的使用情况。
随着时间的推移,使用多个核心和线程的效率越高,App的性能就越好。
不熟悉的同学,可以参考官方文档 Track CPU core and thread use
你需要在用Time Profiler之前,需要开启生成dSYM符号文件,否则你只能看到系统函数的调用。
Debug模式,默认不会生成dSYM符号文件。
需要在Build Setting > Debug Infomation Format 选项中,为Debug开启dSYM文件的生成。
然后启动Xcode,build当前项目。
再打开Instrument,选择Time Profiler模板,开始录制。
Time Profiler中会记录每个线程中的函数调用关系树,使我们更容易定位到是哪一段代码导致了线程的阻塞。
双击这条记录,就能看到这段代码的源码
OK,到此为止,Time Profiler就介绍到这里,几乎都是UI界面,大家很容易就能使用了。
那说说Time Profiler的缺点
- 检测时,必须有Xcode支持。
- 真机检测时,必须处于联机状态。
- 无法实现自定义的输出内容。
自制检测工具
Time Profiler虽然好用,但也有局限性,这时自己搭建一套检测工具,想必是大家都会想的事情。
这个检测 工具 的功能可以参照Time Profiler
- 函数调用的关系树
- 通过配置对统计数据进行筛选
- 捕获的函数尽可能的多
- 格式化输出统计数据
- 统计数据日志化管理,上传到服务器
开始我也是从Method Swizzle思路出发,对UIViewController、UIView的耗时进行了,可惜仅仅这么做的话,统计的颗粒度太粗了,实际用起来并不好。
hook objc_msgSend函数会是个更好的选择。我查阅了objc4的源码后,发现会涉及到对C库的Hook,以及使用汇编语言对objc_msgSend实现的重写,线程的局部存储。
好在,iOS发展到现在,已经有很多的大神给我们提供了轮子,比如我这找到了 戴铭老师
的轮子进行二次封装。
我的项目还没整理完,不过实现原理基本借鉴了 戴铭老师
的设计,大家可以参考他的项目搭建自己的统计系统。
借鉴的项目地址: GCDFetchFeed
C库的hook用的是FaceBook的 fishhook ,我就不多介绍了,这个大家应该耳熟能详了吧。
此次封装涉及文件:
SMCallTrace.h SMCallTrace.m SMCallTraceCore.c SMCallTraceCore.h
我对hook objc_msgSend方法的主要实现部分进行了代码注释,希望能帮助大家理解,hook是如何完成的。
// // GHObjcMsgSendHook.c // CommercialVehiclePlatform // // Created by JunhuaShao on 2019/3/17. // Copyright © 2019 JunhuaShao. All rights reserved. // /******************************************************** objc_msgSend Hook代码来源:https://github.com/ming1016/GCDFetchFeed 线程局部存储:https://blog.csdn.net/vevenlcf/article/details/77882985 ******************************************************** */ #import "GHObjcMsgSendHook.h" // 此Hook只支持arm64架构 #ifdef __aarch64__ //#import <objc/runtime.h> //#import <sys/time.h> // //#import <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stddef.h> #include <stdint.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/time.h> #include <objc/message.h> #include <objc/runtime.h> #include <dispatch/dispatch.h> #include <pthread.h> #import "fishhook.h" /** Configuration */ // 调用记录工具开关 static bool _call_record_enabled = true; // 设置最小耗时阈值,单位:微秒 static uint64_t _min_time_cost = 1000; //us // 设置最大调用深度阈值 static int _max_call_depth = 3; /** iVar */ // 被替换的原objc_msgSend __unused static id (*orig_objc_msgSend)(id, SEL, ...); // 用于和调用线程关联的key,在开启工具时初始化 static pthread_key_t _thread_key; // 格式化的耗时记录 static GHCallRecord *_ghCallRecords; // 格式化的耗时记录占用空间 static int _ghRecordAlloc; // 格式化耗时记录的个数 static int _ghRecordNum; /** 被hook函数的调用记录 */ typedef struct { // 通过 object_getClass 能够得到 Class id self; // 通过 NSStringFromClass 能够得到类名 Class cls; // 通过 NSStringFromSelector 方法能够得到方法名 SEL cmd; // us 调用时间(微秒) uint64_t time; // link register 用于指定下一个函数的地址 uintptr_t lr; } thread_call_record; /** 线程中函数调用栈 */ typedef struct { // 当前被hook函数的调用记录 thread_call_record *stack; // 当前存储空间大小 int allocated_length; // 当前记录序号 int index; // 是否在主线程上 bool is_main_thread; } thread_call_stack; /** 获得线程中的函数调用栈 @return 函数调用栈 */ static inline thread_call_stack * get_thread_call_stack() { /** int pthread_setspecific (pthread_key_t key, const void *value) 用于将value的副本存储于一数据结构中,并将其与调用线程以及key相关联。 参数value通常指向由调用者分配的一块内存。 当线程终止时,会将该指针作为参数传递给与key相关联的destructor函数。 void *pthread_getspecific (pthread_key_t key); 当线程被创建时,会将所有的线程局部存储变量初始化为NULL。 因此第一次使用此类变量前必须先调用pthread_getspecific()函数来确认是否已经于对应的key相关联, 如果没有,那么可以通过分配一块内存并通过pthread_setspecific()函数保存指向该内存块的指针。 */ thread_call_stack *cs = (thread_call_stack *)pthread_getspecific(_thread_key); if (cs == NULL) { // 为函数调用栈开辟空间 cs = (thread_call_stack *)malloc(sizeof(thread_call_stack)); // 为hook函数记录开辟空间,大小为128个记录大小 cs->stack = (thread_call_record *)calloc(128, sizeof(thread_call_record)); // 初始当前存储空间为64个记录大小 cs->allocated_length = 64; // 初始化序号 cs->index = -1; /** int pthread_main_np(void); 如果在主线程上,会返回不为零的结果 */ cs->is_main_thread = pthread_main_np(); // 将调用栈与线程关联 pthread_setspecific(_thread_key, cs); } return cs; } static void release_thread_call_stack(void *ptr) { thread_call_stack *cs = (thread_call_stack *)ptr; if (!cs) return; // 释放调用栈 if (cs->stack) free(cs->stack); free(cs); } static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr) { // 获得当前线程关联的调用栈 thread_call_stack *cs = get_thread_call_stack(); if (cs) { // 序号增一 int nextIndex = (++cs->index); // 如果序号超过了当前存储空间大小 if (nextIndex >= cs->allocated_length) { // 将当前存储空间增长64个记录大小 cs->allocated_length += 64; // 为指针重新分配调用栈的空间,为当前存储空间大小。 cs->stack = (thread_call_record *)realloc(cs->stack, cs->allocated_length * sizeof(thread_call_record)); } // 获得当前序号对应的内存地址,创建新记录 thread_call_record *newRecord = &cs->stack[nextIndex]; // 记录调用对象 newRecord->self = _self; // 记录调用class newRecord->cls = _cls; // 记录调用函数 newRecord->cmd = _cmd; // 记录下一个调用函数地址 newRecord->lr = lr; /** 当前线程为主线程,并且开启了调用记录功能。 目的是只统计主线程耗时 */ if (cs->is_main_thread && _call_record_enabled) { /** Linux定义的timeval结构体 __darwin_time_t tv_sec; //seconds __darwin_suseconds_t tv_usec; //and microseconds tv_sec为Epoch到创建struct timeval时的秒数, tv_usec为微秒数,即秒后面的零头。 这里用了高精度,所以对两者进行了相加,取了最近的100秒 */ struct timeval now; // 获得当前时间 gettimeofday(&now, NULL); newRecord->time = (now.tv_sec % 100) * 1000000 + now.tv_usec; } } } static inline uintptr_t pop_call_record() { // 获取当前调用栈 thread_call_stack *cs = get_thread_call_stack(); // 当前调用记录序号 int curIndex = cs->index; // 父级函数调用记录序号 int nextIndex = cs->index--; // 获取父级函数调用记录,出栈 thread_call_record *pRecord = &cs->stack[nextIndex]; // 同样是主线程,并且开启记录功能 if (cs->is_main_thread && _call_record_enabled) { // 获取当前时间 struct timeval now; gettimeofday(&now, NULL); uint64_t time = (now.tv_sec % 100) * 1000000 + now.tv_usec; // 如果当前时间小于上次记录的时间,则进位了,这里加上100秒 if (time < pRecord->time) { time += 100 * 1000000; } // 获得耗时 uint64_t cost = time - pRecord->time; // 耗时大于耗时阈值,调用深度小于最大深度,则进行记录。这里调用序号,即为深度 if (cost > _min_time_cost && cs->index < _max_call_depth) { // 初始化格式化耗时记录 if (!_ghCallRecords) { // 创建空间大小为1024个记录 _ghRecordAlloc = 1024; _ghCallRecords = (GHCallRecord *)malloc(sizeof(GHCallRecord)*_ghRecordAlloc); } // 记录个数加一 _ghRecordNum++; // 当前记录个数大于空间时,重新为指针分配内存,大小为比原来多1024个记录 if (_ghRecordNum >= _ghRecordAlloc) { _ghRecordAlloc += 1024; _ghCallRecords = (GHCallRecord *)realloc(_ghCallRecords, sizeof(GHCallRecord) * _ghRecordAlloc); } // 获取当前页数对应的地址,创建格式化记录。 GHCallRecord *log = &_ghCallRecords[_ghRecordNum - 1]; // 保存调用class log->cls = pRecord->cls; // 保存调用深度 log->depth = curIndex; // 保存调用方法 log->sel = pRecord->cmd; // 保存耗时 log->time = cost; } } // 返回下个函数的调用地址 return pRecord->lr; } void hook_before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) { // 函数调用记录入栈 push_call_record(self, object_getClass(self), _cmd, lr); } uintptr_t hook_after_objc_msgSend() { // 函数调用记录出栈 return pop_call_record(); } // replacement objc_msgSend (arm64) // https://blog.nelhage.com/2010/10/amd64-and-va_arg/ // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf // https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html #define call(b, value) \ __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \ __asm volatile ("mov x12, %0\n" :: "r"(value)); \ __asm volatile ("ldp x8, x9, [sp], #16\n"); \ __asm volatile (#b " x12\n"); #define save() \ __asm volatile ( \ "stp x8, x9, [sp, #-16]!\n" \ "stp x6, x7, [sp, #-16]!\n" \ "stp x4, x5, [sp, #-16]!\n" \ "stp x2, x3, [sp, #-16]!\n" \ "stp x0, x1, [sp, #-16]!\n"); #define load() \ __asm volatile ( \ "ldp x0, x1, [sp], #16\n" \ "ldp x2, x3, [sp], #16\n" \ "ldp x4, x5, [sp], #16\n" \ "ldp x6, x7, [sp], #16\n" \ "ldp x8, x9, [sp], #16\n" ); #define link(b, value) \ __asm volatile ("stp x8, lr, [sp, #-16]!\n"); \ __asm volatile ("sub sp, sp, #16\n"); \ call(b, value); \ __asm volatile ("add sp, sp, #16\n"); \ __asm volatile ("ldp x8, lr, [sp], #16\n"); #define ret() __asm volatile ("ret\n"); __attribute__((__naked__)) static void hook_Objc_msgSend() { // Save parameters. save(); __asm volatile ("mov x2, lr\n"); __asm volatile ("mov x3, x4\n"); // Call our before_objc_msgSend. call(blr, &hook_before_objc_msgSend); // Load parameters. load(); // Call through to the original objc_msgSend. call(blr, orig_objc_msgSend); // Save original objc_msgSend return value. save(); // Call our after_objc_msgSend. call(blr, &hook_after_objc_msgSend); // restore lr __asm volatile ("mov lr, x0\n"); // Load original objc_msgSend return value. load(); // return ret(); } void ghAnalyerStart() { _call_record_enabled = true; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ pthread_key_create(&_thread_key, &release_thread_call_stack); rebind_symbols((struct rebinding[6]){ {"objc_msgSend", (void *)hook_Objc_msgSend, (void **)&orig_objc_msgSend}, }, 1); }); } void ghAnalyerStop() { _call_record_enabled = false; } void ghSetMinTimeCallCost(uint64_t us) { _min_time_cost = us; } void ghSetMaxCallDepth(int depth) { _max_call_depth = depth; } GHCallRecord *ghGetCallRecords(int *num) { if (num) { *num = _ghRecordNum; } return _ghCallRecords; } void ghClearCallRecords() { if (_ghCallRecords) { free(_ghCallRecords); _ghCallRecords = NULL; } _ghRecordNum = 0; } #else void ghAnalyerStart() {} void ghAnalyerStop() {} void ghSetMinTimeCallCost(uint64_t us) { } void ghSetMaxCallDepth(int depth) { } GHCallRecord *ghGetCallRecords(int *num) { if (num) { *num = 0; } return NULL; } void ghClearCallRecords() {} #endif /** 下面是Hook objc_msgSend的相关部分汇编源码 .macro MethodTableLookup // push frame SignLR stp fp, lr, [sp, #-16]! mov fp, sp // save parameter registers: x0..x8, q0..q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // receiver and selector already in x0 and x1 mov x2, x16 bl __class_lookupMethodAndLoadCache3 // IMP in x0 mov x17, x0 // restore registers and return ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 AuthenticateLR .endmacro */ 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。