类的加载原理:
iOS 类的加载原理上
iOS 类的加载原理中
iOS 类的加载原理下
分类的加载原理补充及类扩展 , 关联对象介绍
ro, rw, rwe
在继续讲类的加载之前我们先来了解一个概念,什么是 ro, rw 跟 rwe。
ro:app 在使用类时,是需要在磁盘中 app 的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags 和方法缓存,而类的额外信息(name、方法、协议和实例变量等)存储在class_ro_t中。class_ro_t简称ro,read only,将类从磁盘中读取到内存中就是对ro的赋值操作。由于ro是只读的,加载到内存后不会发生改变又称为clean memory(干净内存)。
rw:class_rw_t简称rw,read write,用于读写编写程序。drity memory在进程运行时发生更改的内存。类一经使用运行时就会分配一个额外的内存,那么这个内存变成了drity memory。但是在实际应用中,类的使用量只是 10%,这样就在rw中造成了内存浪费,所以苹果就把rw中方法、协议和实例变量等放到了class_rw_ext_t中。
rwe:class_rw_ext_t简称rwe,read write ext,用于运行时存储类的方法、协议和实例变量等信息。
推荐大家看下 WWDC20 这段视频,相信大家看完之后会对 ro, rw 跟 rwe 有更详细的了解。
attachCategories 反推思路
了解了 ro, rw 跟 rwe 之后,我们先来看下 rew 是在哪里被赋值的。
在源码的 attachCategories 方法中我们可以看到 auto rwe = cls->data()->extAllocIfNeeded(),所以如果要对 rwe 赋值的话必然要调用 extAllocIfNeeded。
通过搜索 extAllocIfNeeded 可以看到分别在添加分类 , 协议 , 属性等这些地方才会调用 extAllocIfNeeded 函数。 这也验证了 WWDC 视频里面讲的只有在分类被加载时向类中添加方法或者调用运行时 API 动态添加方法或者属性的时候才会更改 rwe。因为这里我们要探究分类的加载,说以我们要看 attachCategories 函数。因为我们不知道分类是什么时候被加载进来的,所以我们通过反推的方式来看下 attachCategories 在哪些地方被调用了。
搜索可以看到分别在 load_categories_nolock 跟 attachToClass 这两个地方调用了 attachCategories 函数。
接着我们搜索 attachToClass 发现有三个地方调用了这个函数,但是都是在 methodizeClass 函数里面调用的。我们看到代码逻辑走到 if (previously) {} 的前提是 previously 有值,而 previously 是函数参数,所以我们搜索 methodizeClass 函数。
搜索之后看到 methodizeClass 在 realizeClassWithoutSwift 里面被调用,所以继续搜索 realizeClassWithoutSwift。
搜索之后可以看到调用 realizeClassWithoutSwift 函数时 previously 传的都是 nil,是备用参数,所以不会走到 if (previously) {} 里面。所以 attachCategories 调用流程如下。
load_categories_nolock->attachCategoriesrealizeClassWithoutSwift->methodizeClass->attachToClass->attachCategories
分类和主类加载的 5 种情况
通过前面的分析我们已经知道类的加载跟是否实现 load 方法分为懒加载跟非懒加载两种情况,那么分类的加载是否也跟 load 方法有关系呢?这里我们分几种情况来探究一下。
1. 类与分类都实现 load 方法
@interface LGPerson : NSObject
{
int a;
}
@property (nonatomic, copy) NSString *name;
- (void)saySomething;
+ (void)sayHappy;
@end
#import "LGPerson.h"
#import <objc/message.h>
@implementation LGPerson
+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : Happy!!!");
}
@end
#import "LGPerson.h"
@interface LGPerson (LGA)
@property (nonatomic, copy) NSString *cateA_name;
- (void)saySomething;
- (void)cateA_instanceMethod1;
+ (void)cateA_classMethod1;
@end
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod1{
NSLog(@"%s",__func__);
}
@end
首先是类与分类都实现了 load 方法,然后我们在 attachCategories 中来做拦截,看一下整体的加载流程。
通过断点拦截可以看到类与分类都实现 load 方法时的加载流程如下:
_read_images(非懒加载类) ->realizeClassWithoutSwift->load_categories_nolock->attachToClass->attachCategories
当来到 realizeClassWithoutSwift 函数的时候我们打印 ro 信息,看到 $3 里面方法数量是 3 个,都是主类的方法,说明分类的方法并没有加载到 ro里面。
在 load_categories_nolock 函数中我们打印可以看到 count 是当前分类的数量,打印 cat 也可以看到当前分类的结构及分类名称。
接着在 for (uint32_t i = 0; i < cats_count; i++) 中也可以看到 mlist 正好就是对应分类 LGPerson (LGA) 中的两个方法,代码执行的逻辑就是把 mlist 地址存放在 mlists 的第 63 位。
接着我们往下看。
在这里我们可以看到 mlists + ATTACH_BUFSIZ - mcount 是一个二级指针,prepareMethodLists 之前已经分析过了,会进行方法的排序。接着我们来到 attachLists 函数。
在这里 lists 存放的就是一级数组指针,array()->lists[1] 存放的是主类的方法列表,array()->lists[0] 存放的是分类的方法列表。
当有多个分类的时候,会有一些不同,在 attachLists 函数中会走到 if (hasArray()) 条件里面。
2. 分类实现 load 方法,主类不实现 load 方法
@interface LGPerson : NSObject
{
int a;
}
@property (nonatomic, copy) NSString *name;
- (void)saySomething;
+ (void)sayHappy;
@end
#import "LGPerson.h"
#import <objc/message.h>
@implementation LGPerson
//+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : Happy!!!");
}
@end
#import "LGPerson.h"
@interface LGPerson (LGA)
@property (nonatomic, copy) NSString *cateA_name;
- (void)saySomething;
- (void)cateA_instanceMethod1;
+ (void)cateA_classMethod1;
@end
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod1{
NSLog(@"%s",__func__);
}
@end
当主类不实现 load 方法,分类实现 load 方法的时候加载流程如下,跟主类实现 load 方法,分类不实现 load 方法的情况比较相似,也没有走 attachCategories 方法。
_read_images(非懒加载类) ->realizeClassWithoutSwift->load_categories_nolock->attachToClass
在这里可以看到一个问题,主类没有实现 load 方法的情况下也会走到非懒加载加载流程,这是因为分类实现了 load 方法。但是没有走 attachCategories 方法,那么分类的方法是什么时候加载到方法列表的呢?
通过在 realizeClassWithoutSwift 方法这里断点输出可以看到,method_list_t 中方法 count 为 5,正好是主类加上分类的方法数量,而且输出 p $3.get(1).big() 可以看到正好是分类里的方法,说明在主类不实现 load 方法,分类实现 load 方法的时候,在 realizeClassWithoutSwift 函数中伴随着类的加载,分类的方法数据在这个时候通过 data() 就可以获取到了。
3. 分类不实现 load 方法,主类实现 load 方法
@interface LGPerson : NSObject
{
int a;
}
@property (nonatomic, copy) NSString *name;
- (void)saySomething;
+ (void)sayHappy;
@end
#import "LGPerson.h"
#import <objc/message.h>
@implementation LGPerson
+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : Happy!!!");
}
@end
#import "LGPerson.h"
@interface LGPerson (LGA)
@property (nonatomic, copy) NSString *cateA_name;
- (void)saySomething;
- (void)cateA_instanceMethod1;
+ (void)cateA_classMethod1;
@end
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
//+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod1{
NSLog(@"%s",__func__);
}
@end
当分类不实现 load 方法,主类实现 load 方法的时候可以看到加载流程如下,相对于类与分类都实现 load 方法的情况下没有走 attachCategories 方法。
_read_images(非懒加载类) ->realizeClassWithoutSwift->load_categories_nolock->attachToClass
当分类不实现 load 方法,主类实现 load 方法的时候在 realizeClassWithoutSwift 方法中这里打印可以看到跟当主类不实现 load 方法,分类实现 load 方法的时候相似,都是在这里通过 data() 就可以获取到分类的方法数据。
4. 分类与主类都不实现 load 方法
@interface LGPerson : NSObject
{
int a;
}
@property (nonatomic, copy) NSString *name;
- (void)saySomething;
+ (void)sayHappy;
@end
#import "LGPerson.h"
#import <objc/message.h>
@implementation LGPerson
//+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : Happy!!!");
}
@end
#import "LGPerson.h"
@interface LGPerson (LGA)
@property (nonatomic, copy) NSString *cateA_name;
- (void)saySomething;
- (void)cateA_instanceMethod1;
+ (void)cateA_classMethod1;
@end
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
//+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod1{
NSLog(@"%s",__func__);
}
@end
当主类与分类都不实现 load 方法的时候可以可以看到会直接来到 main 函数这里。
当主类与分类都没有实现 load 方法的时候,可以看到类的初始化会推迟到当前类接收第一条消息的时候,但是不管是主类还是分类方法的加载都是在 realizeClassWithoutSwift 函数中通过 data() 就可以获取到,这个时候就已经加载了。
5. 当主类实现 load 方法,分类不全实现 load 方法
@interface LGPerson : NSObject
{
int a;
}
@property (nonatomic, copy) NSString *name;
- (void)saySomething;
+ (void)sayHappy;
@end
@implementation LGPerson
+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : Happy!!!");
}
@end
@interface LGPerson (LGA)
@property (nonatomic, copy) NSString *cateA_name;
- (void)saySomething;
- (void)cateA_instanceMethod1;
+ (void)cateA_classMethod1;
@end
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
+ (void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod1{
NSLog(@"%s",__func__);
}
@end
@interface LGPerson (LGB)
- (void)saySomethingB;
@end
@implementation LGPerson (LGB)
//+ (void)load{}
- (void)saySomethingB {
NSLog(@"%s",__func__);
}
@end
这里有一种特殊的情况,当主类 LGPerson 实现 load 方法,但是分类只有 LGA 实现 load 方法的时候可以看到会来到 load_categories_nolock 方法,count 正好是分类的数量。在这里会循环执行 attachCategories 函数。上面可以看到正常情况下我们可以通过 data() 读取到分类及主类的方法数据,其实都是由 MachO 文件直接加载数据,但是 load 方法会打破这个顺序,所以会非常耗时,所以我们要谨慎使用 load 方法。