runtime 源码学习笔记
Objc 中类和对象的本质
Objc 中任何对象都可以称之为 id
类型,那么看下在 objc.h
对 id
类型的定义:
1 | /// A pointer to an instance of a class. |
注释中的描述是 一个指向类的实例的指针,那么是不是意味一个类的实例即对象就是一个 objc_object
结构体呢?再看源码:
1 | /// An opaque type that represents an Objective-C class. |
通过阅读 runtime 的源码可以得出以下结构:
注:
class_ro_t
中存放的是编译时可以确定的属性、方法和协议等
- Objc 中的对象是一个
objc_object
结构体,结构体中第一个变量是isa_t
,存放着该对象所属类的信息; - 类是一个
objc_class
结构体,继承自objc_object
,所以类也是一个对象,另外还有两个变量进行方法缓存和数据存放,比如变量、方法(实例方法,以-
开头的方法)和所遵守的协议。 - 类的
isa
中存放的类是元类(meta-class),类是一个对象,对象的类型就是元类,元类存放着类的方法(类方法,以+
开头的方法)。 - 元类也是类,所以元类也是对象。元类
isa
变量中存放的类是根元类(一般是NSObject
)。 - 根元类
isa
的类信息指向其自身 - 类和元类都是单例
那么 isa
到底存放了什么?
1 | union isa_t { |
objc_object
中的 isa
是通过位域+掩码来优化存储。isa
共8个字节64位,其中存放类信息的 shiftcls
占去了 33 位,存放引用计数的 extra_rc
占用了 19 位,验证对象是否初始化完整的 magic
占用 6 位,其他都是只占用了 1 位。
再看下 class_data_bits_t
中 class_rw_t
的结构:
1 | struct class_rw_t { |
const class_ro_t *ro
中存放的是类在编译时期就确定的方法、协议等,分类及运行时添加的属性、方法都在 class_rw_t
存储。但是 class_data_bits_t
中的 data(class_rw_t
) 一开始是 const class_ro_t *ro;
,在初始运行时环境中的 realizeClass
函数中,重新生成 class_rw_t
data。并且通过 attachCategories
函数,将分类的方法、协议等添加到 class_rw_t
中。至此,class_rw_t
存放了一个类所需要的完整信息。
objc_msgSend
调用方法(函数)是语言经常使用的功能,在 Objective-C 中专业一点的叫法是 传递消息(pass a message)。Objective-C 的方法调用都是 动态绑定 ,而C语言中函数调用方式是 静态绑定 ( static binding ),也就是说,在编译时期就能决定和知道在运行时所调用的函数。
以下面代码为例:
1 | void sayHello(){ |
基本上,上面的代码在编译的时候编译器就知道 sayHello 和 sayGoodBye 两个函数的存在,函数地址是硬编码在指令之中的。但是如果换一种写法:
1 | void sayHello(){ |
这就得使用 动态绑定 ,待调用的函数地址需要到运行时才能读取出来。
在 Objective-C 中,对某一个对象传递消息,会用动态绑定机制来决定到底是调用哪个方法。而Objective-C是 C 的超集,底层是由 C语言实现,但是对象接收消息后会调用哪个方法都是在运行期决定。
1 | id object = [list objectAtIndex:1]; |
在这行代码中, list 称为 接收者, objectAtIndex 叫做 选择器, 选择器和参数合起来称为消息。当编译器看到这行代码的时候,会换成标准的C语言函数调用:
1 | void objc_msgSend(id self, SEL cmd, ...); |
objc_msgSend 这个函数可以接收两个及两个以上的参数,第一个参数是接收者,第二个参数是选择器,后面的参数是保持顺序的原来消息传递的参数,objc_msgSend 会依据接收者和选择器来决定调用哪个方法,首先在接收者的方法列表缓存(objc_class/cache_t cache
)中寻找,如果方法缓存中找不到就类信息存储的方法列表(objc_class/class_data_bits_t/class_rw_t/methods
)中找,如果再找不到就会沿着继承体系(objc_class/superclass/class_data_bits_t/class_rw_t/methods
)去向上一层一层的寻找,如果仍旧找不到就会执行 消息转发(message forwarding) 。
当消息第一次传递之后,objc_msgSend 会将匹配结果进行缓存(objc_class/cache_t cache
),下次会直接调用方法。消息传递除了objc_msgSend之外在特殊情况下还会有其他的方法来处理:
- objc_msgSend_stret 如果待发送的消息返回一个结构体,就会调用这个函数来处理。
- objc_msgSend_fpret 如果消息返回的是浮点数,就会调用这个函数进行处理。
- objc_msgSendSuper 如果要传递消息给父类。
总结:
- 消息由 接收者、选择器及参数构成,给某对象 发送消息( invoke a message ) 也就相当于在该对象上调用方法。
- 发送给某对象的全部消息都要有动态消息派发系统( dynamic message dispatch system ) 来处理。
Objective-C 的消息转发机制与动态添加方法
刚才说了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。这里就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )。
开发可能经常会遇到这种情况:
1 | 2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50' |
这个异常信息是由 NSObject 的 doesNotRecognizeSelector: 方法抛出来的,本来是给 AutoDictionary 的一个实例对象发送消息,但是该对象并没有 setDate: 方法,所以消息转发给了 NSObject ,最后抛出异常。
先看下消息处理机制流程图:
消息转发分为两阶段三步,第一阶段先看接受消息的对象能不能自己处理这个无法解读的消息,这一步可以动态的添加方法去解读接受这个消息;第二阶段是先看看对象自己不能处理这个消息,能不能交给其他对象来进行处理,在这一步如果仍然无法解读消息,那么就会走最后一步:把和消息有关的所有细节封装到一个 NSInvocation 中,再询问一次对象是否能解决。
看下三个方法:
1 | // 询问对象是否自己处理,是返回YES,一般会在这个方法里面动态添加方法 |
来一段代码示例:
新建一个 AutoDictionary 类,添加一个 NSDate 类型的 date 属性,在实现文件里面用 @dynamic date; 禁止自动生成存取方法,这样当代码中给 AutoDictionary 实例对象的 date属性赋值时就会出现消息无法解读的现象。
.h 文件:
1 | @interface AutoDictionary : NSObject |
.m 实现文件代码内容:
1 | @interface AutoDictionary() |
测试代码:
1 | AutoDictionary *dict = [AutoDictionary new]; |
method swizzling 与AOP编程
什么是 AOP
: (site: baike.baidu.com),引用百度百科中的解释就是:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能:
日志记录,性能统计,安全控制,事务处理,异常处理等等主要意图:
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
iOS 开发中的 AOP
在 Objective-C 中,类的方法列表会把选择器的名称映射到方法的实现上,这样 动态消息转发系统 就可以以此找到需要调用的方法。这些方法是以函数指针的形式来表示,这种指针叫做 IMP。
如下:
1 | id (*IMP) (id, SEL, ...) |
Objective-C
的 runtime
机制以此提供了获取和交换映射IMP
的的接口:
1 | // 获取方法 |
我们可以通过上面两个方法来进行选择器和所映射的IMP
进行交换:
来,直接上代码示例,比如我们的要实现功能是在每个控制器的- viewDidLoad
方法里面log一下,一般有三种实现方式:
- 直接修改每个页面的 view controller 代码,简单粗暴;
- 子类化 view controller ,并让我们的 view controller 都继承这些子类;
- 使用
Method Swizzling
进行 hook,以达到AOP
编程的思想
第一种实现的代码是在每个类的里面都这么写:
1 | - (void)viewDidLoad { |
第二种是只在基类里面写。然后所有的控制器都继承这个基类。
最后一种是最佳的解决方案:
1 | @implementation UIViewController (Log) |
注意:
- 为什么使用
+ (void)load
?因为父类、子类和分类的该方法是分别调用,互不影响,而且是在类被加载的时候必定会调用的方法。
那么为什么说“父类、子类和分类的该方法是分别调用”且”类被加载的时候必定会调用的方法“呢?
在 runtime 源码中的 _objc_init() -> load_images() -> call_load_methods()
函数里,可以发现load方法不是走的消息发送,而是直接使用函数地址指针直接调用,且保证了类的load方法在分类之前调用,父类的load方法再子类之前调用。具体逻辑见伪代码片段:
1 | void call_load_methods(void) |
给对象、分类添加实例变量
在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)
关联对象 可以给某个对象关联其他对象并用key来区分其他对象。需要注意的是,存储对象的时候要指明 存储策略,用来维护对象的内存管理语义。存储策略是 objc_AssociationPolicy 枚举定义,以下是存储策略对应的 @property属性:
存储策略类型 | 对应的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | weak |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
OBJC_ASSOCIATION_RETAIN | strong |
OBJC_ASSOCIATION_COPY | copy |
用下面的方法可以管理关联对象:
1 | // 这个方法可以根据指定策略给对象关联对象值 |
注意:
- 定义关联对象时需要指定内存管理语义,用来模拟对象对变量的拥有关系
- 尽量避免使用关联对象,因为如果出现bug不易于问题排查
Associated Object的实现方式
通过源码发现,存储使用的数据结构如上图所示。
1 | void objc_setAssociatedObject(id object, const void *key, |
AssociationsManager
类中有个静态变量AssociationsHashMap *_map
AssociationsHashMap
中以object
为 key,value 是ObjectAssociationMap
ObjectAssociationMap
中 key 就是入参key
,value 就是入参value
后话
做个试验,新建一个 BBObject
类,添加一个属性 bb_name
,头文件如下:
1 | @interface BBObject : NSObject |
那么以下代码会输出什么呢:
1 | NSString *name = @"这是 bb_name"; |
输出的是:2017-12-16 19:55:33.477716+0800 runtime[45876:7116185] 这是 bb_name
。
因为 Objc 中一个完整的对象就是 首地址指向一个类的连续空间,为什么是连续空间?那是因为对象还有自己属性变量的值要存储,这也是为什么没有给 bb_obj
的 bb_name
属性赋值,却打印出 name
值的原因,在 iOS 中,栈的地址是由高到低,堆的地址是由低到高,在这段代码中栈中依次压入了 name
、bb_obj
,而 bb_obj
对象自身的属性是根据自身首地址进行偏移去获取,所以会取到 name
的值。
使用 clang -rewrite-objc BBObject.m
可以把得到重写后的 C++ 文件,在其中也可以看到其中获取属性就是自身地址加偏移量:
1 | static NSString * _I_BBObject_bb_name(BBObject * self, SEL _cmd) { |