1. Runtime

1.1 Runtime介绍

Runtime称之为运行时,它与编译时的区别:

  • 编译时:源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段
  • 运行时:代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段

1.2 Runtime的两个版本

  • Legacy版本:早期版本

◦ 对应Objective-C 1.0编程接⼝
◦ ⽤于32位Mac OS X平台上

  • Modern版本:现⾏版本

◦ 对应Objective-C 2.0编程接⼝
◦ 用于iPhone程序和Mac OS X v10.5及以后系统中的64位程序

1.3 使用Runtime的三种方式

  • 通过Objective-C代码,例如:[person sayNB]
  • 通过Foundation框架的NSObject类定义的方法,例如:isKindOfClass
  • 通过Runtime API,例如:class_getInstanceSize

image.png

  • Compiler:编译器
  • Runtime System LibararyRuntime的底层系统库

官方文档:Objective-C Runtime Programming Guide

2. 方法本质

2.1 方法底层的实现

main函数中,写入以下代码:

  1. LGPerson *person = [LGPerson alloc];
  2. [person sayNB];
  3. [person say:@"NB"];

生成cpp文件,底层代码如下:

  1. LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
  2. ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
  3. ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_d8842a_mi_3);

方法的本质:objc_msgSend消息发送

objc_msgSend的参数:

  • 消息接收者
  • 消息主体(SEL + 参数)

2.2 objc_msgSend

Build Setting中,将Enable Strict Checking of obc_msgSend Calls设置为NO
image.png

导入头文件:

  1. #import <objc/message.h>

main函数中,写入以下代码:

  1. @interface LGPerson : NSObject
  2. - (void)sayNB;
  3. @end
  4. @implementation LGPerson
  5. - (void)sayNB{
  6. NSLog(@"666");
  7. }
  8. @end
  9. int main(int argc, const char * argv[]) {
  10. @autoreleasepool {
  11. LGPerson *person = [LGPerson alloc];
  12. ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
  13. }
  14. return 0;
  15. }
  16. -------------------------
  17. //输出结果:
  18. 666
  • 和调用OC方法的执行结果相同
  • sel_registerName@selector()的底层实现

2.2 objc_msgSendSuper

子类调用父类方法时,可使用objc_msgSendSuper,向父类发送消息

在objc源码中,找到objc_msgSendSuper的定义

  1. OBJC_EXPORT void
  2. objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
  3. OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
  • 传入objc_super类型的结构体指针

找到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. __unsafe_unretained _Nonnull Class super_class;
  6. /* super_class is the first class to search */
  7. };
  • receiver:消息接收者
  • super_class:传入第一查找的类,如果找不到,继续向所属父类一层一层的查找

在项目中调用objc_msgSendSuper

定义LGTeacher,继承于LGPerson

  1. @interface LGTeacher : LGPerson
  2. - (void)sayNB;
  3. @end
  4. @implementation LGTeacher
  5. @end
  • 定义sayNB方法,但不实现

main函数中,写入以下代码:

  1. struct lg_objc_super {
  2. __unsafe_unretained _Nonnull id receiver;
  3. __unsafe_unretained _Nonnull Class super_class;
  4. };
  5. int main(int argc, const char * argv[]) {
  6. @autoreleasepool {
  7. LGTeacher *teacher = [LGTeacher alloc];
  8. struct lg_objc_super lgSuper;
  9. lgSuper.receiver=teacher;
  10. lgSuper.super_class=[LGPerson class];
  11. objc_msgSendSuper(&lgSuper, @selector(sayNB));
  12. }
  13. return 0;
  14. }
  15. -------------------------
  16. //输出结果:
  17. 666
  • 按照objc_super相同结构,定义lg_objc_super结构体
  • 我们已经知道LGTeacher中没有sayNB方法的实现,所以super_class直接传入LGPerson的类对象
  • 如果super_class传入LGTeacher的类对象,打印结果相同。区别在于需要向父类多查找一层

3. objc_msgSend汇编代码

objc4-818.2源码中,不同系统架构的汇编指令都有差异,我们只针对最常用的arm64架构下的汇编代码进行探索
image.png

3.1 objc_msgSend

  1. ENTRY _objc_msgSend
  2. UNWIND _objc_msgSend, NoFrame
  3. cmp p0, #0 // nil check and tagged pointer check
  4. #if SUPPORT_TAGGED_POINTERS
  5. b.le LNilOrTagged // (MSB tagged pointer looks negative)
  6. #else
  7. b.eq LReturnZero
  8. #endif
  9. ldr p13, [x0] // p13 = isa
  10. GetClassFromIsa_p16 p13, 1, x0 // p16 = class
  • ENTRY _objc_msgSend:入口
  • p0寄存器,存储消息接收者
  • cmp p0, #0:消息接收者和#0比较

◦ 汇编代码中,没有nil,只有01
◦ 这里的#0比较,可以理解消息接收者为nil

  • SUPPORT_TAGGED_POINTERS:真机arm64架构,SUPPORT_TAGGED_POINTERS定义为1
  • b.le LNilOrTagged:小于等于0,进入LNilOrTagged流程
  • b.eq LReturnZero:等于0,进入LReturnZero流程
  • 否则,消息接收者存在,继续执行代码

ldr p13, [x0]:将x0寄存器取地址,赋值p13寄存器
p13:存储消息接收者的isa

  • 进入GetClassFromIsa_p16流程,传入isa1、消息接收者

3.2 LNilOrTagged

  1. LNilOrTagged:
  2. b.eq LReturnZero // nil check
  3. GetTaggedClass
  4. b LGetIsaDone
  • b.eq:等于0,进入LReturnZero流程
  • 否则,小于0,继续执行代码
  • 进入GetTaggedClass流程
  • 进入LGetIsaDone流程

3.3 LReturnZero

  1. LReturnZero:
  2. // x0 is already zero
  3. mov x1, #0
  4. movi d0, #0
  5. movi d1, #0
  6. movi d2, #0
  7. movi d3, #0
  8. ret
  • 返回nil

3.4 GetClassFromIsa_p16

  1. .macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
  2. #if SUPPORT_INDEXED_ISA
  3. // Indexed isa
  4. mov p16, \src // optimistically set dst = src
  5. tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
  6. // isa in p16 is indexed
  7. adrp x10, _objc_indexed_classes@PAGE
  8. add x10, x10, _objc_indexed_classes@PAGEOFF
  9. ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
  10. ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
  11. 1:
  12. #elif __LP64__
  13. .if \needs_auth == 0 // _cache_getImp takes an authed class already
  14. mov p16, \src
  15. .else
  16. // 64-bit packed isa
  17. ExtractISA p16, \src, \auth_address
  18. .endif
  19. #else
  20. // 32-bit raw isa
  21. mov p16, \src
  22. #endif
  23. .endmacro
  • 入参:

srcisa
needs_auth1
auth_address:消息接收者

  • 当前不符合SUPPORT_INDEXED_ISA条件,跳过
  • 符合__LP64__条件,不满足needs_auth等于0的条件,进入else分支
  • ExtractISA p16, \src, \auth_address:进入ExtractISA流程

◦ 传入p16寄存器,isa,消息接收者
p16寄存器的值,存储的是什么不重要,因为传入到ExtractISA中会被赋值

3.5 ExtractISA

  1. .macro ExtractISA
  2. and $0, $1, #ISA_MASK
  3. .endmacro
  • $0p16寄存器
  • $1isa
  • and $0, $1, #ISA_MASK:将isa & ISA_MASK的结果,存储到p16寄存器

p16:存储类对象地址

3.6 LGetIsaDone

  1. LGetIsaDone:
  2. // calls imp or objc_msgSend_uncached
  3. CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
  • objc_msgSend的后续流程

◦ 消息接收者存在,并且得到类对象,继续LGetIsaDone流程

  • 进入CacheLookup缓存查找流程,也就是所谓的sel-imp快速查找流程
  • 内部会调用impobjc_msgSend_uncached

3. 流程图

image.png


为什么要获取类对象?

  • 在快速查找流程中,查找的cache存储在类对象中,所以必须拿到类对象才能进行后面的流程

总结

Runtime介绍:

  • Runtime称之为运行时
  • 与编译时的区别:

◦ 编译时:源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段
◦ 运行时:代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段

Runtime的两个版本:

  • Legacy版本:早期版本

◦ 对应Objective-C 1.0编程接⼝
◦ ⽤于32位Mac OS X平台上

  • Modern版本:现⾏版本

◦ 对应Objective-C 2.0编程接⼝
◦ 用于iPhone程序和Mac OS X v10.5及以后系统中的64位程序

使用Runtime的三种方式:

  • 通过Objective-C代码,例如:[person sayNB]
  • 通过Foundation框架的NSObject类定义的方法,例如:isKindOfClass
  • 通过Runtime API,例如:class_getInstanceSize

方法本质:

  • 方法的本质:objc_msgSend消息发送
  • objc_msgSend的参数:

◦ 消息接收者
◦ 消息主体(SEL + 参数)

  • sel_registerName@selector()的底层实现
  • objc_msgSendSuper:向父类发送消息

objc_msgSend汇编代码:

  • ENTRY _objc_msgSend:汇编代码的入口
  • 消息接收者receiver不存在,返回nil
  • 否则,receiver存在:

◦ 获取isa
isa & ISA_MASK,得到类对象
◦ 成功得到类对象,进入CacheLookup缓存查找流程,也就是所谓的sel-imp快速查找流程