Runtime系列文章
Runtime原理探究(一)—— isa的深入体会(苹果对isa的优化)
Runtime原理探究(二)—— Class结构的深入分析
Runtime原理探究(三)—— OC Class的方法缓存cache_t
Runtime原理探究(四)—— 刨根问底消息机制
Runtime原理探究(五)—— super的本质
[Runtime原理探究(六)—— Runtime的应用...待续]-()
[Runtime原理探究(七)—— Runtime的API...待续]-()
Runtime原理探究(八)—— 面试题中的Runtime
我在OC对象的本质(下)—— 详解isa&superclass指针中,有介绍过Class对象的内存结构,如下图
本文就以此为起点,来仔细挖掘一下Class内部都有哪些宝贝。
(一)Class的结构简述
首先我们来看一下objc源码对Class的定义
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable 方法缓存
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 用于获取具体的类信息
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
//......下面的一堆方法/函数代码可以暂时不用关注
};
结构比较清晰
-
Class superclass;
——用于获取父类,也就是元类对象,它也是一个Class类型 -
cache_t cache;
——是方法缓存 -
class_data_bits_t bits;
——用于获取类的具体信息,看到bits
,想必看过我写的Runtime之isa的深入体会(苹果对isa的优化)这篇文章,一定会深有体会。 - 紧接着有一个
class_rw_t *data()
函数,该函数的作用就是获取该类的可读写信息,通过class_data_bits_t
的bits.data()
方法获得,点进该方法看一下
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
可以看到,跟我们之前通过isa获取对象地址操作很像,这里是将类对象里面的class_data_bits_t bits;
和一个FAST_DATA_MASK
进行&
运算取得。返回的是一个指针,类型为class_rw_t *
,查看该类型的源码如下
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods; //⚠️⚠️⚠️方法列表
property_array_t properties; //⚠️⚠️⚠️属性列表
protocol_array_t protocols; //⚠️⚠️⚠️协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
我们知道,OC类的方法、属性、协议都是可以动态添加,也就是可读可写的,从上面的源码中,可以发现确实是有对应的成员来保存方法、属性、协议的信息。而从该结构体的名字class_rw_t
,也暗含了上述的方法、属性、协议信息,是可读可写的。另外,我们知道Class类里面的成员变量是不可以动态添加的,也就是属于只读内容,相应的,可以推断const class_ro_t *ro;
就是指向了该部分内容信息的指针。同样,查看其源码
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance对象占用的内存空间
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;//类名
method_list_t * baseMethodList;//方法列表
protocol_list_t * baseProtocols;//协议列表
const ivar_list_t * ivars;//成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;属性列表
method_list_t *baseMethods() const {
return baseMethodList;
}
};
这个结构体里面,就存放了一些类相关的只读信息
-
uint32_t instanceSize;
——instance对象占用的内存空间 -
const char * name;
——类名 -
const ivar_list_t * ivars;
——类的成员变量列表
以上的发现都符合逻辑,但是发现这里面也有跟类的方法、属性、协议相关的信息
method_list_t * baseMethodList;
、protocol_list_t * baseProtocols;
、property_list_t *baseProperties;
,
这跟我们前面在class_rw_t
中看到的
method_array_t methods;
、property_array_t properties;
、protocol_array_t protocols;
有何关联?有何不同呢?带着这些疑问,继续往下分析。
首先上面的部分用一张图大致总结如下
这个图可以理解成稳定状态下,Class的内部结构。但事实上,在程序启动和初始化过程中,Class并不是这样的结构,我们通过源码可以分析一下。我在 Objective-C之Category的底层实现原理有分析过从objc初始化到category信息加载过程的源码执行路径,这里就不在重复,只作简单表述,有不明白的话可以通过该文章补充。
- 首先在
objc-os.mm
中找到objc的初始化函数void _objc_init(void)
- 继续进入
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
里面的map_images
函数 - 在
map_images
函数里面继续进入map_images_nolock
函数 - 在
map_images_nolock
函数末尾,进入_read_images
函数入口 - 在
_read_images
函数实现里面,可以发现在处理category信息之前,也就是注释// Discover categories
的上面的那部分代码,就是类的初始化处理步骤
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
realizeClass(resolvedFutureClasses[i]);
resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
ts.log("IMAGE TIMES: realize future classes");
进入核心函数realizeClass
static Class realizeClass(Class cls)
{
runtimeLock.assertWriting();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
ro = (const class_ro_t *)cls->data(); //-----⚠️⚠️⚠️最开始,类的data()得到的直接就是class_ro_t
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
//-----⚠️⚠️⚠️如果rw已经分配了内存,则rw指向cls->data(),然后将rw的ro指针指向之前最开始的ro
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data. //-----⚠️⚠️⚠️如果rw还没有分配内存
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); //-----⚠️⚠️⚠️给rw分配内存
rw->ro = ro;将rw的ro指针指向初始的ro
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw); //-----⚠️⚠️⚠️调整类的data()
}
isMeta = ro->flags & RO_META;
......
......
......
}
从该函数,我们可以看出,
- 最开始的时候,Class是没有读写部分的,只有只读部分,也就是
ro = (const class_ro_t *)cls->data();
- 接下来,才会分配空间给读写信息,也就是
rw
,然后通过rw->ro = ro;
将class_rw_t
自身的ro
指针指向真正的ro
信息。- 通过
cls->setData(rw);
将cls->data()
进行修改,并最终通过FAST_DATA_MASK
作用指向rw
完成上面的步骤之后,便可以开始处理category信息的加载了。接下来对于Class的分析,我只论述方法列表部分的处理,属性和协议部分其实跟方法的处理逻辑是相似的。
回到我们上面的问题,class_ro_t
和class_rw_t
中都有方法列表,它们有什么区别呢?
(二)class_ro_t
实际上,class_ro_t
代表Class的只读信息,也就是Class本身的固有信息,再直接一点就是是写在它的@interface
和@end
之间的方法,属性,等信息,当然最重要的作用还是存放类的成员变量信息ivars
,而且是被const
修饰说明是不可修改的,这也就是为什么Runtime无法动态增加成员变量,底层结构决定的。我个人将这部分理解成OC的静态信息。class_ro_t
中的method_list_t * baseMethodList;//方法列表
,是一个一维数组,里面装的就是这个Class本身的方法。
(三)class_rw_t
在有了class_rw_t
之后,便会进行category的处理,将Class本身的方法列表和category里面的方法列表先后放到class_rw_t
的method_array_t methods
里面,Class自身的方法列表会被最先放入其中,并且置于列表的尾部,category方法列表的加入顺序等同与category文件参与编译的顺序,这部分流程的详细说明我在Objective-C之Category的底层实现原理一文里有详细介绍。
因此,method_array_t methods;
是一个二维数组,如下图
(四)method_t
上面我们剖析了class_rw_t
、class_ro_t
这两个重要部分的结构,并且主要关注了其中的方法列表部分,而从上面的分析,可发现里面最基本也是重要的单位是method_t
,这个结构体包含了描述一个方法所需要的各种信息。下面,就对它进行一次彻底的扫描。
首先看一下源码里面对它的定义
struct method_t {
SEL name;
const char *types;
IMP imp;
};
IMP imp
——指向函数的指针(也就是方法/函数实现的地址),它也是在objc.h
里面被定义的:typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
SEL name
——我们都知到SEL
是方法选择器,可以理解成方法的名字,但是它到底是什么鬼?其实,他是在objc.h
里面被定义的:typedef struct objc_selector *SEL;
,它就是一个指针类型,指向了结构体类型struct objc_selector
,很遗憾苹果没有对其开源,但是请看下面的代码@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"%s",@selector(test)); } -(void)test { NSLog(@"%s",__func__); } @end ************************************* 2019-08-05 21:37:11.603121+0800 iOS-Runtime[2093:302816] test
从结果看,利用
test
方法的SEL
指针进行打印,输出了test
字符串,说明结构体struct objc_selector
内部包含了一个char *
的成员变量,并且应该是该结构体的第一个成员。因此我们可以用字符串来理解SEL
,它主要的作用就是用来表示方法的名字。它与字符串之间可通过如下方法转换
- 可以通过
@selector()
和sel_registerName()
将字符串转换成SEL
。- 可以通过
sel_getName()
和NSStringFromSelector()
将SEL
转换成字符串- 不同类中的相同名字的方法,所对应的SEL是相同的,也就是说对于
XXX
方法来说,它们的SEL都指向内存里同一个struct objc_selector
结构体对象,不论有多少个类里面定义了该XXX
方法。
const char *types
——函数类型编码(包括返回值类型、参数类型),iOS提供了一个@encode
指令,可以将具体的类型表示成字符串编码,也就是通过字符串来表示类型。主要目的是为了方便运行时,将函数的返回值和参数的类型通过字符串来描述并且存储。请看如下代码示例NSLog(@"%s",@encode(int)); NSLog(@"%s",@encode(float)); NSLog(@"%s",@encode(int *)); NSLog(@"%s",@encode(id)); NSLog(@"%s",@encode(void)); NSLog(@"%s",@encode(SEL)); NSLog(@"%s",@encode(float *)); ******************************************* 2019-08-06 16:13:22.136917+0800 iOS-Runtime[8904:779780] i 2019-08-06 16:13:22.137461+0800 iOS-Runtime[8904:779780] f 2019-08-06 16:13:22.137549+0800 iOS-Runtime[8904:779780] ^i 2019-08-06 16:13:22.137639+0800 iOS-Runtime[8904:779780] @ 2019-08-06 16:13:22.137718+0800 iOS-Runtime[8904:779780] v 2019-08-06 16:13:22.137832+0800 iOS-Runtime[8904:779780] : 2019-08-06 16:13:22.137912+0800 iOS-Runtime[8904:779780] ^f
从上面的打印可以看出各种不同的类型所对应的字符串表达。OC的方法的类型也是按照这个方法来表示的,只不过是把方法里面的返回值和参数的类型组合起来表示
例如- (int)test:(int)age height:(float)height
,我们知道OC方法对应的底层函数前两个是默认参数id self
和SEL cmd
,那么刚才的方法从左到右,返回值和参数的类型分别为int
->id
->SEL
->int
->float
,转换成类型编码,就是i
-@
-:
-i
-f
,而最终系统是这样表示的i24@0:8i16f20
,你应该会好奇,里面怎么多了一些数字,其实它们是用来描述函数的参数的长度和位置的的,从左到右可以这么解读:
i
—— 函数的返回值类型为int
24
—— 参数所占的总长度(24字节)@
—— 第一个参数id
0
—— 第一个参数在内存中的起始偏移量(0字节,也就是从第0个字节开始算起):
—— 第二个参数SEL
8
—— 第二个参数在内存中的起始偏移量(8字节,也就是从第8个字节开始算起,因此上面的id
参数占之前的8个字节)i
—— 第三个参数int
16
—— 第三个参数在内存中的起始偏移量(16字节,也就是从第16个字节开始算起,因此上面的SEL
参数占用了之前的8个字节)f
—— 第四个参数float
20
—— 第四个参数在内存中的起始偏移量(20字节,也就是从第20个字节开始算起,因此上面的int
参数占用了前面的4个字节,而总长度为24,因此最后的4个字节是给float
参数用的)如此一来,对于任意的OC方法,它的
method_t
里面的types字符串的值都可以依照上面的过程推导出来了。你在苹果官方文档搜索Type Encoding
就可以找到更具体的介绍,里面有所有参数类型对应的字符串表达对照表。根据上面的研究,
method_t
里面的三个成员变量就提供了我们对于一个OC方法所需要的所有信息,关于method_t
的解读就暂到这里。
(五)cache_t方法缓存
为了避免篇幅过长,方法缓存的探究请移步到下篇文章——
Runtime笔记(三)—— OC Class的方法缓存cache_t
Runtime系列文章
Runtime原理探究(一)—— isa的深入体会(苹果对isa的优化)
Runtime原理探究(二)—— Class结构的深入分析
Runtime原理探究(三)—— OC Class的方法缓存cache_t
Runtime原理探究(四)—— 刨根问底消息机制
Runtime原理探究(五)—— super的本质
[Runtime原理探究(六)—— Runtime的应用...待续]-()
[Runtime原理探究(七)—— Runtime的API...待续]-()
Runtime原理探究(八)—— 面试题中的Runtime