级别:★★★☆☆
标签:「runtime」「runloop」「hook」「objc_msgSend」
做者: 647
审校: QiShare团队php
前言:
最近,在看戴铭老师关于 “性能监控” 相关的技术分享,感受收获不少。基于最近的学习,总结了一些性能监控相关的实践,并计划落地一系列 “性能监控” 相关的文章。
目录以下:
iOS 性能监控(一)—— CPU功耗监控
iOS 性能监控(二)—— 主线程卡顿监控
iOS 性能监控(三)—— 方法耗时监控git
本篇将介绍iOS性能监控工具(QiLagMonitor)中与 “方法耗时监控” 相关的功能模块。github
定义:hook
是指在原有方法开始执行时,换成你指定的方法。或在原有方法的执行先后,添加执行你指定的方法。从而达到改变指定方法的目的。web
例如:swift
runtime
的 Method Swizzle
。Facebook
所开源的fishhook框架。前者是ObjC
运行时提供的“方法交换”能力。 后者是对Mach-O
二进制文件的符号进行动态的“从新绑定”,已达到方法交换的目的。bash
在《iOS App启动优化(一)—— 了解App的启动流程》中咱们提到,动态连接器dyld
会根据Mach-O
二进制可执行文件的符号表来绑定符号。而经过符号表及符号名就能够知道指针访问的地址,再经过更改指针访问的地址就能替换指定的方法实现了。微信
由于objc_msgSend
是全部Objective-C
方法调用的必经之路,全部的Objective-C
方法都会调用到运行时底层的objc_msgSend
方法。因此只要咱们能够hook objc_msgSend
,咱们就能够掌握全部objc
方法的耗时。(更多详情可看我以前写的《iOS 编写高质量Objective-C代码(二)》的第六点 —— 理解objc_msgSend(对象的消息传递机制))app
另外,objc_msgSend
自己是用汇编语言写的,苹果已经开源了objc_msgSend
的源码。可在官网上下载查看:objc_msgSend源码。框架
struct rebinding {
const char *name;
void *replacement;
void **replaced;
};
struct rebindings_entry {
struct rebinding *rebindings;
size_t rebindings_nel;
struct rebindings_entry *next;
};
复制代码
dyld
内全部的image
,取出其中的header
和slide
。 以便咱们接下来拿到符号表。static int fish_rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
if (retval < 0) {
return retval;
}
// If this was the first call, register callback for image additions (which is also invoked for
// existing images, otherwise, just run on existing images
//首先是遍历 dyld 里的全部的 image,取出 image header 和 slide。注意第一次调用时主要注册 callback
if (!_rebindings_head->next) {
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
uint32_t c = _dyld_image_count();
// 遍历全部dyld的image
for (uint32_t i = 0; i < c; i++) {
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); // 读取image内的header和slider
}
}
return retval;
}
复制代码
dyld
内拿到了全部image
。 接下来,咱们从image
内找到符号表内相关的segment_command_t
,遍历符号表找到所要替换的segname
,再进行下一步方法替换。方法实现以下:static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}
// 找到符号表相关的command,包括 linkedit_segment command、symtab command 和 dysymtab command。
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// 得到base符号表以及对应地址
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// 得到indirect符号表
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
复制代码
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab) {
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
for (uint i = 0; i < section->size / sizeof(void *); i++) {
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char *symbol_name = strtab + strtab_offset;
if (strnlen(symbol_name, 2) < 2) {
continue;
}
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}
复制代码
到这里,经过调用下面的方法,咱们就拥有了hook
的基本能力。ide
static int fish_rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
复制代码
hook_objc_msgSend
方法由于objc_msgSend
是经过汇编语言写的,咱们想要替换objc_msgSend
方法还须要从汇编语言下手。
既然咱们要作一个监控方法耗时的工具。这时想一想咱们的目的是什么?
咱们的目的是:经过hook
原objc_msgSend
方法,在objc_msgSend
方法前调用打点计时操做,在objc_msgSend
方法调用后结束打点和计时操做。经过计算时间差,咱们就能精准的拿到方法调用的时长。
所以,咱们要在原有的objc_msgSend
方法的调用先后须要加上before_objc_msgSend
和after_objc_msgSend
方法,以便咱们后期的打点计时操做。
arm64 有 31 个 64 bit 的整数型寄存器,分别用 x0 到 x30 表示。主要的实现思路是:
里面涉及到的一些汇编指令:
指令 | 含义 |
---|---|
stp | 同时写入两个寄存器。 |
mov | 将值赋值到一个寄存器。 |
ldp | 同时读取两个寄存器。 |
sub | 将两个寄存器的值相减 |
add | 将两个寄存器的值相加 |
ret | 从子程序返回主程序 |
详细代码以下:
#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() // stp入栈指令 入栈参数,参数寄存器是 x0~ x7。对于 objc_msgSend 方法来讲,x0 第一个参数是传入对象,x1 第二个参数是选择器 _cmd。syscall 的 number 会放到 x8 里。
__asm volatile ("mov x2, lr\n");
__asm volatile ("mov x3, x4\n");
// Call our before_objc_msgSend.
call(blr, &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, &after_objc_msgSend)
// restore lr
__asm volatile ("mov lr, x0\n");
// Load original objc_msgSend return value.
load()
// return
ret()
}
复制代码
这时候,每当底层调用hook_objc_msgSend
方法时,会先调用before_objc_msgSend
方法,再调用hook_objc_msgSend
方法,最后调用after_objc_msgSend
方法。
单个方法调用,流程以下图:
举一反“三”,而后多层方法调用的流程,就变成了下图:
这样,咱们就能拿到每一层方法调用的耗时了。
第一步,在项目中,导入QiLagMonitor类库。
第二步,在所须要监控的控制器中,导入QiCallTrace.h
头文件。
[QiCallTrace start]; // 1. 开始
// your codes(你所要测试的代码区间)
[QiCallTrace stop]; // 2. 中止
[QiCallTrace save]; // 3. 保存并打印方法调用栈以及具体方法耗时。
复制代码
PS:目前该工具只能hook
全部objc
方法,并计算出区间内的全部方法耗时。暂不支持swift方法的监听。
最后,本系列我是站在iOS业界巨人的肩膀上完成的,感谢戴铭老师精彩的技术分享。 祝你们学有所成,工做顺利。 另附上,戴铭老师课程连接:《iOS开发高手课》,谢谢!
了解更多iOS及相关新技术,请关注咱们的公众号:
小编微信:可加并拉入《QiShare技术交流群》。
关注咱们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)
推荐文章:
元旦福利!QiShare给你们发2020新年红包啦~
iOS 性能监控(一)—— CPU功耗监控
iOS 性能监控(二)—— 主线程卡顿监控
初识Flutter web
用SwiftUI给视图添加动画
用SwiftUI写一个简单页面
iOS App启动优化(三)—— 本身作一个工具监控App的启动耗时
iOS App启动优化(二)—— 使用“Time Profiler”工具监控App的启动耗时
iOS App启动优化(一)—— 了解App的启动流程
奇舞周刊