OC底层原理 学习大纲
上一节,我们了解了dyld和objc的关联,但是 map_images
是如何将镜像
从macho
中映射
到内存
的呢?
本节内容:
-
_read_images
结构分析 - 类的加载(上)
2.1readClass
读取类
2.2realizeClassWithoutSwift
实现类
2.3methodizeClass
整理类 -
懒加载
和非懒加载
的区别
准备工作:
- 可编译的
objc4-781
源码: https://www.jianshu.com/p/45dc31d91000dyld-750.6
: https://opensource.apple.com/tarballs/dyld/
1. _read_images
结构分析
打开objc4
源码,找到_objc_init
, 进入map_images
:
void _objc_init(void)
{
... ...
// 注册
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
// 进入map_images
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
- 进入
map_images_nolock
,找到核心代码_read_images
读取镜像:
以下是_read_images
源码结构:
- 条件控制进行一次的加载
- 修复预编译阶段的@selector的混乱问题
- 类处理
- 修复重映射一些没被镜像文件加载进来的类
- 修复一些消息
- 当类中有协议时:readProtocol
- 修复没被加载的协议
- 分类处理
- 实现非懒加载类
- 没被处理的类(优化哪些被侵犯的类)
这里内容较多,我们先抓重点:macho
读取到内存
,最重要的是类信息
的读取
。
- 我们发现首先对
类
进行操作
的是第3步 类处理
。主要函数是readClass
类的读取:
我们进入readClass
,详细了解内部功能
2. 类的加载(上)
- 先加上
测试代码
,使用自定义HTPerson
类,便于定位
和验证
:
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *ht_name;
@property (nonatomic, assign) int ht_age;
- (void)ht_func1;
- (void)ht_func3;
- (void)ht_func2;
+ (void)ht_classFunc;
@end
@implementation HTPerson
- (void)ht_func1 { NSLog(@"%s",__func__); };
- (void)ht_func3 { NSLog(@"%s",__func__); };
- (void)ht_func2 { NSLog(@"%s",__func__); };
+ (void)ht_classFunc { NSLog(@"%s",__func__); };
@end
2.1 readClass 读取类
小技巧:
- 源码中
加入
类的判断语句
,精准定位
来分析自己的类
。
- 在
readClass
函数内加入识别
HTPerson的测试代码
,在测试代码
区域加入断点
(下图仅在3196行加入断点),运行代码:
- 代码进入断点,确认是
HTPerson
类后,我们加断点运行
(上图3227、3241行),发现没进
入类的读取和加载
区域。
我们单步执行,查看具体流程:
- 发现进入了
addNamedClass
函数。
参数
mangledName
是当前类名:
已实现
的类,从内存
读取;未实现
的类,从machO
读取
- 进入
addNamedClass
函数:
发现主要是将
类名
插入类表
中,NXMapInsert
内部有关于类名哈希
、表扩容
、插入算法
等细节介绍。此处不做拓展。我们返回上一步,查看下一个
addClassTableEntry
函数:
细节:
- 此处会将
本类
和元类
都注册
到类表
中
- 当前类表中的本类和元类,都只有
名字
、地址
,数据还没写入
- 我们发现
第3步 类处理 readClass
内部并没有完成类的加载
,仅仅将类名
和类地址
记录到类表中。
返回_read_images
中,继续寻找与类相关
的步骤
,发现第9步 实现非懒加载类
和第10步 没被处理的类(优化哪些被侵犯的类)
都调用了realizeClassWithoutSwift
函数,对类进行实现。
2.2 realizeClassWithoutSwift 实现类
我们在_read_images
函数的第9步
和第10步
分别加上测试代码:
const char * mangledName = cls->mangledName();
const char * HTPersonName = "HTPerson";
if (strcmp(mangledName, HTPersonName) == 0) {
printf("resolvedFutureClasses: 精准定位 %s \n", HTPersonName);
}
然而,尴尬的是... 都没有执行到
(倔强的我,HTPerson类
进入_read_images
后,单步断点
一步步的测试,确实发现HTPerson类
没有实现)
思考: 此处是
app启动
前,dyld
调用_objc_init
,执行map_images
,发现自定义的HTPerson
类没有加载
- 如果你在开发中用过
lazy懒加载
,就应该能联想
到苹果的设计机制
。没错,此处也是懒加载模式
。- 当类
没被调用
时,我们只会存储类名
、类地址
- 真正
被调用
时,会检查是否实现,未实现就会触发实现
。
是否是懒加载类
,关键在于是否实现
了+load方法
。 (没实现
+load方法是懒加载类
,实现了
就是非懒加载类
)
- 我们在
HTperson类
的测试代码
中加入+load
的实现:
+ (void)load { NSLog(@"%s",__func__); };
再次运行代码
,按照上述流程,发现精准定位
到了【9.实现非懒加载类】
:
- 进入
realizeClassWithoutSwift
内部:
Q:赋值后bits为何为空?
所以此刻赋值已成功
,但是还未写入内存
ro的读取 :
WWDC2020视频
Advancements in the Objective-C runtime有介绍,据苹果官方统计
,只有10%
的类需要动态修改方法
,所以为了节约内存
,没进行动态拓展的类,直接从macho
读取数据
。反之,用rwe
记录动态修改的数据,并从rwe
读取数据。
下面我们进一步探索methodizeClass
整理类
2.3 methodizeClass 整理类
此时rwe
为Null
,所有rwe
条件判断后的赋值
,都没进入
。
Q:
rwe
什么时候会被赋值
?根据
WWDC2020视频
Advancements in the Objective-C runtime,我们知道rwe
是当类的方法
被动态修改
时,才会创建
。 那什么时候会被动态修改
呢?我们下一节会分析。
在分析rwe
的创建之前,我们先详细讲解懒加载类
和非懒加载类
的区别
3. 懒加载和非懒加载的区别
懒加载类
和非懒加载类
的区别
在于:是否实现了+load
方法。
上面我们加入了
+load
方法,让HTPerson类
变成了非懒加载类
,才进入了realizeClassWithoutSwift
函数。那如果不实现
+load
方法,是懒加载类
,又是如何加载到内存的呢?
懒加载类
的加载:
- 测试代码中,
移除+load方法
的实现,加入HTPerosn
的调用
:int main(int argc, const char * argv[]) { @autoreleasepool { HTPerson * person = [HTPerson alloc]; // 加入HTPerson的调用 } return 0; }
移除
所有断点
,在realizeClassWithoutSwift
函数中,加入定位代码
和断点
:const char * HTPersonName = "HTPerson"; const char * mangledName = cls->mangledName(); if(strcmp(mangledName, HTPersonName) == 0) { auto ht_ro = (const class_ro_t *)cls->data(); auto ht_isMeta = ht_ro->flags & RO_META; if (!ht_isMeta) { printf("%s - 精准定位! - %s\n", __func__, mangledName); } }
- 运行程序,
断点
精准定位到HTPerson类
,查看左边堆栈信息
:
这个流程是否非常熟悉? 这就是之前我们详细分析过的objc_msgSend流程,
APP启动后,我们手动调用了
alloc
方法,触发消息机制
,在进入方法的慢速查找
时,我们会现检测当前类是否已实现
,如果没有实现
,就调用realizeClassWithoutSwift
进行实现。
懒加载类
与非懒加载类
总结:
- 苹果系统
默认
所有类都是懒加载类
,这样不占用启动时间
,且不占用过多资源
。
-
本节回顾:
本节我们了解了map_images
如何将镜像
从macho
映射到内存
中,分析类的加载
,了解懒加载类
与非懒加载类
的区别。
- 但是,关于
rwe
何时加载?分类
的加载方式等,很多细节我们都没有深入探索。
下一节,OC底层原理十八:类的加载(中) SEL & 分类的加载 我们将从分类
的探索开始,深入了解整个流程。
附录:
Q: 我们知道+load方法
在main函数之前
执行,那本类
和分类
的+load执行顺序
,不相干
的两个类
的执行顺序
是怎样的呢?
A:给大家分享这位博主的分析:https://blog.csdn.net/TuGeLe/article/details/86599216