iOS类加载流程(五):read_images流程分析
read_image
方法内部大概做了这么几件事:
- 一些初始化操作;
- 映射 Class、SEL、Protocol;
- Class 的映射方式是保存编译器静态数据的指针,SEL 映射的主要形式是保存 name,Protocol??
- 映射完毕之后,对类进行实现,动态链接阶段只实现非懒加载类;
- 类实现之后,处理分类逻辑,对原来的类进行补充;
read_image
这个方法的逻辑遵循一个原则,这个原则是围绕着 objc 的核心思想:消息转发
OC 中,大部分代码的访问都是通过 objcMsgSend
或者 objcSuperMsgSend
来进行寻找和分发;
分发需要三个关键点:
- 信号(方法名);
- 转发器(objcMsgSend);
- 方法实际拥有者(类/元类)
所以,这个方法首先对所有的方法进行了映射,然后对所有的类进行了映射,还映射了协议相关内容。这样就完成了三个步骤中的前两步,即:消息最终可以转发到对应的类上了。紧接着这个方法对类进行了实现,最后完成分类的解析,在原来的类的基础上进行了完整的补充。
1. 一些初始化操作
read_image
中会通过传递过来的 header list 来递归处理所有的 image ,在这之前会首先进行一些初始化逻辑:
if (!doneOnce) {
doneOnce = YES;
// 高版本iOS中一定是开启
if (DisableTaggedPointers) {
disableTaggedPointers();
}
// 初始化TaggedPointer混淆器
initializeTaggedPointerObfuscator();
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
// totalClasses:遍历image并通过_getObjc2ClassList获取
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
// GDB:GNU symbolic debugger,Linux调试器
// exported for debuggers in objc-gdb.h
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
ts.log("IMAGE TIMES: first time tasks");
}
这里的代码其实有点云里雾里,首先 TaggedPointer 在高版本 iOS 中必定是开启的,不会 disable,所以不需要关注 disableTaggedPointers
;
然后, initializeTaggedPointerObfuscator
是初始化 TaggedPointer 混淆器,本质上是生成了一个随机数,代码比较简单如下:
static void
initializeTaggedPointerObfuscator(void) {
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
// 比较老的SDK要关闭混淆器,以防老的SDK依赖TaggedPointer?
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
这里不是很明白具体的应用场景,只能大致猜一下。
因为使用到 Tagged Pointer 的类一般是 NSNumber、NSString 之类的。内容上,有一些标志位来让 objc 知道这个指针是 Tagged Pointer,其他的比特位则存储具体的值,这样就不需要再堆内存中开辟空间,然后再使用一个指针指向这个堆内存了。使用上直接利用这个 Tagged Pointer 就可以知道对应的值,比如一个 Number 的具体数值,或者一个字符串的具体数值。
那么 TaggedPointer 必定会有一些规则:
- 判断是否属于 TaggedPointer;
- 判断这个 TaggedPointer 是什么类型,NSNmuber,还是 NSString 等;
- 知道了类型后,值是怎么通过其他的比特位来计算的?
上述这些规则如果过于简单,攻击者可能可以直接通过这些规则来反推出变量的数值,进而获取一些信息。所以,这个对 TaggedPointer 做了混淆,每次打开 App 时都不一样,有点类似于 Slide 的作用;
紧接着,又是创建了一个 NXCreateMapTable
,名称为 gdb_objc_realized_classes
。
而 dgb 的全称是 GNU symbolic debugger
,也就是 Linux 平台下的调试器,难道这个表和调试有关系?
这里注释上给予了纠正:
// This is a misnomer: gdb_objc_realized_classes is actually a list of
// named classes not in the dyld shared cache, whether realized or not.
NXMapTable *gdb_objc_realized_classes; // exported for debuggers in objc-gdb.h
misnomer 是用词不当的意思,注释说的很清楚了, gdb_objc_realized_classes
这个全局变量实际上是共享缓存之外的一个类数组,无论这个类是否被实现,都会存在于这个数组中。
最后,创建了一个哈希表 allocatedClasses
,这个后续很多地方会用到,正好和上面的全量数组对应,这里存储已经被分配内存空间的类。
总结:
- 初始化 TaggedPointer 混淆器;
- 创建了两个 MapTable,一个用于全量存储 Class,另外一个存储已经 realized 的类;
2. 类映射 - 概览
这个阶段的代码如下:
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
if (! mustReadClasses(hi)) {
// Image is sufficiently optimized that we need not call 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);
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
上述代码有三个重点:
-
mustReadClasses
:条件与判断; -
readClass
:类的映射; - future class :如果是 future class,则进行 append 和 realloc 操作;
future classes 主要用于 CF 和 NS 类的桥接。因为 future classes 和 普通类的 realizeClassWithoutSwift 逻辑是相反的,暂不关注,后面会有专门的一期(也可能并不会有);
这段代码首先获取了 classlist 列表。该列表是从 mach-O 中的 __objc_classlist
读取,这个 section 存储的就是该 image 中声明的类,是不包括引用的外部类的。而__objc_classrefs
和 __objc_superrefs
中存储的是引用到的所有的类名,也包括外部的,如果是内部的,会指向内部地址:
上图中,三个 ViewController
都继承自 UIViewController
。
MachORuntimeArchitecture 中没有写 objc 相关的 section,也没有找到 __objc 相关的官方文档。所以 objc 相关的 section 只能靠自己根据实际情况去猜,可能会不准确。
即:该方法首先读取了 image 内部声明的所有类;
上述代码重点比较多,需要分开来看~~~
3. 类映射 - 条件预判断(Preflight)
先来看看 mustReadClasses
。
很明显,注释中写了:Image is sufficiently optimized that we need not call readClass()
。也就是说,如果这个函数返回 NO,那么证明这个 image 中的类已经被充分优化过了,不需要再读取。
那么 mustReadClasses
内部逻辑是怎样的呢?先看看代码:
/***********************************************************************
* mustReadClasses
* Preflight check in advance of readClass() from an image.
**********************************************************************/
bool mustReadClasses(header_info *hi)
{
const char *reason;
// If the image is not preoptimized then we must read classes.
if (!hi->isPreoptimized()) {
reason = nil; // Don't log this one because it is noisy.
goto readthem;
}
assert(!hi->isBundle()); // no MH_BUNDLE in shared cache
// If the image may have missing weak superclasses then we must read classes
if (!noMissingWeakSuperclasses()) {
reason = "the image may contain classes with missing weak superclasses";
goto readthem;
}
// If there are unresolved future classes then we must read classes.
if (haveFutureNamedClasses()) {
reason = "there are unresolved future classes pending";
goto readthem;
}
// readClass() does not need to do anything.
return NO;
readthem:
if (PrintPreopt && reason) {
_objc_inform("PREOPTIMIZATION: reading classes manually from %s "
"because %s", hi->fname(), reason);
}
return YES;
}
这个函数是用来做预检验,和后文的 readClass
内部的逻辑对应。该函数主要是做一些逻辑判断,最终返回 YES 或者 NO,其判断逻辑有这么几层:
- 是否被优化过,如果没有则返回 YES,即需要进行类的 read 操作;
- 有些类缺失了弱引用的父类,此时返回 YES,需要进行类的 read 操作;
- 如果有 FutureClass,则返回 YES,需要进行类的 read 操作;
- 上述情况都没有时,返回 YES;
预校验通过之后才会进入到真正的类映射,readClass
的注释如下:
该方法注释如下:
readClass从注释可以看到,该方法的目的是读取编译器生成的类的静态数据,会出现三种情况:
- 正常的类返回 cls,也就是静态数据在内存中的指针;
- 弱链接的类返回 nil;
- future class 返回预留的内存地址;
4. readClass-弱连接
我们先看第一部分,弱链接:
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->mangledName();
if (missingWeakSuperclass(cls)) {
// No superclass (probably weak-linked).
// Disavow any knowledge of this subclass.
addRemappedClass(cls, nil);
cls->superclass = nil;
return nil;
}
......
}
这部分代码是弱链接父类的判断。
missingWeakSuperclass
函数内部会递归判断类的 superclass 是否为空:
因为在 OC 中的跟类的 superClass 为空,也就是 NSObject 类对象。所以如果类的 superClass
为空且该类不是根类,那么这个类就属于缺失了 SuperClass
,有可能是弱引用缺失。
所以,这里的逻辑总结起来:
- 非根类且缺失了
superClass
则表示可能为弱链接缺失; - 弱链接缺失时,将 cls 的映射置为 nil;
- 弱链接缺失映射的 Map 和 FutureClass 是同一个 Map,而不是保存在映射普通类的两个 Map 中;
这里感觉弱链接缺失应该是在处理系统库的版本兼容问题。因为弱链接在我们业务代码中基本不会使用到,而 Future Class 又是和 CF 相关的一些类,或者是被预先添加了一些数据的系统类。而他们量又都映射在同一个 Map 中,所以,感觉这个行为应该也是处理系统类的行为、
弱引用一般用于版本兼容,这里如果是缺失了,可能会有将这个类置为 nil 之类的逻辑吧,不深究了, weak symbol 相关内容详见:dyld:启动流程解析;
根据注释,此时这个类可能就是 future class。这里大部分情况下都为 YES,不会进入到这个逻辑。
弱引用看来还是在系统库之间的引用比较多,这些逻辑已经被封装过了,所以我们的实际主工程基本用不到这些。
5. 类映射 - future class 的映射
swift 的逻辑也先不看,接下来就是对 future class 的一些处理:
if (Class newCls = popFutureNamedClass(mangledName)) {
// This name was previously allocated as a future class.
// Copy objc_class to future class's struct.
// Preserve future's rw data block.
// 为什么是rw?如果是从静态数据来的,那么应该是 ro
// 根据注释,这里rw应该有一些数据的,newCls不是我们熟悉的编译器生成的静态ro,而是系统预留出来的内存,内部提前写入了一些数据
class_rw_t *rw = newCls->data();
// 原本的ro可能为空
const class_ro_t *old_ro = rw->ro;
// 拷贝静态数据cls到newCls,newCls这里应该就是一个指针
memcpy(newCls, cls, sizeof(objc_class));
// 替换静态数据,仍然以编译器生成的为准
rw->ro = (class_ro_t *)newCls->data();
newCls->setData(rw);
freeIfMutable((char *)old_ro->name);
free((void *)old_ro);
addRemappedClass(cls, newCls);
replacing = cls;
cls = newCls;
}
这里大概看下,根据之前的文章,我们知道编译器生成的类相关的静态数据会在 realizeClassWithoutSwift
中被赋值给 rw,进而赋值给 class,完成类的初始化,而且 rw 也是在这个函数中被 alloc 的:
而 future class 这里的处理首先是:
- newCls 是系统提前预留的一段内存,内部写入了一些数据,但是 ro 可能是空的,仍然要以编译器数据为准;
- 获取到 newCls 的指针,并且获取了 rw 和 ro 的地址;
- 使用编译器的静态数据覆盖 newCls;,这里本质上仍然是 ro 的赋值;
- 修复 rw 中 ro 的指向;
- 将 rw 赋值给 newCls,这里最关键的是保留 rw 中除了 ro 之外的数据;
从注释中也可以看到,这么做是为了保留 future class 中 rw 的数据。而 rw 中的 ro 是从 cls 来的,所以保留的数据应该是 rw 中除了 ro 的其他部分:
rw数据所以,这里做个猜测:future class 是系统提前做好了一些初始化操作的类。future class 数据的完整性包括两部分:系统提前植入的数据 + 静态编译器生成的数据。
其实在后面的过程中,对 future class 还做了两个操作:
- resolvedFutureClasses 元素加 1 并且重新生成;
- 和普通的类一样,进行了 realizeClassWithoutSwift 操作;
总结:future class 最关键的是 rw 中除了 ro 之外的数据,可能就是 objc 为系统的类添加了一些特殊的方法、属性或者标志位(如 CF 的桥接相关方法?)。这些类的实现依赖两部分数据,包括系统提前植入的数据和静态时期编译器生成的静态数据。
注意,future class 的映射使用的是
addRemappedClass
,和一般的类存入的地方不一样。一般的类,无论是否 realize,都会先存入这gdb_objc_realized_classes
中;
6. 类映射 - 普通类的映射
至此,readClass
的代码就剩最后一部分了,省略部分代码之后,普通类的处理逻辑如下:
addNamedClass(cls, mangledName, replacing);
addClassTableEntry(cls);
// for future reference: shared cache never contains MH_BUNDLEs
if (headerIsBundle) {
cls->data()->flags |= RO_FROM_BUNDLE;
cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
}
return cls;
上述代码中,一般 image 都不会是 Bundle(MH_BUNDLE),所以关键就在于:
addNamedClass(cls, mangledName, replacing);
addClassTableEntry(cls);
addNamedClass
代码如下:
addNonMetaClass
的方法是对 secondary metaclass 进行处理,这里搞了半天没明白这是个啥,但是可以通过断点的形式来确定不会走到这里,最终会进入到 NXMapInsert
的逻辑:
而 gdb_objc_realized_classes
就是上文提到过的 MapTable,无论 class 是否被 realize,都会先存储到这里。allocated 之后存入 allocatedClasses
。
再来看看 addClassTableEntry
:
allocatedClasses
表只会存储 objc_allocateClassPair
分配内存的 class ,所以这里自然都不会进入到 INsert 操作。可以通过汇编代码验证:
所以,这一步就是将类添加到了上文提到的 gdb_objc_realized_classes
表中,本质上仍然是映射操作;也就是说,对于普通类,就是将静态数据的 cls 在内存中的地址映射到了 gdb_objc_realized_classes
表中。
7. 类映射 - 总结
至此,readClass 流程分析完毕,做下总结:
- 通过
__objc_classlist
获取到所有的类; - 预检验是否需要进行 readClass,一般的类都是未进行预优化的,所以都需要 read;
- 系统为 future class 在 rw 中预先添加了一些数据,比如属性、方法等;
- future class 在 read 阶段获取到了静态数据 ro ,进而在后面的步骤中通过 realize 操作补充静态时期编译器生成属性、方法、协议等数据;
- future class 会被添加到
remapped_class_map
表和resolvedFutureClasses
数组中; - 普通类会被添加到
gdb_objc_realized_classes
表中; - 这一步完成所有类的映射,主要信息为 className 和 class 的指针。class 指针指向的就是编译器生成的静态数据被加载到内存后的地址。后续完成 realize 操作之后会替换这个指针为新的 class 地址,即 runtime 中的 class。
- 这一步就是为下一步 realize 打下铺垫;
总而言之,readClass
这一步完成了 image 中所有类的静态数据的映射。
8. 修复类映射
这部分代码如下:
if (!noClassesRemapped()) {
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// fixme why doesn't test future1 catch the absence of this?
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
}
这部分代码就是获取 mach-o 中的 __objc_classrefs
和 __objc_superrefs
表中的数据。__objc_classrefs
中的数据表示所有内部和外部 class 的引用,而和 __objc_superrefs
中的存储这有继承关系的类;
这里的 ref 是引用,静态时期是 0 或者指向内部:
ref上述代码有个重点:使用的是 remap 相关的方法。所以,需要特别注意,这里的逻辑是处理 Map 相关的类。
那为什么需要处理呢?
对于普通类,只是单纯地使用 Insert 操作记录到 `` 表中。而对于 remap 的表,其对应关系是 cls(key) ---> newCls(Value)。
如果是 future class,或者说 remap 表中有值时,原本的指针(cls)是从 Mach-O 中获取并映射到内存中的。但是如果 remap 表中有值,比如弱引用置空了这个指针的指向,再比如 Future Class 中的 newCls 是系统提前预留的内存空间。这些指针的指向(Value)发生了变化,而动态链接 bind 完成之后,mach-o 中的引用指向的仍然是 cls,也就是静态编译器生成的数据。
所以,这里 objc 需要对这些 ref 进行重置(可以理解成 rebind),重置为 remap 的新的 newCls 的地址,这样不仅能使用到 objc 精心为这些类添加的数据,还能通过 ro 访问静态编译器生成的数据。
其关键代码在于 remapClassRef
函数:
上述代码就是 cls -> newCls 的关键代码;
总结下来,这一步做了这些事:
- 通过
noClassesRemapped
来判断是否有需要重新映射的类; - 如果有,则将
和
这两个表中的类作为 key,去remapped_class_map
中查找; - 找到了新的指针,就将这个指针进行替换;
至此,所有的 class 都完成了映射~~~
从这里可以看出来,类相关的 Map 有三个:
gdb_objc_realized_classes
全量保存普通类,allocatedClasses
保存已经被 realized 的类、remap 表以 key-value 的形式映射到 newCls;
9. 方法映射(方法注册)
方法映射相对简单,代码如下:
static size_t UnfixedSelectors;
{
mutex_locker_t lock(selLock);
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
}
上面代码先获取到了 __objc_selrefs
中的 SEL,然后进入到了 sel_registerNameNoLock
函数。这个函数之前提到过,初始化系统函数时就是用到了这个函数,期待吗如下:
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
SEL result = 0;
// 锁操作
if (shouldLock) selLock.assertUnlocked();
else selLock.assertLocked();
// 错误判断
if (!name) return (SEL)0;
// 已经预优化就不需要再注册了
result = search_builtins(name);
if (result) return result;
// namedSelectors中查找
conditional_mutex_locker_t lock(selLock, shouldLock);
if (namedSelectors) {
result = (SEL)NXMapGet(namedSelectors, name);
}
if (result) return result;
// No match. Insert.
if (!namedSelectors) {
namedSelectors = NXCreateMapTable(NXStrValueMapPrototype,
(unsigned)SelrefCount);
}
if (!result) {
result = sel_alloc(name, copy);
// fixme choose a better container (hash not map for starters)
NXMapInsert(namedSelectors, sel_getName(result), result);
}
return result;
}
这段代码做了这么几件事:
- 在
search_builtins
中查找已经被预优化处理过的函数,如果存在则不再处理; - 已经被映射到
namedSelectors
中的函数不再处理; -
namedSelectors
未创建时创建; - 为 SEL 分配内存并存储到
namedSelectors
中;
sel_alloc
如果不需要 copy,返回的就是 __objc_selrefs
中的字符串指针:
如果这个 image 是 Bundle 类型,则 copy 为 YES,逻辑也是重新分配内存并拷贝这个方法名字符串。
总之,这里就是将方法名进行映射保存,没什么需要特别关注的。
关于 search_builtins
详见之前的文章:iOS类加载流程(四):map_images流程分析
9. 协议 - 概览
协议的处理代码如下:
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
// 引入一个结构体
extern objc_class OBJC_CLASS_$_Protocol;
// 强制类型转换
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
// 指向指针的指针,数组内存存储的是协议结构体的指针
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
这段代码做了几件事:
- 引入了一个结构体,并被传入了在后面的
readProtocol
函数; - 除了常规的是否预优化、是否为 Bundle 之外,还获取到了存储 protocol 的 map;
- 通过
__objc_protolist
获取到了协议列表,列表内存存储这指向结协议结构体的指针;
10. 协议 - 静动态数据流
这里首先来看下 __objc_protolist
:
很明显,我们看到了项目中的一些协议,但是这里有个疑问,如果是自己定义的协议,那存储在本 Mach-O 中,列表中有指针是很正常的,但是为什么系统(动态库)的结构体也有指针指向?比如 NSObject
的,我们来找一下这个 data 的位置:
我们直接来找 _protocol_t
的第二个属性,协议的名称。因为 iOS 是小端模式,即:低内存存储低位地址,所以这里存储的地址是:0x1000003524
,这个位置的数据如下:
如上图,这个地方存储的就是 NSObject
,也就是协议名。其实在 MachOView 中直接就可以看得到,还有其他数据都可以直接看到,很直观:
而 _protocol_t
的结构体如下:
而在源码中,协议是使用 protocol_t
这个结构体来表示的:
再来看看 objc 转化成 C/C++ 的静态代码:
_OBJC_PROTOCOL_NSObject转译代码时,只声明 Protocol 并不会产生转译代码,需要真正使用 Protocol 才会有对应的代码。
而我们知道,objc_object
内部就一个 isa 成员属性。至此,静态数据、runtime 结构体、mach-O 文件都对应上了。
也就是说,这个 NSObject
的协议真的是有数据的,而不是指向外部,等待动态链接时被 rebind。
先来梳理一下这个数据流:
- protocol 列表中存储着
_OBJC_PROTOCOL_NSObject
结构体的地址; -
_OBJC_PROTOCOL_NSObject
中的属性protocol_name
就是协议的名称,存储在__objc_classname
中;
上面一定要用 iPhone 真机进行测试,如果是模拟器,因为没有指针优化,所以会有很大差别。
因此可以得出结论:
- 静态编译器在识别到 @protocol 之后就会生成协议对应的静态结构体,而没有动态链接时 rebind 操作;
这个结论可以通过两个方面来验证:
- 观察 objc 转译后的代码,确实存在协议的一些实现:
NSObject 协议的部分如下:
NSObject- 可以自己写一个系统的 Protocol,再观察转译后的代码:
如上图,属于 <UIKit> 的 UIApplicationDelegate
协议在静态时期直接被覆盖了。
那么这里就有个疑问了,既然上层业务代码可以直接覆盖系统库声明的协议,那这样不会有什么问题吗?这里先留个疑问,后文会讲到。
总结:协议处理的第一步和 Class 是完全一样的,协议和类的静态数据都是来源于编译器。
11. 协议 - 协议重复的处理方式
现在我们回到源码,直接进入到 readProtocol
函数。
这个函数的代码不少,就不直接贴了。代码主要是几个 if else 的判断,先来看第一分支:
static void
readProtocol(protocol_t *newproto, Class protocol_class,
NXMapTable *protocol_map,
bool headerIsPreoptimized, bool headerIsBundle)
{
// This is not enough to make protocols in unloaded bundles safe,
// but it does prevent crashes when looking up unrelated protocols.
auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
if (oldproto) {
// Some other definition already won.
if (PrintProtocols) {
_objc_inform("PROTOCOLS: protocol at %p is %s "
"(duplicate of %p)",
newproto, oldproto->nameForLogging(), oldproto);
}
}
......else if(xxx)......
......省略.......
可以看到,该方法首先通过 getProtocol
来获取已经被映射协议。而保存协议的 Map ,也就是方法的入参都是通过 protocols()
获取的。
这里一般情况下获取到的都为 nil,但是如果协议已经被映射,那么就进入到了第一个分支。
通过代码和注释可以很清楚的看到,如果这个协议已经被定义过,那么直接输入一些日志,不作任何处理;
这里就需要在此剔除刚刚的疑问:编译器识别到协议,会直接生成协议对应的结构体,重复定义时,后者覆盖前者。
编译器编译文件的先后顺序由 Xcode 中的文件顺序决定。但是如果是系统的动态库和业务主工程存在歇息重复的情况时,因为给到 objc 的 image 也是有顺序的。案例来说,主 image 排在第 0 位,libobjc.A.dylib 拍在后面,那么按照代码的逻辑,主工程中协议岂不是优先级更高?
其实不是的,给到 objc 的 images 已经被重新排过序了,相反的,主 image 拍在最后,也就是到最后才会执行 objc 中的整个流程。
如何验证,很简单,符号断点看看 images 的入参就好了。这里把断点打到 map_images_nolock()
这个方法上:
这个断点这么打有几个关键点:
-
map_images_nolock
的入参有mhPaths
,可以看到所有的 image 对应的路径和名称; -
map_images_nolock
中的数组是 dyld 最后调用 objc 回调时的方法,给到的数据比较原始; -
map_images_nolock
使用的是while(i--)
,所以是倒叙添加 header 的;
断点之后,因为入参是存储在 x0-x8 这写寄存器中的,所以首先读取 x1 寄存器的值:
寄存器然后,直接打印这个数组中的元素:
image.png这里需要注意的是,因为 C 语言中,字符的类型是 char
,而字符串是由多个 char
+ \0
组成的,所以字符串本身就是数组。而数组使用指针来指向的,所以字符串对应的类型是 char *
。
而 paths 是由多个字符串组成的数组,字符串数组就是存储这 char *
元素的集合。所以 paths 的类型是 char **
,即:字符串数组需要使用 char **
来标识。
上图可以看到,主 image 拍在第一,但是被 while(i--)
之后,最终在 addHeader
方法中最后被添加。那么 libobjc.A.dylib
在哪呢?
所以,这里就可以释疑了,即:libobjc.A.dylib
等系统库在 addHeader
之后拍在最前面,所以协议优先级最高。
其实到这里,虽然我们知道协议在静态编译时期有代码提示、规范代码、多继承的作用,但是到这里,还是看不出来,协议在 runtime 中的作用是什么?用来方法分发,也用不上协议啊......这个基本在静态时期通过
respondsToSelector
做了容错了。
12. 协议 - 预优化
接着,来看 readProtocl
中第二个分支:
else if (headerIsPreoptimized) {
// Shared cache initialized the protocol object itself,
// but in order to allow out-of-cache replacement we need
// to add it to the protocol table now.
protocol_t *cacheproto = (protocol_t *)
getPreoptimizedProtocol(newproto->mangledName);
protocol_t *installedproto;
if (cacheproto && cacheproto != newproto) {
// Another definition in the shared cache wins (because
// everything in the cache was fixed up to point to it).
installedproto = cacheproto;
}
else {
// This definition wins.
installedproto = newproto;
}
assert(installedproto->getIsa() == protocol_class);
assert(installedproto->size >= sizeof(protocol_t));
insertFn(protocol_map, installedproto->mangledName,
installedproto);
}
预优化的逻辑仍然是大同小异:
- 在预优化的协议表中查询是否存在该协议的映射;
- 如果存在,且不同,则以预优化过的为准,这样就不需要后续的协议解析流程了。
- 如果不存在,或者相同,那么就不是预优化过的协议,需要重走后续协议解析流程;
这里也可以看到 insertFn
中传入的 Map 依然是 protocol_map
。与类的 map 一样,使用 name 作为 key,protocol/cls 作为 Value;
这里可以做下总结:
- 普通类映射:name : cls;
- future class 重映射:cls : newCls;
- future class 重绑定/重映射:cls ---> newCls(替换 mach-O 中被 bebind 过的地址);
- 协议映射:name : protocol;
13. 协议 - 普通协议处理
最后两个逻辑是没有被预优化过的 protocol 的处理逻辑:
else if (newproto->size >= sizeof(protocol_t)) {
// New protocol from an un-preoptimized image
// with sufficient storage. Fix it up in place.
newproto->initIsa(protocol_class); // fixme pinned
insertFn(protocol_map, newproto->mangledName, newproto);
}
else {
// New protocol from an un-preoptimized image
// with insufficient storage. Reallocate it.
size_t size = max(sizeof(protocol_t), (size_t)newproto->size);
protocol_t *installedproto = (protocol_t *)calloc(size, 1);
memcpy(installedproto, newproto, newproto->size);
installedproto->size = (typeof(installedproto->size))size;
installedproto->initIsa(protocol_class); // fixme pinned
insertFn(protocol_map, installedproto->mangledName, installedproto);
}
对于第一个条件 newproto->size >= sizeof(protocol_t)
,猜测大部分下应当是不成立的,因为静态时期这个 size 是这么设置的:
而 runtime 时期的协议结构体会多出几个属性:
protocol_t因为这段代码在 objc-818 版本已经没有了,而且可能是这段函数的代码直接被赋值到了外部函数中,也打不到断点了,暂时不纠结了。
总之,这个分支主要做两件事:
- 协议在 oc 中也近似于一个类,这里对其 isa 进行了初始化,即协议的元类都是
OBJC_CLASS_$_Protocol
; - 读取了协议的静态数据;
- 完成了协议的映射,协议单独存放在
protocol_map
表中,以 name 作为 key,以 protocl 地址作为 value;
至此,readProtocol
中的代码分析完毕~~~~~~说白了,这段代码逻辑和 readClass
基本一致,目的只有一个:映射。
14. 修复协议引用
代码逻辑如下:
for (EACH_HEADER) {
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}
和类引用、方法引用一样,如果存在被 remap 的协议,那么这个协议的地址也需要被重新 rebind;
这里还是不知道协议在 runtime 中的具体作用,但是从这里可以看出来,协议和类是关联的,如果协议只在静态时期有作用,那么这里完全没必要做这么多映射、修复工作?
猜测协议在 runtime 中的作用:
- 参与方法分发的流程
这个需要后续研究 objcMsgSend
流程时,具体看看有没有 protocol 相关的一些判断或者限制代码。
感觉方法查找本质上仍然是去查找方法的实现,protocol 只是声明,并没有实现,所以 protocol 的作用可能仍然偏向于静态时期???
- 对外提供一些结构
比如 class_conformsToProtocol
接口,比如 protocol_getMethodDescription
;
15. 非懒加载类的加载(realize)
上述所有步骤都是在映射,也就是为 objcMsgSend
的信号做准备。信号本身就是一个方法相关的字符串,而方法的实现都是在类中,所以,接下来就真正进入了方法的装配阶段:类实现。
详见:xxx;
16. future class 的实现
这部分代码如下:
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
Class cls = resolvedFutureClasses[i];
if (cls->isSwiftStable()) {
_objc_fatal("Swift class is not allowed to be future");
}
realizeClassWithoutSwift(cls);
cls->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
这部分代码很好理解,上文中,如果存在 future class,最终都会被 remap,进而被解析。而解析的本质就是保留预留的 rw 数据,并读取静态 ro 数据。 resolvedFutureClasses
这个数组则是保存这些 future class 的指针,所以这里通过 resolvedFutureClasses
对指针指向的类进行实现,目的就是将 ro 数据添加到 rw 中,和普通类的实现逻辑一直。
所以,这里在此重申:future class 就是系统提前预留了空间和 一些 rw 数据,只有这部分逻辑和普通类有区别,其他逻辑和普通类基本没有差异。
17. 分类
详见xxx - 6