如何分析底层源码
1. 分析方法
首先使用 clang 将 OC 代码转化为 C++ 源码, 转化代码举例:
// 转化的代码
Person *p = [Person alloc];
// clang 指令
clang -rewrite-objc main.m -o main.cpp
// 关于 clang 指令, 比如转化的文件里包含 UIKit 相关代码, 需要加上引用相关的库。具体方法这里就不做过多介绍了, 可以自行了解一下
复制代码
转化的结果:
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
复制代码
(Person *(*)(id, SEL)
: 为方法的签名, 其中 (id,SEL)是方法的两个参数(void *)objc_msgSend
: objc_msgSend 方法,接下来会重点探索(id)objc_getClass("Person")
: objc_msgSend 方法的第一个参数,表示接收消息的对象sel_registerName("alloc")
: 第二个参数,表示第一个参数对象将要执行的方法的符号
需要说明的一点是,对于 C语言函数 经过 clang 重新编译后不会转变为 C++ 格式的代码,而是保持本身的源代码
2. objc_msgSend 方法说明
objc_msgSend(id _Nullable self, SEL_Nonnull op, ...)
复制代码
方法说明:
- 第一参数:消息接收者,一个用来接收消息的类的实例指针
- 第二参数:方法编号,需要处理的消息的方法编号
- 第三参数:不定参数,参数二代表的方法调用需要的参数列表
objc_msgSend 使用分析
1. 首先关闭 xcode 相关配置
2. 测试类
- Person
- Student
3. objc_msgSend 发送消息
objc_msgSend(id _Nullable, SEL _Nonnull op, ...);
复制代码
对象方法:
Student *s = [Student alloc];
// 1. 调用自己的对象方法
objc_msgSend(s, sel_registerName("sayCode"));
结果:-[Student sayCode]
// 2. 调用父类的对象方法
objc_msgSend(s, sel_registerName("sayHello"));
结果:-[Person sayHello]
// 3. 调用自己的对象方法,带参数
objc_msgSend(s, sel_registerName("sayCode"),@"one");
结果:-[Student sayCode]-one
复制代码
类方法:
// 调用父类的方法
objc_msgSend(objc_getClass("Student"), sel_registerName("sayNB"));
结果:-[Person sayNB]
复制代码
4. objc_msgSendSuper 发送消息
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...);
// 参数说明
// 1. super:消息接收者,objc_super对象
// 2. SEL:要处理的消息的方法编号
// 3. 参数列表
复制代码
对象方法:
// 使用对象 s 调用父类的对象方法
// 1.首先创建 objc_super 变量
struct objc_super mySuper;
// 2.给创建好的变量指定消息接收者
mySuper.receiver = s;
// 3.指定父类
mySuper.super_class = [Person class];
// 4.调用方法
objc_msgSendSuper(&mySuper, sel_registerName("sayNB"));
结果: -[Person sayNB]
复制代码
类方法:
// 使用 Student 类去调用 Person 类的类方法
// 1.首先创建 objc_super 变量
struct objc_super mySuper;
// 2.给变量指定消息接收者
mySuper.receiver = [s class];
// 3.指定元类(因为父类的类方法在元类中存储)
mySuper.super_class = class_getSuperClass(object_getClass([s class]));
// 4.调用方法 (注意:这里的方法不是上面的对象方法,是个类方法)
objc_msgSendSuper(&mySuper, sel_registerName("sayNB"));
结果:+[Person sayNB]
// 注意:这里在指定元类的时候如果指定的是 Student 的父类的话,结果也是一样的,但是流程是有区别的。区别在于去过指定的是父类,流程首先会去查找父类中是否有方法的实现,父类中没有找到,然后根据 isa 的指向又会去元类中查找,找到实现后进行调用,所以最后的结果是一样的。
复制代码
objc_msgSend 流程分析
首先要说一下消息的查找流程相关内容: 方法的查找流程分为两部分
- 快速查找流程, 也就是 objc_msgSend 的实现流程, objc_msgSend 是使用汇编实现的, 这也是为什么快的原因之一。加入在 objc_msgSend 汇编结束以后还没有查找到相关方法, 就会使用慢速流程
- 慢速查找流程, 是在汇编流程一直持续到最后还没有查找到方法实现后, 会调用一个
_class_lookupMethodAndLoadCache3
方法然后转到慢速流程.接下来主要看一下 objc_msgSend 快速流程的汇编实现 :
1. 拿到 isa
首先在开源代码中全局搜索 objc_msgSend
, 会出现很多个文件中都有实现, 这里我们使用的是 arm64 环境, 所以只需要看 objc-msg-arm64.s
文件下的实现就可以了:
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************/
// 在这里对于一些非关键的代码做了一些删减
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
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
复制代码
x0
就是首地址, 然后 []
为取值取号, 在这里的意思是将 x0
地址的值取出来交给 p13
。至于首地址, 就是 isa
存储的地方。然后 GetClassFromIsa_p16
是一个宏, 接下来看看他对获取到的 isa
做了什么.
2. 通过 isa 获取类
// macro中文就是 '宏'
.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 不就是 & 吗
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
复制代码
首先关于判断 SUPPORT_INDEXED_ISA
是在 watchOS
中使用的, 所以不需要管。重点在 and p16, $0, #ISA_MASK
这一行中, $0
是从外部传过来的值(也就是我们前面获取到的 isa), 所以这里的意思是用 #ISA_MASK & isa
, 最后得到的就是类。所以这个宏定义的主要作用就是 根据拿到的 isa 得到相关的类, 然后存储到 p16 当中。
接下来再看拿到后面的代码:
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
复制代码
CacheLookup
也是一个宏定义
3. 查找缓存
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
* Locate the implementation for a selector in a class method cache.
* Takes:
* x1 = selector
* x16 = class to be searched
* Kills:
* x9,x10,x11,x12, x17
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
********************************************************************/
.macro CacheLookup
// p1 = SEL, p16 = isa
// x16表示偏移16个字节, 找到 cache
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))
// 递归, 指针偏移, 找到结果就返回 {imp, sel} = *bucket
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 将 p9 和 p1 进行比较
1: cmp p9, p1 // if (bucket->sel != _cmd)
// b 为跳转, ne 就是 notEqual, 如果不相等就跳转到 2
b.ne 2f // scan more
// 如果相等就表示 缓存命中, 执行 CacheHti 缓存命中逻辑
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
// 不过不相等在这里又继续进行了 递归 操作, 跟上面的 ldp 相似, 然后回到 1 再进行判断
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
// 在这里比较 p12和p10, 如果当 2 个值相等的话说明已经递归了一遍了就没必要再递归下去了, 就调到 3
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
复制代码
- 首先在类中取出
cache
下面的数据, 然后分别存储到p10
和p11
当中, (p10 存储 buckets, p11 存储 occupied 和 mask) - 然后进行递归查找
buckets
下的缓存数据, 如果找到的话就执行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
复制代码
在实现中可以看到, 如果找到了缓存的 imp
, 就会调用 TailCallCachedImp
直接进行方法调用。
- 如果没有找到的话, 最终会因为
CheckMiss
进入到慢速查找流程
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL // 会来到这里
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
复制代码
CheckMiss
后会调用 __objc_msgSend_uncached
, 然后来看看这个方法
4. 查找失败
// __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
// 由于代码太多做了一些删减, 只保留了关键的地方
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
.endmacro
复制代码
最终通过 bl
跳转到了 __class_lookupMethodAndLoadCache3
, 到这里就结束了吗? 继续尝试全局搜索该方法发现没有找到, 然后去掉一个前面的下划线发现了相关方法:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码
这样就转入了慢速查找流程中去, 慢速查找流程会在后面进行继续研究。