编译时与运行时

编译时

编译时顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。实际上只是翻译成某个中间状态的语言。
那编译时就是简单的做一些翻译工作,比如检查代码规范、语法分析之类的过程。

运行时

所谓运行时就是代码跑起来了,被装在到内存中去了,是一个动态过程,而运行时类型检查就是与前面讲的编译时类型检查不一样,不是简单的扫描代码,而是在内存中做了实际操作进行判断。
OC的运行时就是我们所说的RunTime。

Runtime交互的三种方式

image.png

  • Objective-C Code直接调用

比如直接调用方法[self say]、#selector()等。

  • Framework&Serivce

比如NSSelectorFromString、isKindOfClass、isMenberOfClass等方法。

  • RuntimeAPI

比如sel_registerName、class_getInstanceSize等底层方法。

Clang编译OC源代码

环境准备

OC源代码

  1. @interface Person : NSObject
  2. - (void) running;
  3. - (void) swimming;
  4. @end
  5. @implementation Person
  6. - (void) running
  7. {
  8. NSLog(@"running");
  9. }
  10. - (void) swimming
  11. {
  12. NSLog(@"swimming");
  13. }
  14. @end
  15. @interface Student : Person
  16. @end
  17. @implementation Student
  18. @end
  19. int main(int argc, const char * argv[]) {
  20. @autoreleasepool {
  21. Person *person = [Person alloc];
  22. [person running];
  23. objc_msgSend(person, sel_registerName("running"));
  24. Student *student = [Student alloc];
  25. [student swimming];
  26. struct objc_super yjSuper;
  27. yjSuper.receiver = student;
  28. yjSuper.super_class = objc_getClass("Person");
  29. objc_msgSendSuper(&yjSuper, sel_registerName("swimming"));
  30. }
  31. return 0;
  32. }

Clang转换main.cpp

  1. int main(int argc, const char * argv[]) {
  2. /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
  3. Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
  4. ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("running"));
  5. }
  6. return 0;
  7. }

方法调用分解

使用objc_msgSend调用running方法

objc_msgSend(person, sel_registerName(“running”));

使用objc_msgSendSuper调用父类的swimming方法

objc_msgSendSuper(&yjSuper, sel_registerName(“swimming”));
我们看一下objc_msgSendSuper的第一个参数是struct objc_super *结构体指针,源码如下:

  1. struct objc_super {
  2. /// Specifies an instance of a class.
  3. __unsafe_unretained _Nonnull id receiver;
  4. /// Specifies the particular superclass of the instance to message.
  5. #if !defined(__cplusplus) && !__OBJC2__
  6. /* For compatibility with old objc-runtime.h header */
  7. __unsafe_unretained _Nonnull Class class;
  8. #else
  9. __unsafe_unretained _Nonnull Class super_class;
  10. #endif
  11. /* super_class is the first class to search */
  12. };

其中我们用到的两个参数分别是receiver和super_class,分别是消息接受者,和父类对象,消息接受者就是我们对象本身,父类就是Person类对象,由于我们声明的是一个结构体,参数需要是一个指针所以使用&获取结构体地址作为参数传入。

main.cpp分析方法调用过程

  • 通过cpp文件分析我们可以看到alloc方法和running都是通过objc_msgSend方法调用的。
  • objc_getClass()就是runtime的方法,用于获取Person的类对象。
  • sel_registerName()也是runtime的方法,用于获取方法,对应OC的@Selector()、NSSelectorFromString()。
  • 我们通过上述发现,无论是实例方法的调用还是类方法的调用都是通过objc_msgSend方法进行的,只是参数一的消息接收者不同,调用类方法时消息接收者是类对象,调用实例方法时消息接收者时实例对象,参数二就是我们要调用的方法。
  • objc_msgSend会根据参数一在缓存中和方法列表中进行方法查找。
  • 在缓存中查找方法是最快的,所以我们称之为方法快速查找。

Objc_msgSend

介绍

在objc4源码中通过搜索发现objc_msgSend是使用汇编实现的,汇编的主要特征是:

  • 速度快,汇编更容易被机器识别。
  • 方法参数的动态性,汇编调用函数时传入的参数是不确定的,那么消息发送时,直接调用一个函数就可以发送所有消息。

    消息查找机制

    快速查找:cache中查找(缓存查找)。
    慢速查找:methodList中查找(方法列表),和消息转发

    Objc_msgSend快速查找分析

    objc_msgSend调用

    objc_msgSend(person, sel_registerName(“running”));
    传入两个参数,分别是消息接受者和消息的sel。

    objc_msgSend汇编源码

    ```objectivec ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame

    // p0是我们传入的第一个参数:消息接受者 // cmp是比较方法,比较p0是否为nil,如果为nil说明没有消息接受者,直接返回 cmp p0, #0 // nil check and tagged pointer check

    if SUPPORT_TAGGED_POINTERS

    // TagPointer类型 b.le LNilOrTagged // (MSB tagged pointer looks negative)

    else

    // 消息接受者为空返回空 b.eq LReturnZero

    endif

    // p13 是获取消息接受者的首地址,也就是isa ldr p13, [x0] // p13 = isa // GetClassFromIsa_p16 通过isa获取类对象并赋值给p16 GetClassFromIsa_p16 p13, 1, x0 // p16 = class LGetIsaDone: // calls imp or objc_msgSend_uncached // 在cache中查找imp CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

  1. <a name="Ta2XC"></a>
  2. ### 流程图
  3. ![](https://cdn.nlark.com/yuque/0/2021/jpeg/12834404/1634701872641-40ea05cd-4855-43c9-af26-a5c1cd9203f5.jpeg)
  4. <a name="tiRoe"></a>
  5. ### CacheLookup源码
  6. ```objectivec
  7. LLookupStart\Function:
  8. // p1 = SEL, p16 = isa
  9. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
  10. ldr p10, [x16, #CACHE] // p10 = mask|buckets
  11. lsr p11, p10, #48 // p11 = mask
  12. and p10, p10, #0xffffffffffff // p10 = buckets
  13. and w12, w1, w11 // x12 = _cmd & mask
  14. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  15. // 通过isa内存平移16位,获取cache首地址,cache首地址就是maskAndBuckets
  16. ldr p11, [x16, #CACHE] // p11 = mask|buckets
  17. #if CONFIG_USE_PREOPT_CACHES
  18. #if __has_feature(ptrauth_calls)
  19. tbnz p11, #0, LLookupPreopt\Function
  20. and p10, p11, #0x0000ffffffffffff // p10 = buckets
  21. #else
  22. and p10, p11, #0x0000fffffffffffe // p10 = buckets
  23. tbnz p11, #0, LLookupPreopt\Function
  24. #endif
  25. eor p12, p1, p1, LSR #7
  26. and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
  27. #else
  28. // maskAndBuckets是一个共用体,共占8位64字节 高16字节存储着mask值低48字节存储着buckets信息
  29. // 所以此处将mask|buckets & 0x0000ffffffffffff 获取低48字节的信息,也就是获取buckets并赋值给p10
  30. and p10, p11, #0x0000ffffffffffff // p10 = buckets
  31. // 此处_cmd & mask就是缓存插入式hash值的计算方式,catch_hash的原理就是 _cmd & mask
  32. // 所以次数获取的是缓存中的方法hash值,并赋值给p12变量
  33. and p12, p1, p11, LSR #48 // x12 = _cmd & mask
  34. #endif // CONFIG_USE_PREOPT_CACHES
  35. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
  36. ldr p11, [x16, #CACHE] // p11 = mask|buckets
  37. and p10, p11, #~0xf // p10 = buckets
  38. and p11, p11, #0xf // p11 = maskShift
  39. mov p12, #0xffff
  40. lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
  41. and p12, p1, p11 // x12 = _cmd & mask
  42. #else
  43. #error Unsupported cache mask storage for ARM64.
  44. #endif
  45. // 根据hash值索引逻辑计算要获取的bucket
  46. // 我们知道bucket中存储着sel和imp,所以占16字节
  47. // 那么p12就是_cmd & mask就是索引,也就是当前要找的第几位
  48. // (1+PTRSHIFT) == 4
  49. // (_cmd & mask) << (1+PTRSHIFT) 左移4位相当于,乘以2的4次方 也就是乘以16,整好是每个bucket的大小
  50. // 逻辑运算后p13就是我们当前从缓存中找到的bucket
  51. add p13, p10, p12, LSL #(1+PTRSHIFT)
  52. // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
  53. // do {
  54. // 此处是一个do-while循环
  55. // 使用p17和p9记录当前bucket的imp和sel
  56. // 同时bucket--,将bucket向前移动
  57. 1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
  58. // _cmd就是当前要调用方法的方法编号
  59. // 比较缓存中获取到的sel与我们传入的_cmd是否一致,如果一致则调用CacheHit命中方法,结束
  60. cmp p9, p1 // if (sel != _cmd) {
  61. // 如果不一致则调用 3:方法
  62. b.ne 3f // scan more
  63. // } else {
  64. 2: CacheHit \Mode // hit: call or return imp
  65. // }
  66. 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
  67. // 循环结束条件 当获取的bucket地址小于buckets的首地址时,说明已经取超,跳出循环
  68. cmp p13, p10 // } while (bucket >= buckets)
  69. // 跳转 1:方法,比较当前sel与_cmd是否一致
  70. b.hs 1b
  71. // wrap-around:
  72. // p10 = first bucket
  73. // p11 = mask (and maybe other bits on LP64)
  74. // p12 = _cmd & mask
  75. //
  76. // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
  77. // So stop when we circle back to the first probed bucket
  78. // rather than when hitting the first bucket again.
  79. //
  80. // Note that we might probe the initial bucket twice
  81. // when the first probed slot is the last entry.
  82. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
  83. add p13, p10, w11, UXTW #(1+PTRSHIFT)
  84. // p13 = buckets + (mask << 1+PTRSHIFT)
  85. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  86. // 调出循环,则说明当前获取的bucket已经超过了buckets了
  87. // 此时需要将bucket移动到buckets的最后,在重新从后向前查找一遍
  88. add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
  89. // p13 = buckets + (mask << 1+PTRSHIFT)
  90. // see comment about maskZeroBits
  91. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
  92. add p13, p10, p11, LSL #(1+PTRSHIFT)
  93. // p13 = buckets + (mask << 1+PTRSHIFT)
  94. #else
  95. #error Unsupported cache mask storage for ARM64.
  96. #endif
  97. // 标记已经查找到buckets一次了
  98. add p12, p10, p12, LSL #(1+PTRSHIFT)
  99. // p12 = first probed bucket
  100. // do {
  101. // 再次循环查找
  102. 4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
  103. cmp p9, p1 // if (sel == _cmd)
  104. b.eq 2b // goto hit
  105. cmp p9, #0 // } while (sel != 0 &&
  106. // 当在查找到buckets还为找到时,则结束快速查找,说明缓存中没有要调用的方法
  107. ccmp p13, p12, #0, ne // bucket > first_probed)
  108. b.hi 4b
  109. LLookupEnd\Function:
  110. LLookupRecover\Function:
  111. b \MissLabelDynamic

快速查找完整流程图

image.png