这是我参与8月更文挑战的第5天,活动详情查看: 8月更文挑战
我们在之前的文章中讲了类的加载原理
,但是到目前为止,类的rwe
什么时候才会赋值,我们依然不得而知,但是我们能推测出应该是在methodizeClass
方法中,但是我们之前调试的结果,rwe
在这个方法中是nil
,那么有没有其他办法能够让rwe
有值呢?我们注意到methodizeClass
方法的注释Attach categories
是附加类别,那么类别是否会影响到rwe
呢?
category的本质
首先为Person
类新增类别:
@interface Person (A)
@property (nonatomic, copy) NSString *catName;
@property (nonatomic, assign) int catAge;
- (void)catInstanceA;
+ (void)catClassA;
@end
@implementation Person (A)
- (void)catInstanceA {
NSLog(@"__%s__", __func__);
}
+ (void)catClassA {
NSLog(@"__%s__", __func__);
}
@end
复制代码
然后,命令行生成Person+A.m
的cpp
文件:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+A.m -o Person+A.cpp
复制代码
查看Person+A.cpp
文件:
在cpp
文件中,我们发现Person+A
对应的是一个_category_t
类型的结构体,其结构如下:
因为
分类没有元类
,所以它包含了instance_methods
和class_methods
,而name
应该是分类的后缀A
,cls
是分类对应的Person
;
但是我们查看Person+A
的时候其结果确实:
这是因为,在cpp
静态文件中,Person+A
还没有跟Person
进行关联,所以这里name
直接写了Person
,而cls
是0
;
我们在Person+A.h
中实现NSObject
协议,然后重新生成cpp
文件,查看其结构:
多出了(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_A,
再来看方法列表:
可以看出类方法
和对象方法
都已经有了,唯独没有属性
的setter
和getter
方法,这说明在编译阶段
就已经确定了category
中的属性不会生成setter
和getter
方法;
那么,Person+A
里边的属性
和方法
是如何加载到主类Person
中的呢?
category的加载
在上一篇文章中,断点调试的时候:
发现在这个地方rwe
没有值,根据auto rwe = rw->ext();
我看需要去看一下rw
的ext()
方法,如果ext()
有值的话,那么rwe
就有值了
在ext()
方法下边,我们看到了extAllocIfNeeded()
方法,此方法是创建ext
的,那么它是在哪调用的呢?
全文搜索发现调用extAllocIfNeeded
的地方有好几处:
attachCategories
objc_class::demangledName
class_setVersion
addMethods_finish
class_addProtocol
_class_addProperty
objc_duplicateClass
我们通过其方法名称及注释会发现,它们大都与运行时有关,也就是说在运行时
才会有rwe
的创建,这跟WWDC2020
中关于运行时的优化
的内容是一致的;
因为在methodizeClass
中有Attach Categories
的操作(根据注释判断),那么接下来,我们主要研究attachCategories
方法
attachCategories
全文搜索,调用attachCategories
的地方发现有以下两处:
attachToClass
load_categories_nolock
attachToClass
接下来,我们需要梳理,都哪些地方调用了attachToClass
,搜索之后发现只有methodizeClass
方法中调用了:
那么调用的是if
里边的还是外边你的呢?previously
是方法传参,那么我们看都什么地方调用了methodizeClass
方法,以及传参的previously
什么值?
搜索发现methodizeClass
方法的previously
,来源于realizeClassWithoutSwift
方法,而源码中所有的realizeClassWithoutSwift
方法,参数previously
都为nil
;
那么也就是说,最终调用的attachToClass
方法是:
其调用流程为:
realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
–>attachCategories
也就是在类的加载的过程中,会调用attachCategories
;
load_categories_nolock
此流程我们稍后分析
主类与分类的load方法分情况加载
接下来,结合主类
和分类
是否实现load
方法的不同情况来看一下其加载流程,在之前的分析中,我们大致已经了解了分类的加载流程,接下来,我们已经在各个流程节点上添加拦截Person
的打印信息,接下来,分情况看一下执行流程:
主类有load,分类有load
执行流程为:
readClass
–>_read_images
–>realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
–>load_categories_nolock
–>attachCategories
主类没有load,分类有load
执行流程为:
readClass
–>_read_images
–>realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
主类有load,分类没有load
执行流程为:
readClass
–>_read_images
–>realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
主类没有load,分类没有load,main函数什么都没有
执行流程为:
readClass
后边无调用
主类有load,分类不全有(4个分类)
只有一个分类没有load
执行流程为:
readClass
–>realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
–>attachCategories
两个分类没有load
执行流程为:
readClass
–>realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
–>attachCategories
三个分类没有load
执行流程为:
readClass
–>realizeClassWithoutSwift
–>methodizeClass
–>attachToClass
单个分类加载流程分析-attachLists
接下来,我们以主类
和分类
都有load
的情况为例调试分析:
在realizeClassWithoutSwift
方法中,查看ro
数据,发现分类方法还没有进来,那么继续向下调试进入attachCategories
:
向下执行:
mlist
是分类
中的方法列表
,将其地址
放在mlists
列表的最后;
继续执行,进入prepareMethodLists
方法,mlists + ATTACH_BUFSIZ - mcount
取地址平移,将最后一个元素地址
传进去:
然后进入fixupMethodList
方法进行排序操作:
继续执行,即将进入attachLists
方法,传入一个二维指针
,这个指针(最终指向了分类的方法地址):
执行进入attachLists
方法:
继续执行:
发现将主类
的方法的指针
放在了数组的第二个位置
也就是最后,而分类
的方法的指针
放在了数组的第一个位置
;分类方法在前,主类方法在最后;
多个分类加载分析-attachLists
添加多个Person
类的分类,均实现了load
方法:
然后运行项目进行调试,因为主类
和分类
都实现load
方法时会经过load_categories_nolock
方法,我们在此处断点调试:
此处for
循环的刚好是4
个分类,当i
= 0
,也就是拦截到Person+C
的时候,我们在之前的流程已经走过了(单个分类加载),这次来看i
= 1
的时候,也就是加载第二个分类时的情况:
在attachLists
方法中会走到第一个if
条件中:
解析:
oldCount
=2
,addedCount
=1
,newCount
=3
旧的数组有两个元素第一个加载进来的分类
和主类
,当前需要再添加一个分类
,之后生成一个3
个元素的数组;
第一次循环,向newArray->lists[1 + 1]
的位置赋值array()->lists[1];
(主类的)
第二次循环,向newArray->lists[0 + 1]
的位置赋值array()->lists[0];
(第一个分类)
也就是将旧数组中的两个数据,放在了新数组的后边两个位置1
和2
上
继续执行:
将要添加的第二个分类的数据,放在了newArray->lists[0]
的位置,打印结果显示newArray->lists[0]
地址改变了;数组里存放的都是指针地址
;
ro和rw数据加载分析:
主类有load,分类有load
走attachCategories
,前边已经分析过,此处不再分析
主类没有load,分类有load
流程走到realizeClassWithoutSwift
之后,打印看一下ro
数据:
baseMethods
里边有14
个数据,说明主类
和父类
的方法都被加载过了,都来自与data()
主类有load,分类没有load
baseMethods
里边也是14
个数据,说明主类
和父类
的方法都被加载过了,都来自与data()
主类没有load,分类没有load
根据堆栈信息,发现走了 第一次消息发送的流程,数据都存放在data()
主类有load,多个分类不全有load
说明Person+A
,Person+B
,Person+C
,Person+D
四个分类,非懒加载
的相关函数也都会被调用;其流程与主类
和分类
都实现load
方法是一样的,最后都是通过attachCategories
实现分类加载;
load
方法其实是很耗时的;