前言
通过前三篇文章,了解了程序的加载流程,dyld和_objc_init的对接关系,还有load_images的执行情况,还有dyld的迭代变化。
dyld链接的是镜像文件images。根据程序加载流程知道,images已经映射到应用程序里面了,但是映射过来只是对应的库,没有转变成相应的内存。我们工程,通过编译后,会形成machO(可执行文件),但是如何加载进入内存的?就是我们接下来要探索的。
准备资源
objc源码下载:多个版本的objc源码- 小板凳、冰🍺
进入正文
在objc源码中,当工程运行起来时,会执行_objc_init,然后就是相应的注册_dyld_objc_notify_register,如下图:
就是
_dyld_objc_notify_register(&map_images, load_images, unmap_image)这句代码,起到了承接作用,用简图描述下:
map_images:管理可执行文件中和动态库中所有的符号,完成class、selector、protocol、category的加载;load_images:加载执行load方法。
分析_objc_init()里面的方法
environ_init()
读取影响运⾏时的环境变量,在源码中稍微做一些修改,就能直接打印信息,如下图:
运行后,可以获得打印结果:
打印的都是
objc的相应信息。当然,还可以通过控制台,进行设置,来调整打印情况,如:
- 是否对
isa进行优化处理OBJC_DISABLE_NONPOINTER_ISA; - 是否打印
load方法OBJC_PRINT_LOAD_METHODS; 设置方法:Product-->Scheme-->Edit Scheme,就能打开设置框,在设置框里面:Run-->Arguments-->Environment Variables,在Environment Variables添加。
比如,先打印下LGPerson类的isa指针:
从打印结果看,尾数是1,代表着已经开启了指针优化处理,接着就关掉isa指针优化处理,就是在设置框里面,设置OBJC_DISABLE_NONPOINTER_ISA为YES,如下图:
再运行工程,通过lldb调试,打印isa指针的二进制:
此时的尾数就是0,代表着已经关闭了指针优化处理。
同样的方法,可以通过设置OBJC_PRINT_LOAD_METHODS,来输出调用load方法的类,如下图的打印结果:
tls_init()
关于线程key的绑定-⽐如每线程数据的析构函数,如下图:
static_init()
运⾏C++静态构造函数。在dyld调⽤我们的静态构造函数之前,libc 会调⽤ _objc_init(),因此我们必须⾃⼰做,如下图:
runtime_init()
其中,runtime运⾏时环境初始化,初始化两张表:
unattachedCategories.init(32)分类表的初始化;allocatedClasses.init()内存中类表的创建;
exception_init()
初始化libobjc的异常处理系统
当出现一个异常,会判断是否为objc异常,如果是objc异常会执行回调函数uncaught_handler。全局搜索uncaught_handler,找到回调函数设置的方法。
在OC层,可以通过调用方法NSSetUncaughtExceptionHandler设置回调函数,回调函数会被赋值给uncaught_handler。
cache_t::init()
缓存条件初始化
_imp_implementationWithBlock_init()
启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib。
_dyld_objc_notify_register()
在程序加载流程一文中,就讲解了,当程序加载到后面,会执行_objc_init函数,然后就会执行_dyld_objc_notify_register(),进行注册,相当于一个桥梁,将三个方法注册到dyld中去:
在这个函数里面,这三个方法是:
map_images:管理可执行文件中和动态库中所有的符号,完成class、selector、protocol、category的加载;load_images:加载执行load方法。unmap_image:dyld将image移除时,会触发该函数。
_read_images()分析
整体分析
map_images()
处理由dyld映射的给定镜像文件。
map_images_nolock()
- 流程:
map_images()-->map_images_nolock()-->_read_images()到了_read_images()函数,就已经到了重心处了。
// 重点
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
bool launchTime = NO;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertLocked();
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
//✅1:条件控制进⾏⼀次的加载 ---- 找到一个全网的总表
if (!doneOnce) {...}
//✅2:修复预编译阶段的`@selector`的混乱问题
//因为在每个镜像文件,同名的方法的位置是不相同的,所以要局部处理
// Fix up @selector references
// sel 名字 + 地址
static size_t UnfixedSelectors;
{...}
ts.log("IMAGE TIMES: fix up selector references");
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
//✅3:错误混乱的类处理
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: discover classes");
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.
//✅4:修复重映射⼀些没有被镜像⽂件加载进来的类
if (!noClassesRemapped()) {...}
ts.log("IMAGE TIMES: remap classes");
#if SUPPORT_FIXUP
//✅5:修复⼀些消息!
// Fix up old objc_msgSend_fixup call sites
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
//✅6:当我们类⾥⾯有协议的时候:readProtocol
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: discover protocols");
//✅7:修复没有被加载的协议
// Fix up @protocol references
// Preoptimized images may have the right
// answer already but we don't know for sure.
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: fix up @protocol references");
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes. rdar://problem/53119145
//✅8:分类处理
if (didInitialAttachCategories) {...}
ts.log("IMAGE TIMES: discover categories");
// Category discovery MUST BE Late to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.
// +load handled by prepare_load_methods()
// Realize non-lazy classes (for +load methods and static instances)
//✅9:类的加载处理
for (EACH_HEADER) {...}
ts.log("IMAGE TIMES: realize non-lazy classes");
//✅10:没有被处理的类,优化那些被侵犯的类
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {...}
ts.log("IMAGE TIMES: realize future classes");
if (DebugNonFragileIvars) {...}
// Print preoptimization statistics
if (PrintPreopt) {...}
#undef EACH_HEADER
}
- 1: 条件控制进⾏⼀次的加载;
- 2: 修复预编译阶段的
@selector的混乱问题; - 3: 错误混乱的类处理;
- 4: 修复重映射⼀些没有被镜像⽂件加载进来的类;
- 5: 修复⼀些消息;
- 6: 当我们类⾥⾯有协议的时候 :
readProtocol; - 7: 修复没有被加载的协议;
- 8: 分类处理;
- 9: 类的加载处理;
- 10: 没有被处理的类 优化那些被侵犯的类;
很显然
8和9是核心。也就是load_categories_nolock与realizeClassWithoutSwift。
局部详细解析
doneOnce 分析
-
小对象类型的处理。
-
创建一张类的总表,这个表包含所有的类,目的是方便快捷查找类。
-
表的大小也遵循负载因子,这里
namedClassesSize=totalClasses*4 / 3相当于是负载因子3/4的逆过程。namedClassesSize相当于总容量,totalClasses相当于要占用的空间。 -
根据
runtime_init()函数里面的两张表,如下图:与
gdb_objc_realized_classes的比较: -
gdb_objc_realized_classes是一张总表,无论类是否实例化。 -
allocatedClasses包含的是所有allocated的类和元类。 所以gdb_objc_realized_classes应该包含allocatedClasses。doneOnce的作用相当于是创建类的总表。
UnfixedSelectors 分析
修复预编译阶段的 @selector 的混乱问题,如下图:
因为sel = 名字 + 地址,在每个镜像文件,有同名的方法,但是这些方法位置是不相同的,所以要局部处理,可以通过打断点调试:
从调试的结果,可以看出,同样类的
retain 方法,但是地址却不一样。但是最终以 dyld 所加载的真实地址为准。
如上图所示,在我们整个系统中会有多库,如果每个库都有一个retain方法,那么在执行该方法时,需要将方法平移到程序出口的位置进行执行,那么在A库中的retain方法的地址相当于首地址, 在B库中的retain方法的地址需要平移A库的地址大小。因此,地址不同,方法需要进行平移调整。
readClass 错误混乱的类处理(核心重点)
在此部分会初始化类的名称。类已移动但未删除,对错误混乱的类进行处理,比如有片空间里面存储类,当这片空间被移动后,原始的类就要被干掉,如果没有被完全干掉,就会有残留,变得混乱,出现野指针,到了后面,才会被干掉。源码如下:
通过_getObjc2ClassList,从可执行文件machO中获取类列表,然后就能对类进行处理。可以通过调试来看下情况,如下图:
当没有执行readClass函数,此时cls还只是一个地址 0x00007fff889de040 ,当执行了readClass函数之后,cls 就变成了 __NSStackBlock__。
这说明在 readClass 函数中,应该是做了 类名 和 地址 相关的一些处理,接下来对 readClass 进行分析。
进入readClass中查看其实现:
添加的断点①和断点②是为下文做准备,我们自己先创建了一个 LGPerson 类,为了更清楚看清readClass里面做的事情,再自行添加printf("%s -KC - %s\n",__func__,mangledName);,执行打印,如下图:
能够找到我们自己创建的类 LGPerson。
当然我们也可以过滤掉其他类的打印,只打印 LGPerson类,如下图:
可以直接打印到对应的LGPerson类。可能会比较迷惑啊,这有什么用?在这里可以单独对某个类进行处理,方便跟踪。
接着断点往下走,这里就能用到刚刚设置的断点①和断点②了,如下图:
之所以这么做,查看了一些资料,说是会走3365处这个if判断块的代码,但是实际上是没有执行的。继续跟踪代码,程序会运行到addNamedClass(),通过该方法,将类名添加到已命名的非元类映射(关联类信息,加入总表 gdb_objc_realized_classes),如下图:
- 将
cls类加入gdb_objc_realized_classes总表中。总表是在doneOnce中创建的。 - 执行
addNamedClass函数后,类与地址进行关联了。其中,核心逻辑是NXMapInsert处理的。也就是插入总表的时候进行的关联。以MapPair(key-value)的形式进行关联。
接着断点往下跟踪,就执行 addClassTableEntry 函数,如下图:
- 将类和元类插入
allocatedClasses表中。这张表是在runtime_init中创建的。
remapClasses
- 通过
noClassesRemapped方法判断是否有类引用(_objc_classrefs)需要进行重映射 - 接着就是对类进行
重新映射,读取的是macho中的数据__objc_classrefs与__objc_superrefs。最终调用remapClassRef进行重新映射。
objc_msgSend_fixup
- 修复
sel的调用,比如我的第二篇博客里面,写的苹果关于alloc的hook操作,alloc的imp改为直接调用objc_alloc,而不是走alloc的实现。当然正常情况下不会走这个逻辑,在llvm阶段已经处理了。
我们再看下 fixupMessageRef 的源码,就很熟悉了,如下图:
discover categories
根据注释说明,不会进入这个逻辑(即使实现了分类的+ load也不会进入)。分类的加载必须在load_images之后。
realize non-lazy classes
非懒加载类的处理,如下图:
- 一般情况下自己实现的类是不会进入这个逻辑的(除非实现了
+ load方法)。 - 根据注释可以看到只有非懒加载类会进入这个逻辑,
nlclslist就是获取非懒加载类列表。通过macho的__objc_nlclslist获取。实现了+load方法的类会出现在__objc_nlclslist中。 - 核心就是
realizeClassWithoutSwift的初始化逻辑了。这个方法在之前的消息慢速查找流程遇见过了。
非懒加载类分为三种情况:
1.本类实现了+ load方法。
2.子类实现了+ load方法。(因为子类初始化会连带着初始化父类的)
3.分类实现了+load方法。(这里包括自己的分类以及子类的分类)
看下 machO 文件里面对应的 __objc_nlclslist 信息,如下图:
- 这就说明了尽量避免在
+ load方法中进行逻辑处理。整个过程是一个连锁反映。添加+load方法的类就会出现在__objc_nlclslist中。
为什么有懒加载和非懒加载类的区别?
因为苹果系统是按需分配的,在启动过程中初始化的类越少,那么启动速度就越快。
现在已经清楚了非懒加载类的实例化入口,那么懒加载类是在哪里实例化的呢?
既然要实例化肯定要在调用realizeClassWithoutSwift,在其中打个调试断点,把load方法去掉:
从堆栈信息可以看到,在调用 alloc 的时候进行慢速消息查找的时候实例化的。那就直接调用一个类方法,发现堆栈的信息中,执行的步骤是一样的。那么就说明了在类进行第一次发送消息的时候进行的实例化。
- 对于非懒加载类,实现了
+load方法(子类/分类/自己),类就会提前加载,为+ load的调用做准备。 - 对于懒加载类,是在第一次消息发送
objc_msgSend,进行lookUpImpOrForward消息慢速查找的时候进行初始化的。
懒加载和非懒加载对比:
-
懒加载类:数据加载推迟到第一次发送消息的时候。
lookUpImpOrForward-->realizeClassMaybeSwiftMaybeRelock-->realizeClassWithoutSwift-->methodizeClass -
非懒加载类:
map_images的时候加载所有类数据。readClass-->_getObjc2NonlazyClassList-->realizeClassWithoutSwift-->methodizeClass