iOS 类的加载原理下

1,411 阅读6分钟

类的加载原理:
iOS 类的加载原理上
iOS 类的加载原理中
iOS 类的加载原理下
分类的加载原理补充及类扩展 , 关联对象介绍

ro, rw, rwe

在继续讲类的加载之前我们先来了解一个概念,什么是 ro, rwrwe

ro:app 在使用类时,是需要在磁盘中 app 的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags 和方法缓存,而类的额外信息(name、方法、协议和实例变量等)存储在 class_ro_t 中。class_ro_t 简称 roread only,将类从磁盘中读取到内存中就是对 ro 的赋值操作。由于 ro 是只读的,加载到内存后不会发生改变又称为 clean memory(干净内存)。

rwclass_rw_t 简称 rwread write,用于读写编写程序。 drity memory 在进程运行时发生更改的内存。类一经使用运行时就会分配一个额外的内存,那么这个内存变成了drity memory。但是在实际应用中,类的使用量只是 10%,这样就在 rw 中造成了内存浪费,所以苹果就把 rw 中方法、协议和实例变量等放到了class_rw_ext_t 中。

rwe: class_rw_ext_t 简称 rweread write ext,用于运行时存储类的方法、协议和实例变量等信息。

推荐大家看下 WWDC20 这段视频,相信大家看完之后会对 ro, rwrwe 有更详细的了解。

attachCategories 反推思路

了解了 ro, rwrwe 之后,我们先来看下 rew 是在哪里被赋值的。

image.png

在源码的 attachCategories 方法中我们可以看到 auto rwe = cls->data()->extAllocIfNeeded(),所以如果要对 rwe 赋值的话必然要调用 extAllocIfNeeded

image.png image.png image.png

通过搜索 extAllocIfNeeded 可以看到分别在添加分类 , 协议 , 属性等这些地方才会调用 extAllocIfNeeded 函数。 这也验证了 WWDC 视频里面讲的只有在分类被加载时向类中添加方法或者调用运行时 API 动态添加方法或者属性的时候才会更改 rwe。因为这里我们要探究分类的加载,说以我们要看 attachCategories 函数。因为我们不知道分类是什么时候被加载进来的,所以我们通过反推的方式来看下 attachCategories 在哪些地方被调用了。

image.png image.png

搜索可以看到分别在 load_categories_nolockattachToClass 这两个地方调用了 attachCategories 函数。

image.png

接着我们搜索 attachToClass 发现有三个地方调用了这个函数,但是都是在 methodizeClass 函数里面调用的。我们看到代码逻辑走到 if (previously) {} 的前提是 previously 有值,而 previously 是函数参数,所以我们搜索 methodizeClass 函数。

image.png

搜索之后看到 methodizeClassrealizeClassWithoutSwift 里面被调用,所以继续搜索 realizeClassWithoutSwift

image.png

搜索之后可以看到调用 realizeClassWithoutSwift 函数时 previously 传的都是 nil,是备用参数,所以不会走到 if (previously) {} 里面。所以 attachCategories 调用流程如下。

  1. load_categories_nolock -> attachCategories
  2. 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 中来做拦截,看一下整体的加载流程。

image.png image.png image.png image.png image.png image.png

通过断点拦截可以看到类与分类都实现 load 方法时的加载流程如下:

_read_images(非懒加载类) -> realizeClassWithoutSwift -> load_categories_nolock -> attachToClass -> attachCategories

image.png image.png

当来到 realizeClassWithoutSwift 函数的时候我们打印 ro 信息,看到 $3 里面方法数量是 3 个,都是主类的方法,说明分类的方法并没有加载到 ro里面。

image.png

load_categories_nolock 函数中我们打印可以看到 count 是当前分类的数量,打印 cat 也可以看到当前分类的结构及分类名称。

image.png image.png image.png

接着在 for (uint32_t i = 0; i < cats_count; i++) 中也可以看到 mlist 正好就是对应分类 LGPerson (LGA) 中的两个方法,代码执行的逻辑就是把 mlist 地址存放在 mlists 的第 63 位。

接着我们往下看。

image.png

在这里我们可以看到 mlists + ATTACH_BUFSIZ - mcount 是一个二级指针,prepareMethodLists 之前已经分析过了,会进行方法的排序。接着我们来到 attachLists 函数。

image.png

在这里 lists 存放的就是一级数组指针,array()->lists[1] 存放的是主类的方法列表,array()->lists[0] 存放的是分类的方法列表。

当有多个分类的时候,会有一些不同,在 attachLists 函数中会走到 if (hasArray()) 条件里面。

image.png

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

image.png image.png image.png image.png

当主类不实现 load 方法,分类实现 load 方法的时候加载流程如下,跟主类实现 load 方法,分类不实现 load 方法的情况比较相似,也没有走 attachCategories 方法。

_read_images(非懒加载类) -> realizeClassWithoutSwift -> load_categories_nolock -> attachToClass

在这里可以看到一个问题,主类没有实现 load 方法的情况下也会走到非懒加载加载流程,这是因为分类实现了 load 方法。但是没有走 attachCategories 方法,那么分类的方法是什么时候加载到方法列表的呢?

image.png image.png

通过在 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

image.png image.png image.png image.png

当分类不实现 load 方法,主类实现 load 方法的时候可以看到加载流程如下,相对于类与分类都实现 load 方法的情况下没有走 attachCategories 方法。

_read_images(非懒加载类) -> realizeClassWithoutSwift -> load_categories_nolock -> attachToClass

image.png

当分类不实现 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

image.png

当主类与分类都不实现 load 方法的时候可以可以看到会直接来到 main 函数这里。

image.png image.png

当主类与分类都没有实现 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

image.png

这里有一种特殊的情况,当主类 LGPerson 实现 load 方法,但是分类只有 LGA 实现 load 方法的时候可以看到会来到 load_categories_nolock 方法,count 正好是分类的数量。在这里会循环执行 attachCategories 函数。上面可以看到正常情况下我们可以通过 data() 读取到分类及主类的方法数据,其实都是由 MachO 文件直接加载数据,但是 load 方法会打破这个顺序,所以会非常耗时,所以我们要谨慎使用 load 方法。