在上一篇文章类的加载(上)了解了到了_read_images大概流程,也详细讲解了readClass方法 技能回顾
下面讲解 标红区域 因为讲的是类的加载,当类的加载了解清楚了 其他步骤 也就清晰可见
懒加载类和非懒加载类
首先我们看类加载处理这里的源码
在源码注释里我们很清晰的看到了一句话 Realize non-lazy classes (for +load methods and static instances) 翻译过来 就是 实现非懒加载类(当实现了+load方法 或者 static instances)❓
带着疑问 我们修改源码 在 类的加载(上)我们曾在readClass里面也进行了修改 这次我们在 下面继续断点 并 修改源码目的针对性研究
截屏2020-10-20 下午1.31.55.png
非懒加载类环境流程
将LGPerson里的项目 添加上+load 运行 按照源码所说 此时它为非懒加载类
截屏2020-10-20 下午1.35.40.png
项目断在readClass 并继续过断点
截屏2020-10-20 下午1.37.20.png
截屏2020-10-20 下午1.40.48.png
我们看到确实来到了修改源码的断点 并即将进入 realizeClassWithoutSwift 函数 下面我们进行研究
realizeClassWithoutSwift
源码注释此方法的含义
截屏2020-10-20 下午1.47.10.png
解读
- 首次对类cls进行初始化,
- 包括分配读写数据。
- 不执行任何快速端初始化。
- 返回类的真实类结构。
源码查看及解读 (由于代码太长分段解读)
截屏2020-10-20 下午1.57.22.png
第一步:读取data数据
- 当前类的判空 是否已经实现 的判断
- 未实现也不为nil 读取 class的data数据 并强转为 (class_ro_t*)类型 获取 ro
- 获取元类判断条件
- 如果是未来的类 ,此时 rw 数据 已经准备好 直接读取 data() 获取 rw ro
- 正常的类 分配可写入的类数据。
- 申请开辟 <class_rw_t>数据结构 rw模板
- 根据ro设置rw 从ro中copy到rw中
- 将cls的data赋值为rw形式
第二步:递归调用 realizeClassWithoutSwift 完善 继承链
截屏2020-10-20 下午2.29.45.png-
递归调用 realizeClassWithoutSwift设置父类、元类
2.如果类已经实现,则直接返回cls 截屏2020-10-20 下午2.38.53.png
注: isa找到根元类之后,根元类的isa是指向自己,并不会返回nil,所以有以下递归终止条件,其目的是保证类只加载一次
1.如果类不存在,则返回nil
- remapClass 如cls不存在,则返回nil 截屏2020-10-20 下午2.41.06.png
-
设置父类和元类的isa指向
-
通过addSubclass 和 addRootClass设置父子的双向链表指向关系,即父类中可以找到子类,子类中可以找到父类
第三步:通过 methodizeClass 方法化类
源码注释此方法的含义
截屏2020-10-20 下午2.49.43.png
- 修复cls的方法列表、协议列表和属性列表。
- 附加任何突出类别。
源码查看及解读
截屏2020-10-20 下午3.28.43.png
- 在realizeClassWithoutSwift 我们已经拿到了 ro rw 没见到 rwe的踪影
- method_list 方法处理
注:在这里 我们想起了在 消息发送流程的慢速查找流程,其中查找算法是二分查找
,说明sel-imp是有序排列,那么如何排序的呢?
就是在这里
截屏2020-10-20 下午3.38.54.png
它内部是通过fixupMethodList
方法排序
截屏2020-10-20 下午3.41.01.png
进入fixupMethodList
源码实现,是根据selector address排序
截屏2020-10-20 下午3.43.11.png
- 无论是
methods
还是properties
还是protocols
我么发现都去调用了一个方法attachLists
添加上针对的代码 并断点到LGPerson
截屏2020-10-20 下午4.05.32.png
结果发现 rwe 为NULL 并未运行其 rwe为真的判断 为什么 留疑问
-
继续过断点来到了 attachToClass
截屏2020-10-20 下午4.11.02.png
截屏2020-10-20 下午4.19.50.png
在methodlist方法主要是将分类添加到主类中 当前环境没有分类所以下面都没有运行
懒加载类环境流程
👆上面的流程我们都是在实现了load的情况下进行跟进的下面👇我们来屏蔽load使其变为懒加载环境
截屏2020-10-20 下午10.23.29.png
并在初始化LGPerson代码断言 运行
截屏2020-10-20 下午10.29.57.png
发现 在main函数之前调用的 mapimages -> _read_images -> readClass
在次向下走一步断点
截屏2020-10-20 下午10.43.28.png
进入到methodizeClass里查看调用栈
截屏2020-10-20 下午10.48.33.png
这不正是我们之前学习的消息慢速转发流程lookImpOrForward发起的调用嘛
总结
- 什么是懒加载类 和非懒加载类
截屏2020-10-20 下午10.51.39.png - ro rw rwe 定义及为什么存在
-
app在使用类时,是需要在磁盘中app的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags和方法缓存 那么类的额外信息(name、方法、协议和实例变量等)存储在class_ro_t中
-
class_ro_t简称 ro: read only 干净的内存空间 只读 又称为 clean memory
-
class_rw_t简称 rw: read write 意义: 由于ios有运行时 会不断的插入 添加 删除 也就是 增删改 查 频繁 对当前 这块内存操作比较严重 为了防止对原始数据的破坏,所以就有了 rw 用于读写编写程序。 drity memory 在进程运行时发生更改的内存。类一经使用运行时就会分配一个额外的内存,那么这个内存变成了drity memory。但是在实际应用中,类的使用量只是10%,这样就在rw中造成了内存浪费,所以苹果就把rw中方法、协议和实例变量等放到了class_rw_ext_t中。
-
class_rw_ext_t简称 rwe: read write ext,用于运行时存储类的方法、协议和实例变量等信息。
class ro - rw - rwe.jpg
-
分类的加载流程
attachToClass
在methodlist方法主要是将分类添加到主类中,其源码实现如下
截屏2020-10-20 下午11.05.34.png
- 找到一个分类进来一次,即一个一个加载分类,不要混乱
- 当主类没有实现load, 分类开始加载、迫使主类加载,会走到if流程
下面研究 attachCategories
attachCategories
截屏2020-10-21 下午4.58.58.png源码注释解析
-
将方法列表、属性和协议从类别附加到类中。
假设所有cats中的类别都已加载并按加载顺序排序,
先有最古老的类别。 -
只有少数类在启动期间有超过64个类别。
这使用了一个小堆栈,并避免malloc。类别必须按正确的顺序添加,也就是从后到前。为了完成分块操作,我们从前向后迭代cats_list,向后构建本地缓冲区,并在块上调用attachLists。attachLists突出显示的
列表,因此最终结果按照预期的顺序。
在这里我们看到了 rwe 赋值
截屏2020-10-21 下午5.19.08.png-
判断rwe是否存在,存在直接取值不存在 则根据ro开辟。
截屏2020-10-21 下午5.26.46.png
点进去我们也看到了 attachLists方法 从0-1的过程也存在下面进行分析它
attachLists
截屏2020-10-21 下午5.40.21.png经过分析 我们可以看到 attachLists 方法存在三种情况 下面为 rwe 方法列表 0 -1的过程
rwe方法列表 0-1
回到上面断点处 我们打印 rwe
截屏2020-10-21 下午6.24.57.png
此时没有走下面的时候 rwe的 methods 正是 本类也就是ro->baseMethods 数据的 copy
我们继续向下走
截屏2020-10-22 上午10.51.07.png
画图表示 这个算法
category存入 64个空间的 mlists
- 先存进之前开辟好的64个空间的栈里
- mlists + ATTACH_BUFSIZ - mcount 获取mlists的首地址+ 64 - 有几个分类 最终等于 这几个分类的真实地址。抹掉没用到的。
- 排序
-
存入到rwe的methods中.
1). 从上面的0-1 过程 就是 开始创建 rwe 将本类 ro中的baseMethods存入rwe中 此时 attachlists 是 0-1 是一个 一纬数组。
2). 这次再进入 就是一个 1对多 的过程。
list ->many Lists
在下面是 多对多的逻辑示意图
截屏2020-10-22 下午1.15.14.png
总结
- readClass 主要是读取类,即此时的类仅有地址+名称,还没有data数据
- 懒加载类 未实现load的的类 数据加载推迟到第一次消息的时候
- 非懒加载类 map_images的时候 加载所有的类数据
- methodizeClass 两种情况都会调用 一个是main函数之前 一个是消息发送之慢速查找。
- 在mehodizeClass里 方法序列化 为了 在慢速查找中的二分查找, 有附加分类 方法 ,主要是将 rwe的初始化(从ro中获取主类初始值)、将主类的 methods、 properties、 protocols 等添加到 rwe中, 其帖进去的方法核心为 attachLists函数算法
类的加载源码解析分析图
类加载处理.jpg上面我们已经大概理解了类是如何从Mach-0 加载到内存中,下面我们来详细了解 分类 如何加载到类中,以及 类 + 分类搭配使用情况
分类的本质
通过 clang 以及 苹果官方文档 及 objc源码 我们都可以了解到分类的本质就是一个结构体 截屏2020-10-22 下午6.15.17.png- instanceMethods: 实例方法列表
- classMethods: 类方法列表
- protocols: 协议表
- instanceProperties: 实例属性 表
- _classProperties : 类属性表 ; 拓: @property(class,nonatomic,copy) NSString * classPropertie
- methodsForMeta() : 获取方法列表 判断是否是元类 还是 类。(因为我们知道 实例方法在类中,类方法在元类中)
- propertiesForMeta() 获取元类或者类的属性 表
- protocolsForMeta() 获取 元类 或者 类的 协议表 (从这里可以看出协议只存在类对象中)
分类的调用时机
上面的流程分析了 attachCategories的源码干了点什么事情它主要是 是否存才 rwe 不存在 初始化rwe 及 将分类的信息 帖到 rwe中 下面是它的流程
realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories
上面支线我们已经跑通 下面来全局搜索 attachCategories 看哪里还用到了它经过我们的反推有了以下流程
load_images -> loadAllCategories -> load_categories_nolock -> attachCategories
示意图
截屏2020-10-22 下午6.29.28.png
-
realizeClassWithoutSwift 方法 (已知)
懒加载类的时候 是 由 lookUpImpOrForward 发起的调用
非懒加载类的时候 是由 map_images 发起的 -
attachCategories 方法
- 首先必要条件是研究的这个类存在分类(已知)
- attachToClass 是由realizeClassWithoutSwift掉起的 什么情况下才会掉起 attachCategories ?
- 既然 有懒加载类 和 非懒加载 区别 那么 分类是不是也有?
- 通过反推查看objc源码 得知 load_images 也可以掉起 attachCategories?什么时机掉起的?
-
带着上面已知和未知的问题 就引出了下面要讲解的 类 和 分类搭配到的加载
类和分类搭配加载
首先配置环境主类 LGPerson
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, assign) int kc_age;
- (void)kc_instanceMethod1;
- (void)kc_instanceMethod3;
- (void)kc_instanceMethod2;
+ (void)kc_sayClassMethod;
@end
NS_ASSUME_NONNULL_END
#import "LGPerson.h"
@implementation LGPerson
+ (void)load{
}
- (void)kc_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)kc_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)kc_sayClassMethod{
NSLog(@"%s",__func__);
}
@end
分类1 LGPerson+LGA
#import "LGPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson (LGA)
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end
NS_ASSUME_NONNULL_END
#import "LGPerson+LGA.h"
@implementation LGPerson (LGA)
+ (void)load{
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_2{
NSLog(@"%s",__func__);
}
- (void)cateA_1{
NSLog(@"%s",__func__);
}
- (void)cateA_3{
NSLog(@"%s",__func__);
}
@end
分类2 LGPerson+LGB
#import "LGPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson (LGB)
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
NS_ASSUME_NONNULL_EN
#import "LGPerson+LGB.h"
@implementation LGPerson (LGB)
+ (void)load{
}
- (void)kc_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateB_2{
NSLog(@"%s",__func__);
}
- (void)cateB_1{
NSLog(@"%s",__func__);
}
- (void)cateB_3{
NSLog(@"%s",__func__);
}
@end
1.非懒加载类 + 非懒加载分类
-
首先 在 realizeClassWithoutSwift 写下针对性研究LGPerson类的代码 并在里断点 看其调用堆栈
截屏2020-10-23 下午12.56.46.png -
继续 在methodizeClass 写下针对性代码 并在里断点 看其调用堆栈
截屏2020-10-23 下午1.03.22.png - 继续 在attachToClass 写下针对性代码 并在里断点 看其调用堆栈
-
在下面打上新的断点 并 在 attachCategories里到打下断点
截屏2020-10-23 下午1.18.36.png
截屏2020-10-23 下午1.20.39.png
过断点 发现 上面我们标红区域并没有走 但是确实也来到了 attachCategories方法 确实也是LGPerson 我们仔细看堆栈情况
截屏2020-10-23 下午1.26.31.png
(1). 此时发现 map_image掉起的流程已经结束 并没有调用 attachCategories。流程: map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass ->attachToClass ->结束
(2).而是由 load_image发起的调用流程:load_image -> loadAllCategories ->load_categories_nolock -> attachCategories
(3).此时 rwe 有了值 我们bt 打印其 methods 发现其正是 对 ro->methods的数据拷贝 对于extAllocifNeeded()我们上面 也已经详细分析过了。
在下面继续断点跟踪
截屏2020-10-23 下午2.07.18.png
1.发现 cats_count =1 此时循环一次 分类的名字 为LGA 将分类中的方法 存入mists 的最后一位 上面有对此详细的解答
- 将分类的方法 进行排序 并将 分类的方法存入到 rwe中 通过 attachLists方法 上面我们也有对attachLists算法的详细解答
我们断过上面 来到 1399 1400 方法 并将 1367 和1399 1400 断点取消
截屏2020-10-23 下午2.21.37.png
继续过断点操作 发现此时断点又断回了attachCategories 中的针对性代码区域
截屏2020-10-23 下午2.25.40.png
继续打开 1367 1399 1400 向下走
截屏2020-10-23 下午2.30.11.png
1.发现 cats_count =1 此时循环一次 分类的名字 为LGB 将分类中的方法 存入mists 的最后一位
- 将分类的方法 进行排序 并将 分类的方法存入到 rwe中 通过 attachLists方法
继续取消 1367 和1399 1400 断点 向下走 发现又回到了 realizeClassWithoutSwift
截屏2020-10-23 下午2.42.20.png
断点到针对性代码下面
截屏2020-10-23 下午2.46.18.png
继续过断点 发现再此处 cls->isRealized() 判断了一下是否实现 实现返回cls 并没有向下执行 又回到了 realizeClassWithoutSwift方法对性代码里
截屏2020-10-23 下午2.49.29.png
继续过断点 从判断 cls->isRealized() 直接返回cls 整个流程跑完了程序进入了main函数
截屏2020-10-23 下午2.53.34.png
注:删掉其他断点留下针对性调试的断点 我们下面还会用哦
总结:非懒加载类+ 非懒加载分类 : 首先map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass ->attachToClass ->结束
在这条支线上并未对 分类进行什么操作 只是完成了类的加载
然后发起 load_image函数的回调 -> loadAllCategories ->load_categories_nolock -> attachCategories (在这里有多次调用有几个非懒分类会调用几次)将分类的数据贴到 rwe中,并再次发起了 realizeClassWithoutSwift(有几个非懒分类会调用几次)的调用 来判断类是否已经实现。
2.非懒加载类 + 懒加载分类
-
下面我们将源码中 所有分类的load都去掉 使分类变为懒加载概念 并运行 来到 realizeClassWithoutSwift 此时是由 map_images发起的调用
截屏2020-10-23 下午3.49.37.png -
继续过断点 来到methodizeClass
截屏2020-10-23 下午3.51.04.png -
继续过断点 来到attachToClass
截屏2020-10-23 下午3.54.36.png -
将下面断点打开 并运行 发现并没有调用 attachCategories 中断
截屏2020-10-23 下午3.58.46.png
总结: 非懒加载类 + 懒加载分类 map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass ->attachToClass ->结束
3.懒加载类 + 懒加载分类
-
下面将主类和分类里所有的load方法全部去掉 使其变为所谓的懒加载 运行 来到 realizeClassWithoutSwift
截屏2020-10-23 下午4.04.30.png
发现此时的 realizeClassWithoutSwift 是由消息慢速查找流程里的lookUpImpOrForward掉起的。这说明在第一次消息 调用时候加载数据
-
我们继续向下走 来到methodizeClass 方法
截屏2020-10-23 下午4.11.13.png -
继续向下走 来到attachToClass 方法
截屏2020-10-23 下午4.12.39.png -
打开下图所示断点 看是否可以进去调用 attachCategories
截屏2020-10-23 下午4.14.27.png - 发现并未进去 此时运行完毕
总结流程: lookUpImpOrForward -> realizeClassMaybeSwiftMaybeRelock ->realizeClassWithoutSwift->methodizeClass->走完
4.懒加载类 + 非懒加载分类 (经测试 一个类拥有多个分类的情况下只要有大于等于两个分类实现load就会走下面流程)
-
下面将主类去掉 load ,分类全部加上load 我们再次分析
运行发现奔溃 去掉 针对性代码的元类判断 我们根据断点打印查看当前是否为 LGPerson
截屏2020-10-23 下午4.27.26.png -
继续运行 来到 realizeClassWithoutSwift
截屏2020-10-23 下午4.29.32.png
我们看调用栈 realizeClassWithoutSwift 是由 load_images发起的
-
继续运行来到 methodizeClass
截屏2020-10-23 下午4.34.27.png -
继续运行来到 attachToClass
截屏2020-10-23 下午4.36.45.png -
打开下面断点看其是否可以进去
截屏2020-10-23 下午4.38.22.png -
继续运行 它进来了
截屏2020-10-23 下午4.39.34.png
*继续过断点 来到了 attachCategories
截屏2020-10-23 下午4.40.52.png
-
继续向下走
截屏2020-10-23 下午4.43.31.png
(1) .此时 cats_count = 2 因为两个分类 (还记得 非懒加载类+非懒加载分类 时 cats_count是=1 而是 调用了两遍 attachCategories方法,也就是 调用一次 初始化一个64位的栈 并将方法 分类方法存入此64位的最后一位)
(2) .此时 for循环2遍 拿到 两个分类的 mlist 存入 mlists[64 - ++mcount ] 先进来的放在最后面 第二次进来的放在倒数第二未
(3).分类方法排序prepareMethodLists
(4). 通过attachLists方法 将mlists插入 rwe的方法列表
总结
- 懒加载类 + 非懒加载分类 调用 load_image函数的回调 -> prepare_load_methods ->realizeClassWithoutSwift ->methodizeClass -> attachToClass -> 并进入到 分类附加方法里 调用->attachCategories -> for循环 分类个数次数 ; 分类方法mlist 存入 mlists[64 - ++mcount ] 先进来的放在最后面 第二次进来的放在倒数第二 -> 1.将mlists方法 序列化 ;2.通过attachLists算法将 分类的方法 贴到rwe之中 -> realizeClassWithoutSwift 判断是否实现了cls实现返回 进入 main函数
懒加载类 + 非懒加载分类 + 懒加载分类(这种情况多个分类的情况下 有且只有一个分类为非懒加载才会走下面)
这种情况当由主类没有实现load 分类 两个其中的一个实现load 另一个不实现
-
运行发现断到realizeClassWithoutSwift是由 map_images掉起的
截屏2020-10-23 下午5.23.48.png -
过断点来到 methodizeClass
截屏2020-10-23 下午5.26.35.png -
过断点来到 attachToClass
截屏2020-10-23 下午5.27.29.png -
继续向下过断点 看是否会进入到下边
截屏2020-10-23 下午5.28.41.png
*继续向下过断点 发现并未进入到 下方 attachCategories 取消断点继续运行 进入到了main
截屏2020-10-23 下午5.32.53.png总结:懒加载类 + 非懒加载分类 + 懒加载分类 流程 map_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ->进入main并未发现 attachCategories的调用
最后总结
我们将这几种情况的调用流程拿出来
1.非懒加载类 + 非懒加载分类
map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass
load_image -> loadAllCategories ->load_categories_nolock -> attachCategories
2.非懒加载类 + 懒加载分类
map_image -> _read_images() -> realizeClassWithoutSwift -> methodizeClass
3.懒加载类 + 懒加载分类
lookUpImpOrForward -> realizeClassMaybeSwiftMaybeRelock ->realizeClassWithoutSwift->methodizeClass
4.懒加载类 + 非懒加载分类(多个分类大于等于两个实现load流程)
load_image -> prepare_load_methods ->realizeClassWithoutSwift ->methodizeClass -> attachToClass ->attachCategories
5.懒加载类 + 非懒加载分类 + 懒加载分类(多个分类有且只有一个分类实现load)
map_images ->_read_images()-> realizeClassWithoutSwift -> methodizeClass
可见load的非必须不要随意用 ,因为 在main函数之前会做很多很多事情。
截屏2020-10-23 下午5.43.43.png
网友评论