iOS探索 -- 消息的查找流程(一)

如何分析底层源码

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, ...)
复制代码

img

方法说明:

  • 第一参数:消息接收者,一个用来接收消息的类的实例指针
  • 第二参数:方法编号,需要处理的消息的方法编号
  • 第三参数:不定参数,参数二代表的方法调用需要的参数列表

objc_msgSend 使用分析

1. 首先关闭 xcode 相关配置

img

2. 测试类

  • Person

img

  • Student

img

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 流程分析

首先要说一下消息的查找流程相关内容: 方法的查找流程分为两部分

  1. 快速查找流程, 也就是 objc_msgSend 的实现流程, objc_msgSend 是使用汇编实现的, 这也是为什么快的原因之一。加入在 objc_msgSend 汇编结束以后还没有查找到相关方法, 就会使用慢速流程
  2. 慢速查找流程, 是在汇编流程一直持续到最后还没有查找到方法实现后, 会调用一个 _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
复制代码
  1. 首先在类中取出 cache 下面的数据, 然后分别存储到 p10p11 当中, (p10 存储 buckets, p11 存储 occupied 和 mask)
  2. 然后进行递归查找 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 直接进行方法调用。

  1. 如果没有找到的话, 最终会因为 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*/);
}
复制代码

这样就转入了慢速查找流程中去, 慢速查找流程会在后面进行继续研究。

猜你喜欢

转载自juejin.im/post/7106916036146331661
今日推荐