Objective-C的+load方法调用原理分析
Objective-C的+initialize方法调用原理分析
Category的使用场景
我个人粗浅理解,就是将一个类的实现,拆解成小的模块,便于管理和维护。因为实际项目中,有些类的功能可能会非常复杂,导致一个类的代码过多,这对后期修改和维护是比较不利的,所以category方便了程序员,可以根据功能,业务等形式的划分,将类的一大堆方法分组放置以及调用。
#####有趣的思考
先来看一个最简单的category结构,一下代码定义了一个CLPerson
类 和它的一个category CLPerson+Test
// ******************** CLPerson
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
-(void)run;
@end
#import "CLPerson.h"
@implementation CLPerson
-(void)run
{
NSLog(@"CLPerson Run");
}
@end
// ******************** CLPerson+Test
#import "CLPerson.h"
@interface CLPerson (Test)
-(void)test;
@end
#import "CLPerson+Test.h"
@implementation CLPerson (Test)
-(void)test{
NSLog(@"Test");
}
@end
// ******************** CLPerson+Eat
#import "CLPerson.h"
@interface CLPerson (Eat)
-(void)eat;
@end
#import "CLPerson+Eat.h"
@implementation CLPerson (Eat)
-(void)eat{
NSLog(@"Eat");
}
@end
请问❓❓❓:以下的两个方法调用,底层到底发生了什么,它们本质是否相同?
CLPerson *person = [[CLPerson alloc]init];
[person run]; //类的实例方法调用
[person test];//分类的实例方法调用
[person eat];//分类的实例方法调用
我们都知道,[实例对象 方法]
这种写法,经过底层转换之后,实际上就是,objc_msgSend(类对象, @selector(实例方法))
,也就我们oc的一个基本概念,消息发送机制。因此,我们可以推定,***[person run]
这句代码,在消息发送机制下,首先会根据 person
的isa
指针找到CLPerson
的类对象,然后在类对象的方法列表(method_list_t * methods
)里面找到该方法的实现,然后进行调用。***
接下来,你肯定会想
- 那么
[person test]
和[person eat]
呢?它的消息是发送给谁呢? - 是发送给
person
的类对象吗? - 还是说,对于
CLPerson+Test.h
和CLPerson+Eat.h
来说,也有其独立对应的分类对象呢?
带着这些思考和问题,我们接下来一步一步地进行拆解。
Category的实现原理
底层结构——所有一切始于编译
要想知道原理,不要猜,也不要轻易相信别人说的东西,自己验证一下才是最靠谱的。在命令行下,进入CLPerson+Test.m
文件所在路径执行以下命令–>
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc CLPerson+Test.m
得到编译后的c++文件CLPerson+Test.cpp
,将其拖入xcode项目中进行查看,但是不要加入编译列表,否则程序跑不起来。直接查看文件底部,就可以找到category相关的底层信息,请看下图剖析
上图比较粗糙,请谅解,但比文字描述来的更加直观,上面基本上分析清楚了在编译结束之后,category是以何种形式存在的,现在用文字来总结一下:
category经过编译过程之后,系统为其定义了如下的一个结构体
//注意,编译后的cpp文件一般比较长,会有好几万行,
//一般我们关注类结构相关的信息,都在最后,
//所以可以直接把文件拖到底,便可以找到这些信息
struct _category_t {
const char *name; //用来存放类名
struct _class_t *cls;
const struct _method_list_t *instance_methods;//用来存放category里面的实例方法列表
const struct _method_list_t *class_methods;//用来存放category里面的类方法列表
const struct _protocol_list_t *protocols;//用来存放category里面的协议列表
const struct _prop_list_t *properties;//用来存放category里面的属性列表
};
这个struct _category_t
结构体,就是在程序在编译之后,被用来存放category
的相关信息(instance methods
, class methods
,protocol
,property
)的。
反过来描述,编译的时候,系统会给每一个category
生成一个对应的结构体变量,而且他们都是struct _category_t
类型的,然后把category
里面的信息存到这个变量里面。
在我的示例里面,这个变量的名称叫_OBJC_$_CATEGORY_CLPerson_$_Test
,这个名字很清晰的表明,它存储的是Objective-c
下的CLPerson
类的Test
分类的信息。
struct _category_t
中定义了六个成员变量,除去其中的第二个,我个人还没搞明白有什么用,其他的五个作用则非常清晰了
const char *name;
上图中的a部分,其值表示category
所对应的类的名字。
const struct _method_list_t *instance_methods;
上图中的b部分,其值就是实例方法列表,可以看到里面正好放了我们定义的实例方法-test
const struct _method_list_t *class_methods;
上图中的c部分,其值就是类方法列表,可以看到里面放了我们定义的类方法-classTest
const struct _protocol_list_t *protocols;
上图中的d部分,其值就是协议列表,可以看到里面存放了NSCoping
协议
const struct _prop_list_t *properties;
上图中的e部分,其值就是属性,可以看到里面有我们定义的age
属性
源码分析
上面的篇章,我们通过查看编译后的cpp文件,了解了category
在编译阶段完成后的存在形式,以CLPerson+Test
为例,它所对应的struct _category_t
变量中,第一个成员变量name
的值为"CLPerson"
(CLPerson+Eat
对应的name
也是"CLPerson"
,可以自行验证),而且根据我在对象的本质(上)——OC对象的底层实现中所讨论所得出的结果可以知道,一个OC类XXX
在底层都存在一个对应的C++结构体实现struct XXX_IMPL
,但我们在CLPerson+Test.cpp
文件中,并没有发现 struct CLPerson+Test_IMPL
/struct CLPerson+Eat_IMPL
,因此,我猜想CLPerson
的category
中的信息,应该还是存储在CLPerson
所对应的class
对象和meta-class
对象中,category
自己并没有独立的class
对象和meta-class
对象。CLPerson
旗下的所有category
里面的信息,应该是在某个阶段被合并到了类的CLPerson
的class
对象和meta-class
对象中。从编译的结果看,我们并没有发现有合并的操作,仅仅是给每个category
生成了对应的struct _category_t
类型的变量,存放其信息。所以我合理怀疑,合并操作应该是发生在Runtime阶段。
为了证明以上猜想,我们还是要挖掘Runtime的源码。我们先去苹果官网下载一份objc4的最新源码。然后我们直接寻找objc-os.mm
文件,这个文件可以看作是Runtime进行初始化的地方。然后找到_objc_init()
方法,这个方法是Runtime被加载后执行的第一个方法,可以理解成Runtime的入口方法。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier w