iOS底层原理16:类的加载原理(下)

这是我参与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.mcpp文件:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+A.m -o Person+A.cpp
复制代码

查看Person+A.cpp文件:

cpp文件中,我们发现Person+A对应的是一个_category_t类型的结构体,其结构如下:

因为分类没有元类,所以它包含了instance_methodsclass_methods,而name应该是分类的后缀Acls是分类对应的Person

但是我们查看Person+A的时候其结果确实:

这是因为,在cpp静态文件中,Person+A还没有跟Person进行关联,所以这里name直接写了Person,而cls0

我们在Person+A.h中实现NSObject协议,然后重新生成cpp文件,查看其结构:

多出了(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_A,

再来看方法列表:

可以看出类方法对象方法都已经有了,唯独没有属性settergetter方法,这说明在编译阶段就已经确定了category中的属性不会生成settergetter方法;

那么,Person+A里边的属性方法是如何加载到主类Person中的呢?

category的加载

在上一篇文章中,断点调试的时候:

发现在这个地方rwe没有值,根据auto rwe = rw->ext();我看需要去看一下rwext()方法,如果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=2addedCount=1,newCount=3

旧的数组有两个元素第一个加载进来的分类主类,当前需要再添加一个分类,之后生成一个3个元素的数组;

第一次循环,向newArray->lists[1 + 1]的位置赋值array()->lists[1]; (主类的)

第二次循环,向newArray->lists[0 + 1]的位置赋值array()->lists[0]; (第一个分类)

也就是将旧数组中的两个数据,放在了新数组的后边两个位置12

继续执行:

将要添加的第二个分类的数据,放在了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+APerson+BPerson+CPerson+D四个分类,非懒加载的相关函数也都会被调用;其流程与主类分类都实现load方法是一样的,最后都是通过attachCategories实现分类加载;

load方法其实是很耗时的;

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享