上一节,我们了解了dyld和objc的关联,但是 map_images是如何将镜像从macho中映射到内存的呢?
dyld与objc关联
本节内容:
-
_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读取镜像:
image.png
以下是_read_images源码结构:
image.png
- 条件控制进行一次的加载
- 修复预编译阶段的@selector的混乱问题
- 类处理
- 修复重映射一些没被镜像文件加载进来的类
- 修复一些消息
- 当类中有协议时:readProtocol
- 修复没被加载的协议
- 分类处理
- 实现非懒加载类
- 没被处理的类(优化哪些被侵犯的类)
这里内容较多,我们先抓重点:macho读取到内存,最重要的是类信息的读取。
- 我们发现首先对
类进行操作的是第3步 类处理。主要函数是readClass类的读取:
image.png
我们进入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行加入断点),运行代码:
image.png
- 代码进入断点,确认是
HTPerson类后,我们加断点运行(上图3227、3241行),发现没进入类的读取和加载区域。
我们单步执行,查看具体流程:
image.png
- 发现进入了
addNamedClass函数。
image.png
参数
mangledName是当前类名:
已实现的类,从内存读取;未实现的类,从machO读取
image.png
- 进入
addNamedClass函数:
image.png
-
发现主要是将
类名插入类表中,NXMapInsert内部有关于类名哈希、表扩容、插入算法等细节介绍。此处不做拓展。 -
我们返回上一步,查看下一个
addClassTableEntry函数:
image.png
细节:
- 此处会将
本类和元类都注册到类表中
- 当前类表中的本类和元类,都只有
名字、地址,数据还没写入
- 我们发现
第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);
}
image.png
image.png
然而,尴尬的是... 都没有执行到 😂
(倔强的我,HTPerson类进入_read_images后,单步断点一步步的测试,确实发现HTPerson类没有实现)
思考: 此处是
app启动前,dyld调用_objc_init,执行map_images,发现自定义的HTPerson类没有加载
- 如果你在开发中用过
lazy懒加载,就应该能联想到苹果的设计机制。没错,此处也是懒加载模式。- 当类
没被调用时,我们只会存储类名、类地址- 真正
被调用时,会检查是否实现,未实现就会触发实现。
是否是懒加载类,关键在于是否实现了+load方法。 (没实现+load方法是懒加载类,实现了就是非懒加载类)
- 我们在
HTperson类的测试代码中加入+load的实现:
+ (void)load { NSLog(@"%s",__func__); };
再次运行代码,按照上述流程,发现精准定位到了【9.实现非懒加载类】:
image.png
- 进入
realizeClassWithoutSwift内部:
realizeClassWithoutSwift
Q:赋值后bits为何为空?
image.png
所以此刻赋值已成功,但是还未写入内存
ro的读取 :
image.png
WWDC2020视频Advancements in the Objective-C runtime有介绍,据苹果官方统计,只有10%的类需要动态修改方法,所以为了节约内存,没进行动态拓展的类,直接从macho读取数据。反之,用rwe记录动态修改的数据,并从rwe读取数据。
下面我们进一步探索methodizeClass 整理类
2.3 methodizeClass 整理类
123.png
此时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类,查看左边堆栈信息:image.png
这个流程是否非常熟悉? 😃 这就是之前我们详细分析过的objc_msgSend流程,
APP启动后,我们手动调用了
alloc方法,触发消息机制,在进入方法的慢速查找时,我们会现检测当前类是否已实现,如果没有实现,就调用realizeClassWithoutSwift进行实现。image.png
懒加载类与非懒加载类总结:
- 苹果系统
默认所有类都是懒加载类,这样不占用启动时间,且不占用过多资源。
image.png
-
本节回顾:
image.png
本节我们了解了map_images如何将镜像从macho映射到内存中,分析类的加载,了解懒加载类与非懒加载类的区别。
- 但是,关于
rwe何时加载?分类的加载方式等,很多细节我们都没有深入探索。
下一节,OC底层原理十八:类的加载(中) SEL & 分类的加载 我们将从分类的探索开始,深入了解整个流程。

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











网友评论