前言

主线程耗时是一个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 能够得到 Classid 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;// 记录调用classnewRecord->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 microsecondstv_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];// 保存调用classlog->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();// returnret();
}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;
}#elsevoid 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 frameSignLRstp    fp, lr, [sp, #-16]!mov    fp, sp// save parameter registers: x0..x8, q0..q7sub    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 x1mov    x2, x16bl    __class_lookupMethodAndLoadCache3// IMP in x0mov    x17, x0// restore registers and returnldp    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, fpldp    fp, lr, [sp], #16AuthenticateLR.endmacro*/复制代码

转载于:https://juejin.im/post/5c95e75df265da60f6730f0c

iOS主线程耗时检测方案相关推荐

  1. Android主线程耗时方法监控

    前言 相信各位大佬肯定在开发中遇到过这个问题:一顿操作猛如虎之后,总感觉程序有时候有点卡顿:或者自己自测得好好的,但是测试妹子的机器上却卡成狗. 那么,能不能做到运行APP之后,能自动地列出哪些类的哪 ...

  2. iOS开发--APP性能检测方案汇总(一)

    Linux编程点击右侧关注,免费入门到精通! 作者丨青苹果园 https://github.com/SilongLi 1 . CPU 占用率 CPU作为手机的中央处理器,可以说是手机最关键的组成部分, ...

  3. iOS主线程卡顿监测

    iOS开发过程中,我们有时需要监控主线程的卡顿情况,本文介绍主线程卡顿的实现方法. 新建MainThreadMonitor类,并且在.h中声明方法. #import <Foundation/Fo ...

  4. iOS 主线程和主队列

    先说结论, 主队列的任务只在主线程中被执行的,而主线程运行的是一个 runloop,不仅仅只有主队列的中的任务,还会处理 UI 的布局和绘制任务, 同时还可能会有其他队列中的任务. 举个例子 问题1: ...

  5. Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿

    原标题:Android性能优化实战之界面卡顿 作者:红橙Darren https://www.jianshu.com/p/18bb507d6e62 今天是个奇怪的日子,有三位同学找我,都是关于界面卡顿 ...

  6. ios实时卡顿检测和优化方案

    在移动设备上开发软件,性能一直是我们最为关心的话题之一,我们作为程序员除了需要努力提高代码质量之外,及时发现和监控软件中那些造成性能低下的"罪魁祸首"也是我们神圣的职责.友盟+U- ...

  7. [译]不要在UI主线程中进行耗时的操作

    原文: Why Ice Cream Sandwich Crashes your App 问题 自Android Ice Cream Sandwich发布后, 这个问题就开始在StackOverflow ...

  8. iOS面试:主线程如何进行阻塞?

    前言: 今天在群里, 有个小伙伴问了一个问题, 他们和js交互的时候, 需要等待js的一个回调. 主线程调用一个网络请求, 然后要等网络请求的结果回来之后, 然后在return. 他说, 他们之前是根 ...

  9. iOS程序UI主线程和定时器相互阻塞的问题

    解决iOS程序UI主线程和定时器相互阻塞的问题 转载于:https://www.cnblogs.com/hl666/p/3784838.html

最新文章

  1. addroutes刷新_vue 解决addRoutes动态添加路由后刷新失效问题
  2. 如何开启Dubbo框架内部的日志?
  3. 铝聚合物电池工作温度及低温电量差异的说明
  4. enterFrame是什么意思?
  5. android调用文件管理器回调,Android中调用系统管理器选择文件并返回绝对路径
  6. x86_64 Linux 运行时栈的字节对齐
  7. python打开浏览器全屏_python 打开浏览器的两种方式
  8. ZYNQ系统中实现FAT32文件系统的SD卡读写 之一 硬件介绍
  9. isp邮件服务器是什么,与ISP企业邮箱共建邮件服务器
  10. TCP/IP协议头部结构与解析
  11. 微圈社群助手到底是什么?怎么用?
  12. 占豪--2010年的市场机会在哪里(兼谈股指期货与楼市)
  13. rk3399 中间层移除短按power息屏待机+永不深度睡眠
  14. ios使用el-select的远程搜索无法唤起软键盘
  15. Channel接口的层次结构解析
  16. 多段曲线控温“核函数“算法分析(完整梯形图源代码)
  17. 图形化开发(六)01-Three.js之导入模型——3dmax和SketchUp-editor编辑器导出json文件,在创建模型initMesh中外部的JSON文件
  18. IIS简介和WEB部署、FTP部署
  19. 2013年9月4日第一篇CSDN个人博客
  20. CentOS 7.8 remove JDK

热门文章

  1. 漫画:Object 类很大,你忍一下(完结篇)
  2. 百架无人机“失控撞楼”,程序员写的 Bug?
  3. bzoj2458: [BeiJing2011]最小三角形
  4. Mrtg搭建流量监控服务器
  5. JFreeChart_API
  6. Vue 2.5中将迎来有关TypeScript的改进!
  7. 使用 lombok 简化 Java 代码
  8. 认识网页:html + css + JavaScript
  9. YCProgress自定义百分比进度条
  10. Python标准库:内置函数tuple([iterable])