前言: iOS 高级之美 是本人总结了一些工作实际开发研究以及面试重点,围绕底层进行 源码分析
- LLDB 调试
- 源码断点
- 汇编调试
,让读者真正感受 Runtime底层之美~😊
目录如下:
iOS 高级之美(一)—— iOS_objc4-756.2 最新源码编译调试
iOS 高级之美(二)—— OC对象底层上篇
iOS 高级之美(三)—— OC对象底层下篇
iOS 高级之美(四)—— isa原理分析
iOS 高级之美(五)—— 类结构分析上篇
iOS 高级之美(六)—— malloc分析
iOS 高级之美(七)—— 24K纯技术男~KC_2019年终总结
iOS 高级之美(八)—— 类结构分析中篇
iOS 高级之美(九)—— 类结构分析下篇
iOS 高级之美(十)—— 消息发送

一、方法的本质
介绍完Objective-C
的基础数据结构,接下来就是方法的调用了。在讨论“消息”之前。我们先来思考一下,我们总是在说在objc
中,调用方法实际上是发送消息
,那么这个消息的机制是什么? 函数调用的本质应该是什么?我们可以通过一段C的代码来看下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson new];
}
return 0;
}
通过终端我们进行探索一下 : clang -rewrite-objc main.m
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
}
return 0;
}
- 底层
C++
代码更加接近本质
- 里面有很多的类型转换,为了方便查阅我简化一下 :
objc_msgSend(objc_getClass("LGPerson"), sel_registerName("new"))
- 就是通过
objc_msgSend
函数调用, 这种函数调用的方式也称为发送消息
接下来我们再看下面的分析
void func(){
}
int main(int argc, char * argv[]) {
func();
}
这里是一个非常简单的函数,下面通过clang
和objdump
命令来生成可执行文件并查看汇编实现


我们可以看到,函数的调用,实际上就是“地址”的跳转。而函数的地址是在编译期就能确定的(除一些动态库函数外),关于如何在编译期确定地址可以查看之前的静态链接文章。
但是Objective-C
是一门动态性语言,如果直接使用编译得到的地址,就不存在动态性,那么Objective-C
是如何解决这个问题的呢
二、objc_msgSend分析
2.1 objc_msgSend
的结果分析
此汇编结果为iOS13.2.3
真机版本的runtime。可参考arm64汇编文档
libobjc.A.dylib`objc_msgSend:
//判断self 是否 = nil
-> 0x1810aa080 <+0>: cmp x0, #0x0 ; =0x0
//如果是0或者小0,则跳转至 0x1810aa0f8 处
0x1810aa084 <+4>: b.le 0x1810aa0f8 ; <+120>
//读取x0的“值”存入x13中。x13=isa
0x1810aa088 <+8>: ldr x13, [x0]
//x16 = x13 & isamask
//xl6 = cls
0x1810aa08c <+12>: and x16, x13, #0xffffffff8
//读取x16偏移16字节的值,存入 x11
//x11 = cache_t和mask_t共同组成,前48位为cache_t
0x1810aa090 <+16>: ldr x11, [x16, #0x10]
/* CacheLookup */
//苹果开源的runtime(mac10.14)中,cache存储在cls偏移16字节处
//但在目前的版本中(10.15),cache_t会和mask_t公用这8个字节。使用0xffffffffffff获取cache_t。
//x10 = bucket_t = (x11 & 0xffffffffffff)
0x1810aa094 <+20>: and x10, x11, #0xffffffffffff
//x11 >> 48获取mask_t,
//x12 = mask_t & SEL = cache_hash(buckets的索引)
0x1810aa098 <+24>: and x12, x1, x11, lsr #48
//获取索引值相对buckets首地址的偏移量,cache_hash << 4,因为每个bucket占16字节,所以把索引 * 16;
//x12:取索引对应的bucket = buckets首地址+索引偏移量
0x1810aa09c <+28>: add x12, x10, x12, lsl #4
//读取bucket的内容,分别存入 x17,x9
//x17 = imp, x9 = cache_key(SEL)
0x1810aa0a0 <+32>: ldp x17, x9, [x12]
//cachekey == SEL
0x1810aa0a4 <+36>: cmp x9, x1
//cachekey != SEL,跳转到 0x1810aa0b4
0x1810aa0a8 <+40>: b.ne 0x1810aa0b4 ; <+52>
/* CacheHit */
0x1810aa0ac <+44>: eor x17, x17, x16
//直接执行cache中的IMP
0x1810aa0b0 <+48>: br x17
/* CheckMiss */
//调用x9 是否为0 。如果为0 ,这说明这个bucket是空桶,就跳转 _objc_msgSend_uncached->lookupIMPOrForward
0x1810aa0b4 <+52>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached
//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0b8 <+56>: cmp x12, x10
//如果相等,说明找了一圈,仍没有找到,则跳转至 0x1810aa0c8。
0x1810aa0bc <+60>: b.eq 0x1810aa0c8 ; <+72>
//bucket_t的地址详情偏移16,相当于 取当前bucket_t的前一个元素。
//把imp和key分别赋值给 x17 x9
0x1810aa0c0 <+64>: ldp x17, x9, [x12, #-0x10]!
//跳转回0x1810aa0a4,相当于 向前循环遍历查找buckets
0x1810aa0c4 <+68>: b 0x1810aa0a4 ; <+36>
//到这里说明 x12 是buckets中的第一个bucket。
//x11 为mask和buckets,这里右移 44位,说明了buckets实际只是用了 44bit而不是48
//这里相当于把x12重新指向buckets的最后一个bucekt。
//x12 = buckets->last
0x1810aa0c8 <+72>: add x12, x12, x11, lsr #44
//将bucket的imp和key 写入 x17 和 x9
0x1810aa0cc <+76>: ldp x17, x9, [x12]
//对比 SEL == key
0x1810aa0d0 <+80>: cmp x9, x1
//不相等 跳转到 0x1810aa0e0,判断cache_key 是否为空
0x1810aa0d4 <+84>: b.ne 0x1810aa0e0 ; <+96>
/* CacheHit */
//如果相等则
0x1810aa0d8 <+88>: eor x17, x17, x16
//执行x17:IMP
0x1810aa0dc <+92>: br x17
/* CheckMiss */
//如果此时 x9 == 0 则执行_objc_msgSend_uncached
0x1810aa0e0 <+96>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached
//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0e4 <+100>: cmp x12, x10
//如果等于则跳转0x1810aa0f4 -> _objc_msgSend_uncached
0x1810aa0e8 <+104>: b.eq 0x1810aa0f4 ; <+116>
//不相等则 向前查找
0x1810aa0ec <+108>: ldp x17, x9, [x12, #-0x10]!
//跳转0x1810aa0d0
0x1810aa0f0 <+112>: b 0x1810aa0d0 ; <+80>
//跳转_objc_msgSend_uncached
0x1810aa0f4 <+116>: b 0x1810aa3c0 ; _objc_msgSend_uncached
//msgSeng刚开始的第二行判断,这里是nil或者tagged
0x1810aa0f8 <+120>: b.eq 0x1810aa130 ; <+176>
0x1810aa0fc <+124>: adrp x10, 294986
0x1810aa100 <+128>: add x10, x10, #0x340 ; =0x340
0x1810aa104 <+132>: lsr x11, x0, #60
0x1810aa108 <+136>: ldr x16, [x10, x11, lsl #3]
0x1810aa10c <+140>: adrp x10, 294986
0x1810aa110 <+144>: add x10, x10, #0x2a8 ; =0x2a8
0x1810aa114 <+148>: cmp x10, x16
0x1810aa118 <+152>: b.ne 0x1810aa090 ; <+16>
// ext tagged
0x1810aa11c <+156>: adrp x10, 294986
0x1810aa120 <+160>: add x10, x10, #0x3c0 ; =0x3c0
0x1810aa124 <+164>: ubfx x11, x0, #52, #8
0x1810aa128 <+168>: ldr x16, [x10, x11, lsl #3]
0x1810aa12c <+172>: b 0x1810aa090 ; <+16>
0x1810aa130 <+176>: mov x1, #0x0
0x1810aa134 <+180>: movi d0, #0000000000000000
0x1810aa138 <+184>: movi d1, #0000000000000000
0x1810aa13c <+188>: movi d2, #0000000000000000
0x1810aa140 <+192>: movi d3, #0000000000000000
0x1810aa144 <+196>: ret
0x1810aa148 <+200>: nop
0x1810aa14c <+204>: nop
0x1810aa150 <+208>: nop
0x1810aa154 <+212>: nop
0x1810aa158 <+216>: nop
0x1810aa15c <+220>: nop
这里没有分割开来 ,主要是为那些想连贯读下来的小伙伴分析!
整个objc_msgsend
可分为四个阶段:
①. 准备工作,包括获取类对象
,mask_t
,cash_hash(查找索引)
,buckets
等
②. 获取cash_hash
所在buckets
的bucket
,并向前遍历查找,这里有3中情况:
- 1. 如果找到则调用
IMP
。
- 2. 如果找到的
bucket.key == 0
,即bucket
为空桶,则调用 objc_msgSend_uncached
- 3. 如果找到第一个
bucket
,则跳转到3。
- ③. 从`buckets`的最后一个向前查找:
- 1. 如果找到则调用
IMP
。
- 2. 如果找到的
bucket.key == 0
,即bucket
为空桶,则调用 objc_msgSend_uncached
。
④. 调用objc_msgSend_uncached
。
2.2 伪代码
由于汇编看起来不是很直观,可直接参考下面的伪代码,同时这部分伪代码没有处理关于参数和返回值的逻辑。
void discover_msgSend(id self,SEL sel1){
if (self == nil) {
return;
}
SEL sel = sel1;
Class clsHasMask = *(Class *)self;
Class cls = (Class)((uintptr_t)clsHasMask & 0xffffffff8);
struct cache_t * cache = (struct cache_t *)((__bridge void *)cls + 0x10);
struct bucket_t *_buckets = (struct bucket_t *)((uintptr_t)cache->_buckets & 0xffffffffffff);
uintptr_t mask = (uintptr_t)cache->_buckets >> 48;
uintptr_t cache_hash = (uintptr_t)sel & mask;
struct bucket_t* bucket = (_buckets + cache_hash*1);//cache_hash*16
/* CacheLookup */
while ((*bucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*bucket)._key == 0) {
goto _objc_msgSend_uncached;
}
/* 查找到第一个元素了 */
if (bucket == _buckets) {
goto second_loop;
}
bucket = (bucket-1);//[x12, #-0x10]
}
if(((*bucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*bucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
goto _objc_msgSend_uncached;
}
return;
second_loop:
{
int64_t offset = ((uintptr_t)cache->_buckets >> 44);
struct bucket_t* lastBucket = (_buckets + offset/16);
/* CacheLookup */
while ((*lastBucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*lastBucket)._key == 0) {
goto _objc_msgSend_uncached;
}
lastBucket = (lastBucket-1);//[x12, #-0x10]
}
if(((*lastBucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*lastBucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
/* JumpMiss */
goto _objc_msgSend_uncached;
}
return;
}
_objc_msgSend_uncached:
{
printf("\n _objc_msgSend_uncached -> lookUpImpOrForward");
return;
}
}
通过上面的流程我们发现,objc_msgSend
会有两次的查找行为,这又是为什么呢?我们可以通过runtime
的源码中找到答案。
Clone scanning loop to miss instead of hang when cache is corrupt.
前言: iOS 高级之美 是本人总结了一些工作实际开发研究以及面试重点,围绕底层进行 源码分析
- LLDB 调试
- 源码断点
- 汇编调试
,让读者真正感受 Runtime底层之美~😊
目录如下:
iOS 高级之美(一)—— iOS_objc4-756.2 最新源码编译调试
iOS 高级之美(二)—— OC对象底层上篇
iOS 高级之美(三)—— OC对象底层下篇
iOS 高级之美(四)—— isa原理分析
iOS 高级之美(五)—— 类结构分析上篇
iOS 高级之美(六)—— malloc分析
iOS 高级之美(七)—— 24K纯技术男~KC_2019年终总结
iOS 高级之美(八)—— 类结构分析中篇
iOS 高级之美(九)—— 类结构分析下篇
iOS 高级之美(十)—— 消息发送

一、方法的本质
介绍完Objective-C
的基础数据结构,接下来就是方法的调用了。在讨论“消息”之前。我们先来思考一下,我们总是在说在objc
中,调用方法实际上是发送消息
,那么这个消息的机制是什么? 函数调用的本质应该是什么?我们可以通过一段C的代码来看下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson new];
}
return 0;
}
通过终端我们进行探索一下 : clang -rewrite-objc main.m
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
}
return 0;
}
- 底层
C++
代码更加接近本质
- 里面有很多的类型转换,为了方便查阅我简化一下 :
objc_msgSend(objc_getClass("LGPerson"), sel_registerName("new"))
- 就是通过
objc_msgSend
函数调用, 这种函数调用的方式也称为发送消息
接下来我们再看下面的分析
void func(){
}
int main(int argc, char * argv[]) {
func();
}
这里是一个非常简单的函数,下面通过clang
和objdump
命令来生成可执行文件并查看汇编实现


我们可以看到,函数的调用,实际上就是“地址”的跳转。而函数的地址是在编译期就能确定的(除一些动态库函数外),关于如何在编译期确定地址可以查看之前的静态链接文章。
但是Objective-C
是一门动态性语言,如果直接使用编译得到的地址,就不存在动态性,那么Objective-C
是如何解决这个问题的呢
二、objc_msgSend分析
2.1 objc_msgSend
的结果分析
此汇编结果为iOS13.2.3
真机版本的runtime。可参考arm64汇编文档
libobjc.A.dylib`objc_msgSend:
//判断self 是否 = nil
-> 0x1810aa080 <+0>: cmp x0, #0x0 ; =0x0
//如果是0或者小0,则跳转至 0x1810aa0f8 处
0x1810aa084 <+4>: b.le 0x1810aa0f8 ; <+120>
//读取x0的“值”存入x13中。x13=isa
0x1810aa088 <+8>: ldr x13, [x0]
//x16 = x13 & isamask
//xl6 = cls
0x1810aa08c <+12>: and x16, x13, #0xffffffff8
//读取x16偏移16字节的值,存入 x11
//x11 = cache_t和mask_t共同组成,前48位为cache_t
0x1810aa090 <+16>: ldr x11, [x16, #0x10]
/* CacheLookup */
//苹果开源的runtime(mac10.14)中,cache存储在cls偏移16字节处
//但在目前的版本中(10.15),cache_t会和mask_t公用这8个字节。使用0xffffffffffff获取cache_t。
//x10 = bucket_t = (x11 & 0xffffffffffff)
0x1810aa094 <+20>: and x10, x11, #0xffffffffffff
//x11 >> 48获取mask_t,
//x12 = mask_t & SEL = cache_hash(buckets的索引)
0x1810aa098 <+24>: and x12, x1, x11, lsr #48
//获取索引值相对buckets首地址的偏移量,cache_hash << 4,因为每个bucket占16字节,所以把索引 * 16;
//x12:取索引对应的bucket = buckets首地址+索引偏移量
0x1810aa09c <+28>: add x12, x10, x12, lsl #4
//读取bucket的内容,分别存入 x17,x9
//x17 = imp, x9 = cache_key(SEL)
0x1810aa0a0 <+32>: ldp x17, x9, [x12]
//cachekey == SEL
0x1810aa0a4 <+36>: cmp x9, x1
//cachekey != SEL,跳转到 0x1810aa0b4
0x1810aa0a8 <+40>: b.ne 0x1810aa0b4 ; <+52>
/* CacheHit */
0x1810aa0ac <+44>: eor x17, x17, x16
//直接执行cache中的IMP
0x1810aa0b0 <+48>: br x17
/* CheckMiss */
//调用x9 是否为0 。如果为0 ,这说明这个bucket是空桶,就跳转 _objc_msgSend_uncached->lookupIMPOrForward
0x1810aa0b4 <+52>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached
//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0b8 <+56>: cmp x12, x10
//如果相等,说明找了一圈,仍没有找到,则跳转至 0x1810aa0c8。
0x1810aa0bc <+60>: b.eq 0x1810aa0c8 ; <+72>
//bucket_t的地址详情偏移16,相当于 取当前bucket_t的前一个元素。
//把imp和key分别赋值给 x17 x9
0x1810aa0c0 <+64>: ldp x17, x9, [x12, #-0x10]!
//跳转回0x1810aa0a4,相当于 向前循环遍历查找buckets
0x1810aa0c4 <+68>: b 0x1810aa0a4 ; <+36>
//到这里说明 x12 是buckets中的第一个bucket。
//x11 为mask和buckets,这里右移 44位,说明了buckets实际只是用了 44bit而不是48
//这里相当于把x12重新指向buckets的最后一个bucekt。
//x12 = buckets->last
0x1810aa0c8 <+72>: add x12, x12, x11, lsr #44
//将bucket的imp和key 写入 x17 和 x9
0x1810aa0cc <+76>: ldp x17, x9, [x12]
//对比 SEL == key
0x1810aa0d0 <+80>: cmp x9, x1
//不相等 跳转到 0x1810aa0e0,判断cache_key 是否为空
0x1810aa0d4 <+84>: b.ne 0x1810aa0e0 ; <+96>
/* CacheHit */
//如果相等则
0x1810aa0d8 <+88>: eor x17, x17, x16
//执行x17:IMP
0x1810aa0dc <+92>: br x17
/* CheckMiss */
//如果此时 x9 == 0 则执行_objc_msgSend_uncached
0x1810aa0e0 <+96>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached
//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0e4 <+100>: cmp x12, x10
//如果等于则跳转0x1810aa0f4 -> _objc_msgSend_uncached
0x1810aa0e8 <+104>: b.eq 0x1810aa0f4 ; <+116>
//不相等则 向前查找
0x1810aa0ec <+108>: ldp x17, x9, [x12, #-0x10]!
//跳转0x1810aa0d0
0x1810aa0f0 <+112>: b 0x1810aa0d0 ; <+80>
//跳转_objc_msgSend_uncached
0x1810aa0f4 <+116>: b 0x1810aa3c0 ; _objc_msgSend_uncached
//msgSeng刚开始的第二行判断,这里是nil或者tagged
0x1810aa0f8 <+120>: b.eq 0x1810aa130 ; <+176>
0x1810aa0fc <+124>: adrp x10, 294986
0x1810aa100 <+128>: add x10, x10, #0x340 ; =0x340
0x1810aa104 <+132>: lsr x11, x0, #60
0x1810aa108 <+136>: ldr x16, [x10, x11, lsl #3]
0x1810aa10c <+140>: adrp x10, 294986
0x1810aa110 <+144>: add x10, x10, #0x2a8 ; =0x2a8
0x1810aa114 <+148>: cmp x10, x16
0x1810aa118 <+152>: b.ne 0x1810aa090 ; <+16>
// ext tagged
0x1810aa11c <+156>: adrp x10, 294986
0x1810aa120 <+160>: add x10, x10, #0x3c0 ; =0x3c0
0x1810aa124 <+164>: ubfx x11, x0, #52, #8
0x1810aa128 <+168>: ldr x16, [x10, x11, lsl #3]
0x1810aa12c <+172>: b 0x1810aa090 ; <+16>
0x1810aa130 <+176>: mov x1, #0x0
0x1810aa134 <+180>: movi d0, #0000000000000000
0x1810aa138 <+184>: movi d1, #0000000000000000
0x1810aa13c <+188>: movi d2, #0000000000000000
0x1810aa140 <+192>: movi d3, #0000000000000000
0x1810aa144 <+196>: ret
0x1810aa148 <+200>: nop
0x1810aa14c <+204>: nop
0x1810aa150 <+208>: nop
0x1810aa154 <+212>: nop
0x1810aa158 <+216>: nop
0x1810aa15c <+220>: nop
这里没有分割开来 ,主要是为那些想连贯读下来的小伙伴分析!
整个objc_msgsend
可分为四个阶段:
①. 准备工作,包括获取类对象
,mask_t
,cash_hash(查找索引)
,buckets
等
②. 获取cash_hash
所在buckets
的bucket
,并向前遍历查找,这里有3中情况:
- 1. 如果找到则调用
IMP
。
- 2. 如果找到的
bucket.key == 0
,即bucket
为空桶,则调用 objc_msgSend_uncached
- 3. 如果找到第一个
bucket
,则跳转到3。
- ③. 从`buckets`的最后一个向前查找:
- 1. 如果找到则调用
IMP
。
- 2. 如果找到的
bucket.key == 0
,即bucket
为空桶,则调用 objc_msgSend_uncached
。
④. 调用objc_msgSend_uncached
。
2.2 伪代码
由于汇编看起来不是很直观,可直接参考下面的伪代码,同时这部分伪代码没有处理关于参数和返回值的逻辑。
void discover_msgSend(id self,SEL sel1){
if (self == nil) {
return;
}
SEL sel = sel1;
Class clsHasMask = *(Class *)self;
Class cls = (Class)((uintptr_t)clsHasMask & 0xffffffff8);
struct cache_t * cache = (struct cache_t *)((__bridge void *)cls + 0x10);
struct bucket_t *_buckets = (struct bucket_t *)((uintptr_t)cache->_buckets & 0xffffffffffff);
uintptr_t mask = (uintptr_t)cache->_buckets >> 48;
uintptr_t cache_hash = (uintptr_t)sel & mask;
struct bucket_t* bucket = (_buckets + cache_hash*1);//cache_hash*16
/* CacheLookup */
while ((*bucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*bucket)._key == 0) {
goto _objc_msgSend_uncached;
}
/* 查找到第一个元素了 */
if (bucket == _buckets) {
goto second_loop;
}
bucket = (bucket-1);//[x12, #-0x10]
}
if(((*bucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*bucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
goto _objc_msgSend_uncached;
}
return;
second_loop:
{
int64_t offset = ((uintptr_t)cache->_buckets >> 44);
struct bucket_t* lastBucket = (_buckets + offset/16);
/* CacheLookup */
while ((*lastBucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*lastBucket)._key == 0) {
goto _objc_msgSend_uncached;
}
lastBucket = (lastBucket-1);//[x12, #-0x10]
}
if(((*lastBucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*lastBucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
/* JumpMiss */
goto _objc_msgSend_uncached;
}
return;
}
_objc_msgSend_uncached:
{
printf("\n _objc_msgSend_uncached -> lookUpImpOrForward");
return;
}
}
通过上面的流程我们发现,objc_msgSend
会有两次的查找行为,这又是为什么呢?我们可以通过runtime
的源码中找到答案。
Clone scanning loop to miss instead of hang when cache is corrupt.
前言: iOS 高级之美 是本人总结了一些工作实际开发研究以及面试重点,围绕底层进行 源码分析
- LLDB 调试
- 源码断点
- 汇编调试
,让读者真正感受 Runtime底层之美~😊
目录如下:
iOS 高级之美(一)—— iOS_objc4-756.2 最新源码编译调试
iOS 高级之美(二)—— OC对象底层上篇
iOS 高级之美(三)—— OC对象底层下篇
iOS 高级之美(四)—— isa原理分析
iOS 高级之美(五)—— 类结构分析上篇
iOS 高级之美(六)—— malloc分析
iOS 高级之美(七)—— 24K纯技术男~KC_2019年终总结
iOS 高级之美(八)—— 类结构分析中篇
iOS 高级之美(九)—— 类结构分析下篇
iOS 高级之美(十)—— 消息发送

一、方法的本质
介绍完Objective-C
的基础数据结构,接下来就是方法的调用了。在讨论“消息”之前。我们先来思考一下,我们总是在说在objc
中,调用方法实际上是发送消息
,那么这个消息的机制是什么? 函数调用的本质应该是什么?我们可以通过一段C的代码来看下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson new];
}
return 0;
}
通过终端我们进行探索一下 : clang -rewrite-objc main.m
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
}
return 0;
}
- 底层
C++
代码更加接近本质
- 里面有很多的类型转换,为了方便查阅我简化一下 :
objc_msgSend(objc_getClass("LGPerson"), sel_registerName("new"))
- 就是通过
objc_msgSend
函数调用, 这种函数调用的方式也称为发送消息
接下来我们再看下面的分析
void func(){
}
int main(int argc, char * argv[]) {
func();
}
这里是一个非常简单的函数,下面通过clang
和objdump
命令来生成可执行文件并查看汇编实现


我们可以看到,函数的调用,实际上就是“地址”的跳转。而函数的地址是在编译期就能确定的(除一些动态库函数外),关于如何在编译期确定地址可以查看之前的静态链接文章。
但是Objective-C
是一门动态性语言,如果直接使用编译得到的地址,就不存在动态性,那么Objective-C
是如何解决这个问题的呢
二、objc_msgSend分析
2.1 objc_msgSend
的结果分析
此汇编结果为iOS13.2.3
真机版本的runtime。可参考arm64汇编文档
libobjc.A.dylib`objc_msgSend:
//判断self 是否 = nil
-> 0x1810aa080 <+0>: cmp x0, #0x0 ; =0x0
//如果是0或者小0,则跳转至 0x1810aa0f8 处
0x1810aa084 <+4>: b.le 0x1810aa0f8 ; <+120>
//读取x0的“值”存入x13中。x13=isa
0x1810aa088 <+8>: ldr x13, [x0]
//x16 = x13 & isamask
//xl6 = cls
0x1810aa08c <+12>: and x16, x13, #0xffffffff8
//读取x16偏移16字节的值,存入 x11
//x11 = cache_t和mask_t共同组成,前48位为cache_t
0x1810aa090 <+16>: ldr x11, [x16, #0x10]
/* CacheLookup */
//苹果开源的runtime(mac10.14)中,cache存储在cls偏移16字节处
//但在目前的版本中(10.15),cache_t会和mask_t公用这8个字节。使用0xffffffffffff获取cache_t。
//x10 = bucket_t = (x11 & 0xffffffffffff)
0x1810aa094 <+20>: and x10, x11, #0xffffffffffff
//x11 >> 48获取mask_t,
//x12 = mask_t & SEL = cache_hash(buckets的索引)
0x1810aa098 <+24>: and x12, x1, x11, lsr #48
//获取索引值相对buckets首地址的偏移量,cache_hash << 4,因为每个bucket占16字节,所以把索引 * 16;
//x12:取索引对应的bucket = buckets首地址+索引偏移量
0x1810aa09c <+28>: add x12, x10, x12, lsl #4
//读取bucket的内容,分别存入 x17,x9
//x17 = imp, x9 = cache_key(SEL)
0x1810aa0a0 <+32>: ldp x17, x9, [x12]
//cachekey == SEL
0x1810aa0a4 <+36>: cmp x9, x1
//cachekey != SEL,跳转到 0x1810aa0b4
0x1810aa0a8 <+40>: b.ne 0x1810aa0b4 ; <+52>
/* CacheHit */
0x1810aa0ac <+44>: eor x17, x17, x16
//直接执行cache中的IMP
0x1810aa0b0 <+48>: br x17
/* CheckMiss */
//调用x9 是否为0 。如果为0 ,这说明这个bucket是空桶,就跳转 _objc_msgSend_uncached->lookupIMPOrForward
0x1810aa0b4 <+52>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached
//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0b8 <+56>: cmp x12, x10
//如果相等,说明找了一圈,仍没有找到,则跳转至 0x1810aa0c8。
0x1810aa0bc <+60>: b.eq 0x1810aa0c8 ; <+72>
//bucket_t的地址详情偏移16,相当于 取当前bucket_t的前一个元素。
//把imp和key分别赋值给 x17 x9
0x1810aa0c0 <+64>: ldp x17, x9, [x12, #-0x10]!
//跳转回0x1810aa0a4,相当于 向前循环遍历查找buckets
0x1810aa0c4 <+68>: b 0x1810aa0a4 ; <+36>
//到这里说明 x12 是buckets中的第一个bucket。
//x11 为mask和buckets,这里右移 44位,说明了buckets实际只是用了 44bit而不是48
//这里相当于把x12重新指向buckets的最后一个bucekt。
//x12 = buckets->last
0x1810aa0c8 <+72>: add x12, x12, x11, lsr #44
//将bucket的imp和key 写入 x17 和 x9
0x1810aa0cc <+76>: ldp x17, x9, [x12]
//对比 SEL == key
0x1810aa0d0 <+80>: cmp x9, x1
//不相等 跳转到 0x1810aa0e0,判断cache_key 是否为空
0x1810aa0d4 <+84>: b.ne 0x1810aa0e0 ; <+96>
/* CacheHit */
//如果相等则
0x1810aa0d8 <+88>: eor x17, x17, x16
//执行x17:IMP
0x1810aa0dc <+92>: br x17
/* CheckMiss */
//如果此时 x9 == 0 则执行_objc_msgSend_uncached
0x1810aa0e0 <+96>: cbz x9, 0x1810aa3c0 ; _objc_msgSend_uncached
//对比对应出的bucket_t 是否等于 buckets首地址
0x1810aa0e4 <+100>: cmp x12, x10
//如果等于则跳转0x1810aa0f4 -> _objc_msgSend_uncached
0x1810aa0e8 <+104>: b.eq 0x1810aa0f4 ; <+116>
//不相等则 向前查找
0x1810aa0ec <+108>: ldp x17, x9, [x12, #-0x10]!
//跳转0x1810aa0d0
0x1810aa0f0 <+112>: b 0x1810aa0d0 ; <+80>
//跳转_objc_msgSend_uncached
0x1810aa0f4 <+116>: b 0x1810aa3c0 ; _objc_msgSend_uncached
//msgSeng刚开始的第二行判断,这里是nil或者tagged
0x1810aa0f8 <+120>: b.eq 0x1810aa130 ; <+176>
0x1810aa0fc <+124>: adrp x10, 294986
0x1810aa100 <+128>: add x10, x10, #0x340 ; =0x340
0x1810aa104 <+132>: lsr x11, x0, #60
0x1810aa108 <+136>: ldr x16, [x10, x11, lsl #3]
0x1810aa10c <+140>: adrp x10, 294986
0x1810aa110 <+144>: add x10, x10, #0x2a8 ; =0x2a8
0x1810aa114 <+148>: cmp x10, x16
0x1810aa118 <+152>: b.ne 0x1810aa090 ; <+16>
// ext tagged
0x1810aa11c <+156>: adrp x10, 294986
0x1810aa120 <+160>: add x10, x10, #0x3c0 ; =0x3c0
0x1810aa124 <+164>: ubfx x11, x0, #52, #8
0x1810aa128 <+168>: ldr x16, [x10, x11, lsl #3]
0x1810aa12c <+172>: b 0x1810aa090 ; <+16>
0x1810aa130 <+176>: mov x1, #0x0
0x1810aa134 <+180>: movi d0, #0000000000000000
0x1810aa138 <+184>: movi d1, #0000000000000000
0x1810aa13c <+188>: movi d2, #0000000000000000
0x1810aa140 <+192>: movi d3, #0000000000000000
0x1810aa144 <+196>: ret
0x1810aa148 <+200>: nop
0x1810aa14c <+204>: nop
0x1810aa150 <+208>: nop
0x1810aa154 <+212>: nop
0x1810aa158 <+216>: nop
0x1810aa15c <+220>: nop
这里没有分割开来 ,主要是为那些想连贯读下来的小伙伴分析!
整个objc_msgsend
可分为四个阶段:
①. 准备工作,包括获取类对象
,mask_t
,cash_hash(查找索引)
,buckets
等
②. 获取cash_hash
所在buckets
的bucket
,并向前遍历查找,这里有3中情况:
- 1. 如果找到则调用
IMP
。
- 2. 如果找到的
bucket.key == 0
,即bucket
为空桶,则调用 objc_msgSend_uncached
- 3. 如果找到第一个
bucket
,则跳转到3。
- ③. 从`buckets`的最后一个向前查找:
- 1. 如果找到则调用
IMP
。
- 2. 如果找到的
bucket.key == 0
,即bucket
为空桶,则调用 objc_msgSend_uncached
。
④. 调用objc_msgSend_uncached
。
2.2 伪代码
由于汇编看起来不是很直观,可直接参考下面的伪代码,同时这部分伪代码没有处理关于参数和返回值的逻辑。
void discover_msgSend(id self,SEL sel1){
if (self == nil) {
return;
}
SEL sel = sel1;
Class clsHasMask = *(Class *)self;
Class cls = (Class)((uintptr_t)clsHasMask & 0xffffffff8);
struct cache_t * cache = (struct cache_t *)((__bridge void *)cls + 0x10);
struct bucket_t *_buckets = (struct bucket_t *)((uintptr_t)cache->_buckets & 0xffffffffffff);
uintptr_t mask = (uintptr_t)cache->_buckets >> 48;
uintptr_t cache_hash = (uintptr_t)sel & mask;
struct bucket_t* bucket = (_buckets + cache_hash*1);//cache_hash*16
/* CacheLookup */
while ((*bucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*bucket)._key == 0) {
goto _objc_msgSend_uncached;
}
/* 查找到第一个元素了 */
if (bucket == _buckets) {
goto second_loop;
}
bucket = (bucket-1);//[x12, #-0x10]
}
if(((*bucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*bucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
goto _objc_msgSend_uncached;
}
return;
second_loop:
{
int64_t offset = ((uintptr_t)cache->_buckets >> 44);
struct bucket_t* lastBucket = (_buckets + offset/16);
/* CacheLookup */
while ((*lastBucket)._key != (uintptr_t)sel) {
/* CheckMiss */
if ((*lastBucket)._key == 0) {
goto _objc_msgSend_uncached;
}
lastBucket = (lastBucket-1);//[x12, #-0x10]
}
if(((*lastBucket)._key) == (uintptr_t)sel){
IMP x16 = ((uintptr_t)(*lastBucket)._imp) ^ (uintptr_t)cls;
x16();
}else{
/* JumpMiss */
goto _objc_msgSend_uncached;
}
return;
}
_objc_msgSend_uncached:
{
printf("\n _objc_msgSend_uncached -> lookUpImpOrForward");
return;
}
}
通过上面的流程我们发现,objc_msgSend
会有两次的查找行为,这又是为什么呢?我们可以通过runtime
的源码中找到答案。
Clone scanning loop to miss instead of hang when cache is corrupt.