Objective-C runtime - 属性与方法
前言
系列第二篇:深入分析OC中的属性与方法的本质。通过上一篇Objective-C runtime - 类和对象,我们了解了objc_class中的 isa 成员变量;在这篇文章中,则会详细介绍objc_class的另一成员变量 bits。
bits
先回忆一下objc_class的结构
bits
用来存储类的属性,方法,协议等信息。它是一个class_data_bits_t
类型
class_data_bits_t
1 | struct class_data_bits_t { |
这个结构体只有一个64bit的成员变量bits
,先来看看这64bit分别存放的什么信息:
is_swift
: 第一个bit,判断类是否是Swift类
has_default_rr
:第二个bit,判断当前类或者父类含有默认的retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
方法
require_raw_isa
:第三个bit, 判断当前类的实例是否需要raw_isa (不是很懂)
data
:第4-48位,存放一个指向class_rw_t结构体的指针,该结构体包含了该类的属性,方法,协议等信息。至于为何只用44bit来存放地址,可以参考Objective-C runtime - 类和对象
class_rw_t 和 class_ro_t
先来看看两个结构体的内部成员变量
1 | struct class_rw_t { |
可以看出,class_rw_t结构体内有一个指向class_ro_t结构体的指针。
每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass
方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。
用两张图来说明这个过程:
类的realizeClass
运行之前:
类的realizeClass
运行之后:
细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容
属性和实例变量
现在知道了属性(property)存放在class_rw_t中,实例变量(ivar)存放在class_ro_t中。下面来看看他们的真实面目。
先看一段代码:
1 | @interface TestOBJ : NSObject { |
这个TestOBJ类有一个实例变量ivarInt
和 一个属性propertyInt
。使用lldb打印出class_ro_t的ivars:
再使用lldb打印出class_ro_t的baseProperties:
顺便也把class_ro_t的baseMethodList打印出来吧:
从上面三张lldb的结果可以看出:TestOBJ类含有2个实例变量,1个属性和2个方法。但是代码里只有一个实例变量一个属性和0个方法。原因就是声明一个属性的同时,会同时生成一个实例变量和该实例变量的存取方法。
以propertyInt
为例:生成了 _propertyInt
实例变量和 - (NSInteger)propertyInt;
以及 - (void)setPropertyInt:(NSInteger)propertyInt
两个存取方法
property_t
class_ro_t中的baseProperties是一个存放property_t结构体的list。property_t结构体用来存放属性的信息
1 | struct property_t { |
该结构体保存了属性的名字和type encoding。
ivar_t
其实到这里你就会发现property_t结构体中存放的信息没啥用,真正有用的还是存放实例变量的结构体ivar_t
1 | struct ivar_t { |
以TestOBJ中的 ivarInt 实例变量为例来说明,先使用lldb打印该实例变量的详细信息:
name
: 实例变量的名字 -> ivarInt
type
: 实例变量的类型,type encoding -> q 表示int类型
size
: 实例变量的内存大小 -> 8 因为NSInteger类型占8个字节
offset
: 实例变量距离对象的首地址的偏移量 -> offset是一个指向int的指针,取出offset指向的int,发现偏移量为8
上图还打印了class_ro_t,里面有2个变量需要注意一下:
instanceStart
: 对象开始存放实例变量的地址的偏移量 -> 8 因为对象的前8字节存放的是isa_t
结构体
instanceSize
: 对象的大小 -> 24 = isa_t + ivarInt + _propertyInt
所以:获取TestOBJ实例a的ivarInt的过程:取出 对象首地址加ivarInt的偏移量 的内存空间的int
类继承关系中的对象的内存空间结构
先看看没有继承关系的类 TestOBJ:
再看看继承TestOBJ的Son类:
1 | @interface Son : TestOBJ |
它的对象既存储了TestOBJ的实例变量,也存放了Son自己的实例变量
使用lldb打印son的class_ro_t:
可以发现Son类的 instanceStart
为24,因为Son的实例的前8字节存放isa,后面16字节存放父类的ivarInt和_propertyInt两个NSInteger型实例变量。Son类的 instanceSize
为32字节,因为他还有自己的一个指向NSString结构体的指针 _sonStr ,这个指针也占8字节。
另外_sonStr 这一个ivar_t的offset是24,也符合分析
注意Son类的class_ro_t在realizeClass
方法完成后就确定下来了
方法
编译结束之后,类的方法就已经存储在class_ro_t里面的baseMethodList变量中了;当运行完runtime的realizeClass
方法之后,会将baseMethodList拷贝到class_rw_t的methods
变量中,并将各种分类的方法拷贝到methods
中。这样类的所有方法就都聚集在class_rw_t的methods变量中了。
使用lldb打印出class_rw_t中的methods:
可以看到这里的methods
和上面打印出来的class_ro_t中的baseMethodList
一模一样。这是因为这个类没有分类,只是简单的将baseMethodList的内容拷贝过去。
method_t
runtime使用method_t
结构体来存放方法:
1 | struct method_t { |
name
: 方法名,类型为SEL
选择子,可以简单地理解成方法的id,字符串类型
types
: 方法的type encoding
imp
: 方法的实现,类型为IMP
。IMP是函数指针,指向方法的实现,也就是指向实现该方法的代码区
底层存储方式
1 | struct class_rw_t { |
class_ro_t里面的method_list_t
和 ivar_list_t
都是 entsize_list_tt
的结构体;class_rw_t里面的 method_array_t
和 property_array_t
都是 list_array_tt
的结构体。来看看这个底层存储方式
entsize_list_tt
1 | template <typename Element, typename List, uint32_t FlagMask> |
一个存储Element的序列,可以使用get(index)
方法获取特定index的Element;它也提供了iterator遍历器,具体实现细节可以查看代码。objc-runtime-new.h
list_array_tt
1 | template <typename Element, typename List> |
简单来说是一个存放entsize_list_tt
的数组。也提供了iterator遍历器。也定义在objc-runtime-new.h
总结
其实这篇文章就是👇两篇参考文章的一个合体。总结来说:objc_class结构体中的bits成员变量存放着类的属性、方法、协议等信息。这一些信息,有的是在编译期间就确定的;另外一些则是在运行runtime的realizeClass
的方法时添加上去的。
那么当runtime将类初始化之后,所有的类所需的信息都存放在了class_rw_t结构体中
fixme
在这里做个更正:在realizeClass
方法中,并不是将class_ro_t
中的属性、方法的list拷贝到class_rw_t
中;而只是通过class_rw_t
来操作class_ro_t
中的各种list。class_rw_t
中的list_array_tt
有指向 class_ro_t
中的 entsize_list_tt
的指针。