OC消息转发(一)— objc_msgSend探索
前言
该系列我们来探究一下OC
的消息发送和转发机制,本文我们就来对objc_msgSend
做一下初步探索,明白方法调用是如何快速寻找到方法的。以后我们会探索到慢速寻找方法以及找不到方法是如何进行消息转发的。
runtime
简介
要探索objc_msgSend
,我们首先要了解runtime
。runtime
是C
、C++
、汇编混合写成的一套为Objective-C
提供运行时功能的API
。也是因为runtime
,Object-C
才被成为动态语言。
runtime
的版本
runtime
的版本分为两个版本modern
和legacy
(官方文档),我们现在使用的Objective-C 2.0
版本就是modern
版本,只能适用于iOS
和64 bit OS X 10.5
版本及更高版本;legacy
则适用于其他版本和32 bit OS X
。modern
和legacy
最大的区别就是如果更改类中实例变量的布局,legacy
需要重新编译他的子类,modern
版本则不需要。
runtime
的使用
runtime
的使用大致可分为三种使用方法。
-
Objective-C
code:@selector()
等; -
NSObject
的方法:NSSelectorFromString()
等; -
runtime
的api
:sel_registerName()
等;
编译时和运行时
编译时:即编译器对语言的编译阶段,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味着程序就可以成功运行。
运行时:即程序通过了编译这一关之后编译好的代码被装载到内存中跑起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码的简单扫描分析,此时若出错程序会崩溃。这个阶段也是runtime
起作用的阶段。
objc_msgSend
探索
一、clang
生成cpp
文件
创建工程,在main.m
写入以下代码:
void run(){
NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//创建LGPerson类和对象方法sayNB
LGPerson *person = [LGPerson alloc];
[person sayNB];
run();
}
return 0;
}
打开终端进入main.m
文件目录下,执行以下命令:
clang -rewrite-objc main.m -o main.cpp
在此文件夹下会生成一个main.cpp
文件,打开文件滑动到底部可以看到如下代码:
void run(){
NSLog((NSString *)&__NSConstantStringImpl__var_folders_85_h8yymn657hq3vfgnz_xwbtjc0000gp_T_main_26fe1b_mi_0,__func__);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
run();
}
return 0;
}
从代码中可以看出,调用alloc
和sayNB
两个方法被转换成了objc_msgSend
发送消息((void (*)(id, SEL))(void *)
是类型强转),而我写的一个run()
函数则是直接调用,不是通过objc_msgSend
进行消息发送,由此可以看出只有Objective-C
的方法是通过runtime
转换为消息发送的。
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
objc_msgSend
的两个参数id
和sel
代表消息接收者和方法唯一标识。
二、断点看汇编
在sayNB
处打断点,如图:
进入断点,然后菜单 Debug -> Debug Workflow -> Always Show Disassembly
,显示汇编如下:
可以看到objc_msgSend
,然后按着control
+↓
进入objc_msgSend
详情,如下:
可已看出objc_msgSend
是在libobjc
里边,接下来我们去找源码看看objc_msgSend
是如何快速进行方法查找的。
三、objc_msgSend
汇编源码
objc_msgSend
源码是用汇编写的,全局搜索objc_msgSend
找到汇编(文件表示上为s
)arm64
文件,ENTRY _objc_msgSend
是开始如下:
objc_msgSend
汇编源码如下:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
可以看出先进行了nil
和tagged pointer
的检测,SUPPORT_TAGGED_POINTERS
在arm64
下为1,ldr p13, [x0]
把在[x0]
位置的isa
存入p13
中,GetClassFromIsa_p16 p13
通过isa
获取class
,GetClassFromIsa_p16
详情如下:
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
isa指针详解文章中SUPPORT_INDEXED_ISA
在iOS设备上是0,那么进入and p16, $0, #ISA_MASK
中,也即是通过掩码ISA_MASK
和isa
获取类信息。
接下来全局搜索CacheLookup
,找到带有.macro
的宏定义,是CacheLookup
详情。如下:
.macro CacheLookup
// p1 = SEL, p16 = class
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
参考类的结构分析,分析上方汇编代码:
注意:
p
、w
、x
的区别
p16
代表指针;w16
代表32位下的值,4字节;x16
代表64位下的值,8字节;
-
ldp p10, p11, [x16, #CACHE]
:全局搜索define #CACHE
会发现#CACHE
是16,通过GetClassFromIsa_p16
可以知道x16
代表class
,对象结构里内存平移16位(isa
和superclass
)可以得到cache
,cache
又包含了_buckets
、_mask
和_occupied
。这句汇编的意思就是把_buckets
存入到p10
,把_mask
和_occupied
存入到p11
,又因为是小端模式,p11 = occupied|mask
。 -
and w12, w1, w11
:这里用w
是因为代表8字节只取4字节,即w11=mask
、w1
是sel
转换之后的key
,w12
存储的是key&mask
即方法在哈希表的索引值。 -
add p12, p10, p12, LSL #(1+PTRSHIFT)
:p10
是buckets
的首地址,而bucket_t
结构体占用16字节,所以buckets
的首地址加上索引向左偏移(1+PTRSHIFT)
字节得到的值就是函数方法在缓存中的地址。因此p12
就是函数方法对应的bucket
地址。 -
ldp p17, p9, [x12]
:将bucket
存放在p17
和p9
中,p17
装imp
,p9
里装sel
。 -
1: cmp p9, p1
:比较取出来的sel
和p1
是否相等,b.ne 2f
不相等进入2:CheckMiss $0
缓存未命中;相等则是CacheHit $0
缓存命中。CacheHit
详情如下:
// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
CacheHit
就是找到了imp
,那么直接调用TailCallCachedImp
就完成了查找。
-
cmp p12, p10
:比较p12
和p10
是否相等,相等的话说明进入3f:add p12, p12, w11, UXTW #(1+PTRSHIFT)
,索引值即为mask
;不相等则重新赋值p9
,循环进入1f
。下方de就是进入到循环查找imp
的循环中了。 -
JumpMiss $0
:跳转到JumpMiss
。如下:
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
进入NORMAL
判断中,调用__objc_msgSend_uncached
。如下:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
然后调用MethodTableLookup
,如下:
.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
最后是调用了__class_lookupMethodAndLoadCache3
方法,bl
是跳转方法,该方法还带有双下划线,并且搜不到方法的具体实现,可以得出该方法不再是汇编方法,应该是跳转到了C
或者C++
的方法。
到此我们就把objc_msgSend
汇编快速查找方法的探索完了,那为什么要用汇编语言查找方法呢?大概是有两个原因:
1、这个过程需要的是速度,汇编更容易被计算机识别,速度更快。
2、因为方法都会有传参和返回参数,而且是不确定的,相对于C
或者C++
是很难实现这些的,但是汇编是可以的。
总结
1、Objective-C
调用方法是一个通过objc_msgSend
发送消息进行查找方法的实现imp
的。
2、objc_msgSend
查找方法首先是汇编语言查找,这是一个快速的过程。还有一个是慢速查找的过程。