前言
通过前三篇文章,了解了程序的加载流程,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