类的加载原理:
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
->attachCategories
realizeClassWithoutSwift
->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
方法。