Category底层结构原理分析

1.Category底层结构

1.1 Category代码转成C++

在项目中新建一个Person的分类为Person+Test,里面添加一些属性和类方法以及对象方法,如下:

@interface Person (Test)

@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;

+ (void)test;
+ (void)test2;
- (void)test3;

@end

用命令行将OC的代码转成C++,先进入项目的文件夹,再用下面的命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m

回车后,目录下就会自动生成一个Person+Test.cpp的文件如下图:

将文件拖入工程中,但是不要让其参与编译:

查看其代码,如下图:

可知,底层将Person+Test这个类转成了 _category_t 结构体类型的 _OBJC_$_CATEGORY_Person_$_Test 变量,变量里面存放一些值。

1.2 Category的结构体了解

在cpp那个文件中查找_category_t,可以看到_category_t的定义如下:

其中结构体存储的信息如下:

  • 哪个分类的名字
  • 对象方法列表
  • 类方法列表
  • 协议方法列表
  • 属性列表

在看,结构体是如何赋值的:

(1)ame = "Person"

(2)cls = 0 (这个我也不知道为什么)

(3)instance_methods = _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,

 查找_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test这个结构体如下:

可知,这个结构体主要存放了:方法的大小、方法的个数、_obect_method结构体类型的方法实现的数组。

_obect_method的结构体定义如下:

_obect_method这个结构体存储的信息主要是:方法名、方法类型、方法地址。

(4)class_methods = _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,这个结构体存放的数据跟上一个原理一样。

(5)protocols = 0 ,因为我遵守协议,所以为0

(6)properties = _OBJC_$_PROP_LIST_Person_$_Test,如下图:

_prop_t 的结构体定义如下:

attributes是指属性类型。

通过以上分心,可以总结,Xcode编译的时候,都把分类对应的信息转成结构体存储起来。当开始加载类的时候,才会把分类的信息合并到类中。

1.3 Category的信息是如何合并到类信息中?

我们只能通过源码去了解,首先先从源码的_objc_init这个方法入手,_objc_init()方法是runtime被加载后第一个执行的方法。这个方法有如下几个函数:

其中:

environ_init()方法是环境配置
tls_init()方法这里先不多做介绍,因为涉及的知识面比较广,后面的文章会给于详细解释static_init()也非常有意思
lock_init()从名字可以看出是跟锁相关
exception_init()跟异常相关
_dyld_objc_notify_register是这个方法的主角,它会先调用 map_images 做解析和处理,把 Category 的实例方法、协议以及属性添加到类上,把 Category 的类方法和协议添加到类的 metaclass 上;接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 load 方法和其 Category 的 load 方法。

map_images的方法如下:

map_images_nolock()的方法如下:

....

_read_images()的方法如下:

找到该方法,往下找,会看到一个注释:// Discover categories.,如下图:

如果分类中有实例方法、属性或者协议方法,就会调用remethodizeClass()这个方法把这些信息与类对象合并:

attachCategories()方法的源码如下:

因为遍历的时候,i是从大到小的,所以,后编译的会先加入数组中。

attachLists的方法如下:

查看memmove 移动 和memcpy复制 的方法如下:

也就说分类的方法会放在类的方法的前面,也就导致了分类会覆盖父类的方法。分类是后编译先调用。如何查看编译的顺序呢?

1.4 load加载的顺序

查看load加载方法如下:

从源码可知:先调用类的load方法,再调用分类的load的方法。

那么类的load方法调用的顺序又是什么呢?查看源码如下:

从上图可知,调用_getObjc2NonlazyClassList这个方法把项目中的类加载出来,这个类加载的顺序是根据类编译的先后顺序来的

查看schedule_class_load()方法的代码如下:

从上面的源码可知:把一个类加载到数组前,会用递归的方式把父类的方法加载进去。

那么load的方法为什么不会被分类或者子类的load方法覆盖呢?查看源码如下:

load_method_t的结构体定义如下:

从源码可知,调用load方法之前,是把load方法的IMP地址获取出来,直接调用,而不是通过object_msgSend的方法来调用的。object_msgSend的调用机制是先查找本类的方法列表(该方法列表:分类的方法会加载在类的方法的前面),找到对应的方法名,再取出对应的方法地址调用。当通过消息机制获取到的方法,可能是分类的方法实现而不是类本身的方法实现,但是直接通过load的IMP实现地址的话,就不会出现这个问题。

总结load调用顺序如下:

(1)先根据编译的先后顺序调用类的load方法,类的load方法会先调用父类的load方法,再调用子类的load方法

(2)类的load方法调用完后,在根据编译的先后顺序调用分类的load方法

1.4 initialize加载的顺序

+initialize方法会在类第一次接收到消息时调用。查看_class_initialize()的源码如下:

得出调用顺序:

  • 先调用父类的+initialize,再调用子类的+initialize
  • (先初始化父类,再初始化子类,每个类只会初始化1次)。
  • 因为在加载分类的时候,会把分类的方法放在类的方法的前面,所以如果分类重写了initialize,那么会调用分类的initialize方法。

+initialize和+load的很大区别:

  • +initialize是通过objc_msgSend进行调用的,而+load方法是通过load的函数指针调用的,所以有以下特点:
  • 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
  • 如果分类实现了+initialize,就覆盖类本身的+initialize调用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值