在 Clang 编译 Objective-C 源文件时,需要先将 Objective-C 代码转化为 C 语言代码,然后编译得到目标文件(object),最后将目标文件链接为二进制文件(binary)。二进制文件表现为应用或框架、类库等。操作系统通过dyld
提供的 API 启动或加载二进制文件。大致过程是:首先通过open(...)
函数打开二进制文件;然后通过mmap(...)
内存映射函数,将二进制文件中的目标文件映射到内存空间,因此目标文件也可以称为镜像文件,镜像文件所映射的内存区域则为镜像(image);最后进行镜像绑定、依赖初始化等其他操作。
本文的主要内容是:探讨 runtime 如何读取镜像中定义的 Objective-C 元数据。
注意:工程编译生成的动态链接库(Overview of Dynamic Libraries、深入剖析iOS动态链接库)、静态链接库、可执行程序统称为二进制文件(binary)。
一、Objective-C 体系初始化
面向对象的 Objective-C 体系的总加载入口在objc-os.mm
文件中的void _objc_init(void)
。操作系统执行了_objc_init()
后,才能开始加载 Objective-C 编写的应用。
void _objc_init(void)
用于初始化 runtime 环境,并注册三个回调 以监听dyld
加载镜像的两个状态、以及卸载镜像动作。开头的三行代码说明该函数只被执行一次。操作系统调用dyld
的 API 加载(dyld开源代码)二进制文件并完成内存映射后,一旦镜像切换到指定状态 或者 监听到镜像卸载动作,则会触发_objc_init
中所注册的相应的回调函数:
镜像切换到
dyld_image_state_bound
(镜像完成绑定)时,触发map_images(...)
回调函数,将镜像中定义的 Objective-C 元数据(类、分类、协议等等)加载到 runtime;镜像加载切换到
dyld_image_state_dependents_initialized
(镜像的依赖库完成初始化)时,触发load_images (...)
回调函数,执行镜像中的 Objective-C 元素初始化操作,主要是执行类和分类的load
方法;监测到卸载镜像动作时,触发
unmap_image (...)
回调函数,将属于该镜像的元素从 runtime 系统记录的已加载 Objective-C 元素中移除。
// 运行时环境的启动总入口
void _objc_init(void)
{
// 重要:_objc_init全局只调用一次
static bool initialized = false;
if (initialized) return;
initialized = true;
// 运行环境初始化系列操作
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// 监听镜像加载卸载,注册回调
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
二、载入镜像
载入镜像是由系统或应用调用dyld
的 API 实现的,例如用户打开 APP 则会触发载入镜像过程。镜像载入过程需要完成目标文件映射到内存空间、依赖库初始化等工作。而本章仅介绍内存映射过程生成的镜像的内存布局。使用 Clang 编译.m
源文件,所生成的.o
目标文件包含以下三大块信息,依次相邻分布:
- 镜像元数据区:包含头信息(header info)、架构相关信息
fat_header
(针对多架构编译的目标文件才有); - 加载命令区:加载命令(load commands),包含了镜像中保存的数据的元数据,加载命令用于访问或操作目标文件中的数据。例如
LC_SEGMENT_64
用于读取目标数据段中保存的数据; - 数据区:包括代码段(text segment)、数据段(data segment)、符号表(symbol table)等等。其中,代码段顾名思义用于保存代码,目标文件中保存的代码是机器码。数据段用于保存 在代码中初始化的全局静态变量及全局变量,这些数据固化在镜像文件中。
下图是一个.o
目标文件的结构示例:
注意:关于目标文件的结构的详细介绍,可以参考 认识MachO。
2.1 镜像的内存结构
目标文件映射到内存生成镜像,由于操作系统的 CPU 架构通常是单一的,因此镜像基本只包含单一架构的数据。以下是目标文件映射到内存生成的镜像的内存结构示意图。图中左边表示镜像内存的布局结构,从下至上为低地址到高地址。右边的mach_header_64
结构体定义了镜像头信息的数据结构;load_command
结构体表示抽象的加载命令类型,通常镜像中不会直接保存load_command
,而是继承了load_command
的具体加载命令类型,例如:segment_command_64
用于加载镜像中 segment 的数据;section_64
结构体用于记录 segment 中具有特定含义的一块数据(section)的元信息。
- 红色区域表示保存
mach_header
结构体; - 绿色区域表示保存
load_command
结构体,加载数据段; - 黄色区域表示保存
section_64
结构体; - 紫色区域表示保存了一块代码段数据(section);
- 洋红色区域表示保存了一块常量数据段数据(section);
- 青色区域保存保存了一块数据段数据(section);
2.1 读取镜像中的数据
上图中segment_command_64
中红色标记的成员,以及section_64
中绿色标记的成员,它们是读取镜像中数据的关键。
segment_command_64
结构体中:
-
cmdsize
:具体的加载命令结构体类型 所占用字节数; -
fileoff
:数据段(segment)的数据区在镜像内存空间数据区中的相对地址; -
filesize
:数据段(segment)的数据区占用字节数; -
nsect
:表示数据段中包含的 data section 的数量;
section_64
结构体中:
-
size
:数据段 data section 占用字节数; -
offset
:数据段 data section 在镜像内存空间数据区中的相对地址。
读取镜像的数据段(segment)的代码如下。其中head
参数,指向镜像的头信息结构体,即指向镜像文件映射的内存缓冲区,segname
为数据段名称。其原理是:
- 首先通过
头信息内存地址 + sizeof(struct mach_header_64)
获取加载命令区的起始地址; - 然后遍历加载命令区中所有的加载命令,当遍历到的加载命令
sgp
为LC_SEGMENT_64
,且加载命令的数据段名称segname
匹配时,返回该数据段,反之则通过sgp = sgp->cmdsize
跳过命令占用空间以遍历到下一个加载命令。
static const segment_command_64 *
getsegbynamefromheader(const mach_header_64 *head, const char *segname)
{
const segment_command_64 *sgp;
unsigned long I;
sgp = (const segment_command_64 *) (head + 1);
for (i = 0; i < head->ncmds; i++){
if (sgp->cmd == LC_SEGMENT_64) {
if (strncmp(sgp->segname, segname, sizeof(sgp->segname)) == 0) {
return sgp;
}
}
sgp = (const segment_command_64 *)((char *)sgp + sgp->cmdsize);
}
return NULL;
}
读取数据段中的 data section 数据的模拟实现代码如下,该函数定义在<mach-o/getsect.h>
头文件中,但是没有公布实现代码,以下是根据目标文件内存布局模拟的实现逻辑。原理是:
- 首先通过
getsegbynamefromheader(...)
获取目标数据段sgp
; - 然后计算数据区起始地址
datastart = mhp + sizeofheader + sizeofcmds
,表示数据区紧接在 头信息 和 加载命令区 后面; - 然后遍历
sgp
中所有 data section 的元数据sectptr
。当sectname
匹配时返回datastart + sectptr->offset
,表示 data section 的数据区域的起始地址;反之则通过sectptr += sizeof(struct section_64)
以遍历到下一个 data section 的元数据;
extern uint8_t *getsectiondata(
const struct mach_header_64 *mhp,
const char *segname,
const char *sectname,
unsigned long *size){
// 1. 获取目标数据段
const segment_command_64 *sgp = getsegbynamefromheader(mhp, segname);
if (!sgp)
return NULL;
uint_32t sizeofcmds = mhp->sizeofcmds;
uint_32t sizeofheader = sizeof(struct mach_header_64);
uint8_t * datastart = mhp + sizeofheader + sizeofcmds; // 计算数据区起始地址
// 2. 获取目标数据段首个 data section 元数据(section_64结构体)的起始地址
uint8_t * sectptr = sgp + sizeof(struct segment_command_64);
// 3. 遍历目标数据段的所有 data section
for(int i = 0; i < sgp->nsects; i++) {
if (strncmp(sectptr->sectname, sectname, sizeof(sectptr->sectname)) == 0) {
if(size > sectptr->size)
return NULL; // 获取字节数超出 section 数据范围应该给出错误提示
uint8_t *sectdataptr = datastart + sectptr->offset;
return sectdataptr;
}
sectptr += sizeof(struct section_64); // 偏移到下一个
}
return NULL;
}
三、镜像内容映射到内存 map_images
载入镜像后进入特定的状态将触发_objc_init(...)
初始化 Objective-C 运行环境时注册的三个回调。完成镜像绑定后将触发map_images
回调,加载镜像中定义的 Objective-C 元素。传入回调的参数包括:
- 待处理镜像头信息的数组
const struct mach_header * const mhdrs[]
; - 镜像的路径
const char * const paths[]
; - 镜像头信息数组的长度
unsigned count
。
map_images(...)
函数直接调用了map_images_nolock (...)
函数,该函数包含很多底层知识点,非常晦涩,这里并没有深入分析,总结其功能为:
- 预处理优化初始相关,该部分逻辑公开的细节很少可以忽略;
- 通过
addHeader(...)
汇总镜像中的 Objective-C 元素,生成head_info
的数组,作为下一步的输入; - 调用
_read_images(...)
加载镜像中的 Objective-C 元素,如类、分类、协议、消息等等。
第3点是其核心功能。以下是map_images(...)
及map_images_nolock(...)
的源代码,代码中标注了“核心逻辑”的部分将在后文分别详细介绍。
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);
}
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
static bool firstTime = YES;
header_info *hList[mhCount];
uint32_t hCount;
size_t selrefCount = 0;
// 预优化环境初始化
if (firstTime) {
preopt_init();
}
hCount = 0; // 所有 包含Objective-C元素的镜像
int totalClasses = 0; // 类的总数
int unoptimizedTotalClasses = 0; // 未优化的类的总数
{
// 遍历镜像中所有元素信息,逐一转换成header_info,并添加到hList
uint32_t i = mhCount;
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[I];
// (核心逻辑1)addHeader内部逻辑将headerType转化为header_info类型并添加到一张全局的
// 链表中,返回header_info类型的转化结果
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) continue;
if (mhdr->filetype == MH_EXECUTE) {
// 一些可执行文件的初始化代码
#if __OBJC2__
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
#else
_getObjcSelectorRefs(hi, &selrefCount);
#endif
// 忽略GC兼容检查代码
...
}
hList[hCount++] = hi;
}
}
if (firstTime) {
//初始化一些最基本的选择器,如alloc、dealloc、initialize、load等等
sel_init(selrefCount);
// 初始化AutoreleasePool和SideTable
arr_init();
// 忽略GC兼容检查代码
...
// 忽略针对MAC OS X平台的initialize的fork安全检查代码(Fixme: 不懂)
...
}
if (hCount > 0) {
// (核心逻辑2)加载二进制文件中的元素,包括类、分类、协议等等
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
void sel_init(size_t selrefCount)
{
// 保存到SelrefCount静态的int中,只是为了后面初始化namedSelectors哈希表时提供初始size
SelrefCount = selrefCount;
#if SUPPORT_PREOPT
// 预处理优化Selector,忽略
builtins = preoptimizedSelectors();
if (PrintPreopt && builtins) {
uint32_t occupied = builtins->occupied;
uint32_t capacity = builtins->capacity;
}
#endif
// sel_registerNameNoLock用于将SEL添加到全局的namedSelectors哈希表中,方便系统
// 快速判断某个SEL是否可识别
#define s(x) SEL_##x = sel_registerNameNoLock(#x, NO)
#define t(x,y) SEL_##y = sel_registerNameNoLock(#x, NO)
mutex_locker_t lock(selLock);
s(load);
s(initialize);
t(resolveInstanceMethod:, resolveInstanceMethod);
t(resolveClassMethod:, resolveClassMethod);
t(.cxx_construct, cxx_construct);
t(.cxx_destruct, cxx_destruct);
s(retain);
s(release);
s(autorelease);
s(retainCount);
s(alloc);
t(allocWithZone:, allocWithZone);
s(dealloc);
s(copy);
s(new);
t(forwardInvocation:, forwardInvocation);
t(_tryRetain, tryRetain);
t(_isDeallocating, isDeallocating);
s(retainWeakReference);
s(allowsWeakReference);
#undef s
#undef t
}
void arr_init(void)
{
AutoreleasePoolPage::init(); //自动释放池初始化,后续独立文章介绍
SideTableInit(); //SideTable初始化,后续独立文章介绍
}
3.1 核心逻辑 addHeader
addHeader
的作用不仅仅在于返回headerType
对应的header_info
结构体,其内部调用了appendHeader()
将所有已加载的镜像的元素所对应的header_info
串联成链表结构,链表首节点为FirstHeader
、末尾节点为LastHeader
。该链表在后面卸载镜像元素时需要用到。至此完成镜像数据收集工作。
3.1.1 addHeader 所收集数据 header_info
addHeader
所收集的信息header_info
结构体的功能如下:
- 用于快速定位镜像中的头信息(
mach_header_64
结构体)、镜像信息。镜像信息用于标记镜像的属性,以objc_image_info
结构体格式保存于 镜像文件的__DATA
数据段的__objc_imageinfo
data section,镜像信息在构建镜像文件时生成; - 包含
header_info_rw rw_data[]
用于记录镜像以及镜像中的 Objective-C 元素的状态,该成员的数据是可读写的。
镜像具体包含的状态见header_info_rw
结构体:
- 最低位
isLoaded
标记镜像是否已加载; - 次低位
allClassesRealized
标记镜像中所有的 Objective-C 类是否已完成 class realizing; - 其他位用于保存下一个
header_info
节点的地址,由于地址的最低 3 位必为0
因此实际上仅需保存最低 3 位之外的位即可,又因为header_info_rw
仅用了最低 2 位作为特殊标记位,因此指定当前header_info* curHeader
指向下一个节点header_info* nextHeader
时,仅需设置((header_info_rw*)&curHeader->rw_data[0])->next = nextHeader >> 2
。
typedef struct header_info {
private:
// 保存镜像中的头信息mach_header_64相对当前header_info的内存地址偏移
intptr_t mhdr_offset;
// 保存镜像中的镜像信息相对当前header_info
// 的内存地址偏移
intptr_t info_offset;
public:
// 公开的API
...
private:
// 虽然定义为数组,但是包含的元素基本为1个,不会超过1个。
header_info_rw rw_data[];
} header_info;
typedef struct header_info_rw {
// 公开的API
...
private:
#ifdef __LP64__
uintptr_t isLoaded : 1;
uintptr_t allClassesRealized : 1;
uintptr_t next : 62;
#else
uintptr_t isLoaded : 1;
uintptr_t allClassesRealized : 1;
uintptr_t next : 30;
#endif
} header_info_rw;
typedef struct objc_image_info {
uint32_t version; // currently 0
uint32_t flags;
#if __cplusplus >= 201103L
private:
enum : uint32_t {
IsReplacement = 1<<0, // used for Fix&Continue, now ignored
SupportsGC = 1<<1, // image supports GC
RequiresGC = 1<<2, // image requires GC
OptimizedByDyld = 1<<3, // image is from an optimized shared cache
CorrectedSynthesize = 1<<4, // used for an old workaround, now ignored
IsSimulated = 1<<5, // image compiled for a simulator platform
HasCategoryClassProperties = 1<<6, // class properties in category_t
// not yet used = 1<<7
// Swift版本兼容相关,忽略
...
};
public:
// Swift版本兼容相关,忽略
...
public:
// 公开的API,忽略
...
#endif
} objc_image_info;
3.1.2 addHeader 具体处理流程
具体处理流程及细节注释在下列源代码中。
#define SEG_OBJC "__OBJC"
header_info *FirstHeader = 0;
header_info *LastHeader = 0;
int HeaderCount = 0;
static header_info * addHeader(const headerType *mhdr, const char *path, int &totalClasses, int &unoptimizedTotalClasses)
{
header_info *hi;
if (bad_magic(mhdr)) return NULL; // 校验magic,忽略
bool inSharedCache = false;
hi = preoptimizedHinfoForHeader(mhdr); // 预处理优化相关,忽略
if (hi) {
// 预处理优化相关,忽略
// 在 dyld shared cache 中查找到mhdr
// 不允许重复加载
if (hi->isLoaded()) return NULL;
inSharedCache = true;
hi->setLoaded(true);
}
else
{
// 在 dyld shared cache 中未查找到mhdr
// 1. 不允许重复添加header_info到链表。getNext()用于获取下一个header_info节点
for (hi = FirstHeader; hi; hi = hi->getNext()) {
if (mhdr == hi->mhdr()) return NULL;
}
size_t info_size = 0;
unsigned long seg_size;
const objc_image_info *image_info = _getObjcImageInfo(mhdr,&info_size); // 获取镜像信息
const uint8_t *objc_segment = getsegmentdata(mhdr,SEG_OBJC,&seg_size); // 获取__OBJC数据段
// 2. 若__OBJC数据段、镜像信息为空,则直接返回NULL
if (!objc_segment && !image_info) return NULL;
// 3. 分配内存header_info结构体需要占用的内存空间,为header_info结构体占用字节数、header_info_rw
// 结构体的占用字节数之和,因为header_info的rw_data数组仅包含1个header_info_rw元素
hi = (header_info *)calloc(sizeof(header_info) + sizeof(header_info_rw), 1);
// 4. 设置 header_info 中用于定位镜像头信息的 mhdr_offset
hi->setmhdr(mhdr);
// 5. 设置 header_info 中用于定位镜像信息的 info_offset
static const objc_image_info emptyInfo = {0, 0};
hi->setinfo(image_info ?: &emptyInfo);
// 6. 设置镜像isLoaded为YES,表示镜像已加载
hi->setLoaded(true);
// 7. 设置镜像allClassesRealized为NO,表示镜像中定义的类尚未开始class realizing
hi->setAllClassesRealized(NO);
}
{
// 8. 统计镜像中包含的类的总数
size_t count = 0;
if (_getObjc2ClassList(hi, &count)) {
totalClasses += (int)count;
if (!inSharedCache) unoptimizedTotalClasses += count;
}
}
// 9. 将构建的header_info添加到全局的已加载镜像链表,添加到链表末尾
appendHeader(hi);
return hi;
}
void appendHeader(header_info *hi)
{
HeaderCount++;
hi->setNext(NULL);
if (!FirstHeader) {
// 链表为空
FirstHeader = LastHeader = hi;
} else {
if (!LastHeader) {
// 找到最后一个节点
LastHeader = FirstHeader;
while (LastHeader->getNext()) LastHeader = LastHeader->getNext();
}
// 添加到最后一个节点后面
LastHeader->setNext(hi);
LastHeader = hi;
}
}
objc_image_info *
_getObjcImageInfo(const headerType *mhdr, size_t *outBytes)
{
return getDataSection<objc_image_info>(mhdr, "__objc_imageinfo",
outBytes, nil);
}
3.2 核心逻辑 _read_images
_read_images
根据前面生成的header_info
结构体的数组,加载镜像中定义的 Objective-C 元素,如类、分类、协议。下面的代码非常长,将其分为几个部分详细分析。从代码注释中可以清晰地知道其处理流程。
// 加载镜像中的 Objective-C 元素
void _read_images(header_info **hList, uint32_t hCount)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t I;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertLocked();
#define EACH_HEADER \
hIndex = 0; \
crashlog_header_name(nil) && hIndex < hCount && (hi = hList[hIndex]) && crashlog_header_name(hi); \
hIndex++
// 1. 首次运行的初始化配置,注意该逻辑块内的代码全局只执行一次
if (!doneOnce) {
doneOnce = YES;
// 1.1 配置 isa 类型支持相关,忽略
...
// 1.2 配置 tagged pointer 支持相关,忽略
...
// 1.3 初始化gdb_objc_realized_classes
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotal : total) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
// 1.4 初始化allocatedClasses哈希表用于保存已完成内存分配的类及元类,完成内存
// 分配是指完成类的class_rw_t内存分配
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
}
// 2. 发现类,代码见<3.2.1>
...
// 3. 类重映射,代码见<3.2.2>
...
// 4. 获取__objc_selrefs数据section中定义的选择器,添加到namedSelectors哈希表,忽略
...
#if SUPPORT_FIXUP
// 5. 获取__objc_msgrefs数据section中定义的消息,添加消息的选择器到namedSelectors
// 哈希表,若其IMP指向预定义的IMP,则需要保证其指向新版本runtime定义的IMP,忽略
...
#endif
// 6. 发现协议,忽略
...
// 7. 协议排序,忽略
...
// 8. 认识非懒加载类,代码见<3.2.3>
...
// 9. 认识懒加载类,代码见<3.2.4>
...
// 10. 发现分类,代码见<3.2.5>
...
#undef EACH_HEADER
}
3.2.1 发现类
_read_images(...)
中以下代码用于发现类。实际上是构建 future class 的实体类(remapped future class/resolved future class)的过程,可以简称为 future class 解析(future class resolving)。具体步骤是:
- 首先,通过
_getObjc2ClassList(...)
读取header_info
指向的镜像中的__objc_classlist
数据 section,以提取镜像中定义的所有类; - 然后,调用
readClass(...)
读取镜像中对应 future class(保存在全局的future_named_class_map
哈希表中)的类的元数据,构建 future class 的实体类(remapped future class),并将实体类添加到remappedClasses
全局哈希表中; - 最后,若
readClass(...)
返回的类不等于传入的类,则说明该类已被重映射,将返回的类保存到已解析 future class 局部变量resolvedFutureClasses
中。
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
if (! mustReadClasses(hi)) {
// 若镜像有经过优化,则不需要调用readClass()
continue;
}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[I];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
// 若readClass返回的类非空且不等于直接从header_info读出来的类则该类为future class
if (newCls != cls && newCls) {
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
3.2.2 类重新映射
_read_images(...)
中以下代码用于类重映射。若发现类阶段构建了 future class 的实体类,则镜像中 原指向 future class 的类引用 以及父类引用 需要指向实体类,这就是类重映射过程。具体过程如下:
- 调用
_getObjc2ClassRefs(...)
函数获取镜像中__objc_classrefs
数据 section 中保存的所有类引用; - 遍历所有类引用:调用
remapClassRef(Class *clsref)
重映射类引用。内部逻辑是当检测到remppedClass
哈希表内Key为*clsref
的Value值newCls
不等于*clsref
时,表示该类需要重新映射,将clsref
指向newCls
; - 调用
_getObjc2SuperRefs(...)
函数获取镜像中__objc_superrefs
数据 section 中保存的所有父类引用; - 遍历所有类引用:调用
remapClassRef(Class *clsref)
重映射父类引用。
// 若发现类阶段有处理 future class
if (!noClassesRemapped()) {
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
// 重新映射镜像中定义的类的引用
remapClassRef(&classrefs[I]);
}
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
// 重新映射镜像中包含的 super
remapClassRef(&classrefs[I]);
}
}
}
3.2.3 认识非懒加载类
完成 future class 解析和类重映射后,需要加载的类的内存地址即可确定。此时需要对需要加载的类进行 class realizing 以确定其class_rw_t
数据的内存地址,以及确定类的对象内存布局。非懒加载类的 class realizing 的代码如下,通过调用realizeClassWithoutSwift(...)
函数实现:
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
// 模拟器环境特殊配置,忽略
...
addClassTableEntry(cls);
// Swift环境兼容相关,忽略
...
realizeClassWithoutSwift(cls);
}
}
3.2.4 认识 resolved future class
继续进行已解析的 future class 的 class realizing 过程,代码如下:
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
realizeClass(resolvedFutureClasses[I]);
resolvedFutureClasses[i]->setRequiresRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
3.2.5 分类发现
分类中扩展类的遵循协议列表、属性列表、方法列表需要在运行时添加到类的class_rw_t
数据中,该过程必须在类的内存地址、class_rw_t
内存地址、类的对象内存布局确定后方能进行。大致过程如下:
- 调用
_getObjc2CategoryList(...)
函数获取镜像的__objc_catlist
数据 section 中保存的镜像中定义的分类列表; - 遍历分类列表;
- 若分类所扩展的类为空则报错,并直接处理下一个分类;
- 若分类扩展元素包括协议列表或实例属性或实例方法,则将调用
addUnattachedCategoryForClass(...)
将分类添加到扩展类的待处理分类哈希表,然后调用remethodizeClass(...)
将协议列表或实例属性或实例方法元素添加到扩展类的class_rw_t
数据中; - 若分类扩展元素包括协议列表或类属性或类方法,则将调用
addUnattachedCategoryForClass(...)
将分类添加到扩展类的元类的待处理分类哈希表,然后调用remethodizeClass(...)
将协议列表或实例属性或实例方法元素添加到扩展类的元类的class_rw_t
数据中;
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[I];
Class cls = remapClass(cat->cls);
if (!cls) {
// 分类所扩展的类为空则报错
catlist[i] = nil;
continue;
}
// 协议、实例方法、实例属性添加到类的class_rw_t中
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
}
// 协议、类方法、类属性添加到类的元类的class_rw_t中
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
}
}
}
四、初始化类库load_images
第三章介绍的map_images
用于汇总镜像信息,并加载其中定义的 Objective-C 元素,load_images
的逻辑基于map_images
所加载的类、分类信息进行。load_images
主要调用了两个函数:
-
prepare_load_methods(...)
:执行类、分类的load
方法进行类的初始化之前的准备工作,主要是收集镜像中实现了load方法的类和分类; -
void call_load_methods(void)
:调用类、分类中的load
方法初始化类;
prepare_load_methods
和call_load_methods
是成对存在的两个操作,前者记录实现了load
方法的类、分类,后者按正确的顺序调用所记录的类和分类的load
方法。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// 若镜像中的所有类、分类均未实现load方法,则直接返回
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// 收集镜像中实现了load方法的类和分类
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// 执行prepare_load_methods收集的类和分类的load方法
call_load_methods();
}
4.1 prepare_load_methods函数
分析prepare_load_methods
函数逻辑前,首先看两个数据结构loadable_class
、loadable_category
分别表示包含load
方法的类和分类。声明两个静态数组容器loadable_class
的数组loadable_classes
,以及loadable_category
的数组loadable_categories
,分别用于记录包含了load
方法但尚未调用load
的类和分类。prepare_load_methods
的主逻辑就是操作上述两个数组容器。
loadable_classes
数组支持动态扩容,占用内存存在冗余空间,因此不仅需要loadable_classes_used
记录loadable_classes
实际保存的loadable_class
结构体数量,还需要loadable_classes_allocated
记录为数组分配的用于保存loadable_class
结构体的内存单元数量。loadable_categories
同理。
struct loadable_class {
Class cls; // may be nil
IMP method;
};
struct loadable_category {
Category cat; // may be nil
IMP method;
};
// 数组容器,记录包含load方法的所有类的信息
static struct loadable_class *loadable_classes = nil;
//数组内存中存在冗余空间,用loadable_classes_used实际保存的单元数量
static int loadable_classes_used = 0;
//数组内存中存在冗余空间,因此用loadable_classes_allocated记录分配的单元数量
static int loadable_classes_allocated = 0;
// 数组容器,记录包含load方法的所有分类的信息
static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;
prepare_load_methods
包含两大步骤:
- 获取镜像中所有非懒加载类,依次调用
schedule_class_load(...)
函数将包含load
方法的类添加到loadable_class
数组容器。schedule_class_load(...)
函数内部,在调用add_class_to_loadable_list(cls)
将类loadable_class
数组容器前,递归调用了schedule_class_load(cls->superclass)
,以保证父类的load
方法先于子类load
方法执行; - 获取镜像中所有非懒加载分类,依次调用
add_category_to_loadable_list(...)
函数将包含load
方法的类添加到loadable_category
数组容器。注意在这之前需要保证分类所扩展的类完成 class realizing;
void prepare_load_methods(const headerType *mhdr)
{
size_t count, I;
runtimeLock.assertWriting();
// 获取二进制文件中,非懒加载的所有类的信息
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
// 获取二进制文件中,非懒加载的所有分类的信息
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[I];
Class cls = remapClass(cat->cls);
if (!cls) continue;
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
// ------- 将类添加到loadable_classes数组 ------- //
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized());
if (cls->data()->flags & RW_LOADED) return;
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return;
// loadable_classes数组扩容,因此loadable_classes数组中是存在冗余空间的,这是loadable_classes_allocated存在原因
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
// ------- 将分类类添加到loadable_categories数组 ------- //
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
if (!method) return;
// loadable_categories数组扩容
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
4.2 call_load_methods函数
分析完prepare_load_methods
的逻辑,其实call_load_methods
的逻辑已经比较明显了。就是从loadable_classes
容器及loadable_categories
容器中推出类和分类,依次调用load
方法。
call_load_methods(void)
代码的逻辑比较怪异,在do-while
循环内部的while
循环明明已经判断loadable_classes_used <= 0
,为什么在do-while
还要判断loadable_classes_used > 0
进入下一次迭代?这是因为类、分类的load
方法中,均可能存在动态加载镜像文件的逻辑,从而引入新的类、分类的load
方法。do-while
循环内部,执行类的load
方法使用了一个while
循环,而执行分类的load
方法则只调用了一次,这是因为分类load
方法必须等待其扩展类的load
方法执行完毕才能执行,因此需要立即进入下一次迭代以执行扩展类的load
方法。
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. 遍历并执行类的所有可调用的load方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. 执行分类的load方法
more_categories = call_category_loads();
// 3. 循环直到类及分类的所有load方法均被执行
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
4.2.1 执行类的load方法
由于在prepare_load_methods
已经确定了类的load
方法执行顺序,因此call_class_loads(void)
仅需简单迭代执行loadable_class
中的load
方法即可。处理过程大致如下:
- 将局部变量
classes
指向loadable_classes
,将loadable_classes
指向nil
。classes
表示本次需要执行的所有类的load
方法,为旧容器。loadable_classes
表示本次执行的类的load
方法中动态载入的所有新类的load
方法,为新容器; - 遍历
classes
中所有loadable_class
结构体,执行其method
所指向的load
方法。遍历classes
时,若load
方法中载入了新的类的load
方法,则又会被收集于loadable_classes
所指向的新容器中; - 释放
classes
局部变量所指向的旧容器内存空间;
static void call_class_loads(void)
{
int I;
struct loadable_class *classes = loadable_classes;
// loadable_classes指向nil
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
// 执行load方法。load方法可能包含动态加载镜像的逻辑,此时loadable_classes则会指向
// 新的容器来收集动态加载镜像中的load方法
(*load_method)(cls, SEL_load);
}
if (classes) free(classes); // 释放旧容器
}
4.2.2 执行分类的load方法
执行分类的load
方法的逻辑比类稍微复杂。处理过程如下:
- 局部变量
cats
指向loadable_categories
表示旧容器。loadable_categories
指向nil
表示新容器; - 遍历旧容器中的所有
loadable_category
结构体,若loadable_category
的cls
成员非空且可加载,则执行method
成员指向的load
方法,并把cat
成员置nil
; - 用
cats
收集 旧容器中未执行load
方法的所有分类(判断cat
成员非空); - 用
cats
收集 执行旧容器的load
方法过程中动态载入的所有分类; - 若
cats
保存的loadable_category
结构体数量大于0
,则设置loadable_categories
指向cats
所指向的内存空间;反之loadable_categories
置nil
。
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) { //cls->isLoadable()恒为YES
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
// 收集上面for循环未执行load方法的所有分类,其中包含了load方法中可能存在动态加载
// 镜像时载入的分类的load方法,这些load方法不能立刻执行,需要其扩展类的load方法
// 执行完毕后才能执行。
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[I];
} else {
shift++;
}
}
// loadable_categories旧容器中尚未执行load方法的loadable_category结构体数量,这些
// loadable_category均保留在loadable_categories新容器
used -= shift;
// 若loadable_categories_used大于0,说明在执行分类load方法时收集到新的分类load方法
new_categories_added = (loadable_categories_used > 0);
// 将新收集的分类load方法添加到loadable_categories新容器
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[I];
}
// 释放旧loadable_categories容器
if (loadable_categories) free(loadable_categories);
// 赋值新loadable_categories容器
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
return new_categories_added;
}
五、总结
应用加载的本质是加载应用沙盒中的可执行文件(也是目标文件、镜像文件),镜像文件可以映射到内存空间生成镜像。镜像中包含了头信息、加载命令、数据区,镜像中定义的 Objective-C 元素的元信息主要保存在
__DATA
数据段、__CONST_DATA
数据段中。例如__objc_classlist
保存镜像中定义的所有类、__objc_catlist
保存镜像中定义的所有分类;生成镜像后,触发
map_images
回调读取镜像中定义的 Objective-C 元素的元信息,主要包括发现类、发现协议、认识类、将分类中的方法添加到类、元类的class_rw_t
的方法列表中;完成镜像绑定后,触发
load_images
收集需要调用的类及分类的load
方法,其优先级是静态加载的父类>静态加载的子类>静态加载的分类>动态加载的父类>动态加载的子类>动态加载的分类;下一篇介绍 runtime 实现基于引用计数的内存管理机制。