经过之前的文章,我们已经知道了:
- objc 调用 dyld 方法注册 3 个回调时,会通过
notifyBatchPartial
触发map_images
回调; - 有新的 image 被 map 时,也会触发该回调;
那么,map 函数到底做了什么?现在就来看看 map 函数的流程是怎么样的。
先上流程图:
流程图1. map_images
简化代码如下:
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();
}
...省略totalClass的计算逻辑
if (firstTime) {
//sel初始化
sel_init(selrefCount);
// SideTable
arr_init();
}
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
这个方法其实就做了这么几件事:
- 共享缓存相关的预优化操作;
- 计算当前所有 image 中总共的 class 、方法数量;
- 初始化方法表和 SideTable;
- _read_images 相关逻辑;
接下来一步一步分析~~~
2. 预优化
preopt_init
函数在 objc-opt.mm
中有两个,其中一个就是单纯的打印逻辑:
而另外一个函数才是真正的逻辑,而影响的关键在于 SUPPORT_PREOPT
这个宏,其定义如下:
也就是说,iOS 架构下改值必定为 1,所以会进入到下面的逻辑,那么 preopt_init
函数就是这样的:
void preopt_init(void) {
// Get the memory region occupied by the shared cache.
size_t length;
const void *start = _dyld_get_shared_cache_range(&length);
if (start) {
shared_cache_start = (uintptr_t)start;
shared_cache_end = shared_cache_start + length;
} else {
shared_cache_start = shared_cache_end = 0;
}
// `opt` not set at compile time in order to detect too-early usage
const char *failure = nil;
opt = &_objc_opt_data;
if (DisablePreopt) {
// OBJC_DISABLE_PREOPTIMIZATION is set
// If opt->version != VERSION then you continue at your own risk.
failure = "(by OBJC_DISABLE_PREOPTIMIZATION)";
} else if (opt->version != objc_opt::VERSION) {
// This shouldn't happen. You probably forgot to edit objc-sel-table.s.
// If dyld really did write the wrong optimization version,
// then we must halt because we don't know what bits dyld twiddled.
_objc_fatal("bad objc preopt version (want %d, got %d)",
objc_opt::VERSION, opt->version);
} else if (!opt->selopt() || !opt->headeropt_ro()) {
// One of the tables is missing.
failure = "(dyld shared cache is absent or out of date)";
}
if (failure) {
// All preoptimized selector references are invalid.
preoptimized = NO;
opt = nil;
disableSharedCacheOptimizations();
} else {
// Valid optimization data written by dyld shared cache
preoptimized = YES;
}
}
上述代码逻辑如下:
- 获取共享缓存内存地址;
- 使用
&_objc_opt_data
赋值opt
; - 分别根据一些逻辑来判断是否初始化成功;
- 初始化标志位;
上述代码中,最重要的无非就是一句代码:
opt = &_objc_opt_data;
这个 _objc_opt_data
是个啥?
根据注释可以知道 _objc_opt_data
就是 mach-o 文件中的 __TEXT
这个 segment 中的 __objc_opt_ro
这个 section。
这个 __objc_opt_ro
是个啥?相关资料很少,总结一下:
- libobjc.A.dylib 特有;
- dyld 需要对这些数据进行 rebase,也就是更新 slide;
- 主要是存储一些预优化的数据;
对于第一点,可以从 mach-O 上直观看到:
__objc_opt_ro对于第二点,是从 Adding ASLR to jailbroken iPhones 看到的资料:
Adding ASLR to jailbroken iPhones
dyld3 确实也对这些数据进行了 rebase:
dyld3-doOptimizeObjC上面的代码来自 dyld-655.1,这个版本已经被全面应用在 iOS12 上了,dyld3 中优化的逻辑大概是:缓存 dyld 中的一些不会改变或者变化不频繁的数据,下次启动时不需要再重新全量解析,以此来优化启动速度。比如 rebase 属于每次都需要重新解析的,不会也不能缓存。但是一些类的结构,特别是系统相关的类的结构基本不会有变化,所以这一部分数据可以缓存下来,下次启动时直接加载,不需要重新解析了。
对于第三点,是在 IDA 的 7.2 版本的更新公告中找到的:
IDA-7.2上图来自 IDA-7.2 版本的更新公告:https://hex-rays.com/products/ida/news/7_2/
IDA:IDA Pro(简称IDA)是DataRescue公司(home of PhotoRescue, data recovery solution for flash memory cards)出品的一款交互式反汇编工具,它功能强大、操作复杂,要完全掌握它,需要很多知识。
至此,我们知道了 __objc_opt_ro
主要是 dyld3 中用来存储预优化数据的,那么这些数据有什么呢?
回到声明了 _objc_opt_data
的 objc-opt.mm 文件中,可以看到相关的方法并不多:
很明显,在 objc-opt.mm 中,涉及到了协议、方法、类等的优化。
再来看看 objc_opt_t
这个结构体的定义,这个结构体通过 #include <objc-shared-cache.h>
导入:
所以,这里大概可以猜出来,dyld3 对 libobjc.A.dylib 中的类、方法、协议、ro、rw 数据都做了一些预优化操作,大概是加载(map、read)、实现(realize)完成之后,将这些数据保存下来,这样在以后的冷启动过程中,就不需要再执行这些操作,只需要读取完毕之后存入内存缓存即可使用。
相关的 wwdc 可能会有帮助:Advancements in the Objective-C runtime
比如,builtins
这个全局变量存储的就是被优化过的 objc 内置函数(猜的),对应的 search_builtins()
就在很多地方被调用:
static SEL search_builtins(const char *name)
{
#if SUPPORT_PREOPT
if (builtins) return (SEL)builtins->get(name);
#endif
return nil;
}
至此,预优化的逻辑告一段落,做个总结吧:
- dyld3 中读取 libobjc.A.dylib 中的
__objc_opt_ro
数据并进行 rebase; - objc 在冷启动时会进行预加载优化;
- 预加载优化大概是一些系统函数、类的、方法的优化,这样就省去了启动时的消耗;
- 启动优化在 dyld2 进化到 dyld3 时优化比较大,有兴趣的可以重点关注下高版本 dyld 中 dyld3 的源码;
3. objc 相关 image 处理
这一部分主要是处理当前所有的 image 的 header:
// Count classes. Size various table based on the total.
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
{
uint32_t i = mhCount;
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
// 判断image是否有objc相关内容,有则实例化并记录header
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
#if __OBJC2__
// objc2 新增特性
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
// read_image中会被处理
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
#else
_getObjcSelectorRefs(hi, &selrefCount);
#endif
}
hList[hCount++] = hi;
}
}
如上,所有的 image 都会递归调用这段代码。
上述逻辑中,首先调用 addHeader
对 image 的 header 进行处理,该方法代码就不贴了,主要做了这么几件事:
- 有效性判断;
- 通过
_getObjcImageInfo
和获取__OBJC
这个 segment中的内容,以此来判断该 image 是否属于 objc 应该处理的 image; - 如果改 image 属于 objc 相关的 image,则通过
calloc
来实例化 header 并存储相关信息; - 通过
appendHeader
存储实例化的 header 到一个链表中供后续使用; - 通过 mach-o 中的
__objc_classlist
获取该 image 中的 classCount;
总之,这一步就是实例化了 objc 相关的 header 并且以链表的形式存储到内存中以备后用,与此同时,完成了 classCount 和方法引用数量的计算。
4. Sel的初始化操作和注册逻辑
接下来进行了方法相关的初始化逻辑,代码如下:
/***********************************************************************
* sel_init
* Initialize selector tables and register selectors used internally.
**********************************************************************/
void sel_init(size_t selrefCount) {
// save this value for later
SelrefCount = selrefCount;
#if SUPPORT_PREOPT
builtins = preoptimizedSelectors();
#endif
// Register selectors used by libobjc
#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
}
上述方法的关键在于:
builtins = preoptimizedSelectors();
sel_registerNameNoLock
对于第一点,这里的代码:
objc_selopt_t *preoptimizedSelectors(void) {
return opt ? opt->selopt() : nil;
}
这个 opt 就是上文中说到的和预优化有关的逻辑,在 preopt_init()
中被赋值:
是不是很惊喜?正好和上文对上了~~~~~~
此时,再去看看 preoptimizedSelectors
这个方法具体逻辑:
objc_selopt_t *preoptimizedSelectors(void)
{
return opt ? opt->selopt() : nil;
}
而 selopt()
是声明在 objc_opt_t
结构体中的:
const objc_selopt_t* selopt() const {
if (selopt_offset == 0) return NULL;
return (objc_selopt_t *)((uint8_t *)this + selopt_offset);
}
也就是说,预优化过后 sel 的表会被加载到内存中,而这个内存地址在这里被赋值给了 builtins 这个全局变量(猜的),对应的 search_builtins()
就在很多地方被调用:
static SEL search_builtins(const char *name)
{
#if SUPPORT_PREOPT
if (builtins) return (SEL)builtins->get(name);
#endif
return nil;
}
上述代码应该很好理解了,就是给过来一个 sel 对应的 name,在 builtins 中搜索是否存在,存在表示这个 SEL 是被预优化过的,后续可能会节省很多初始化、实现等逻辑,比如 sel_ismapped 函数中:
sel_isMapped继续来看第二步,也就是 sel_registerNameNoLock
函数,这个函数最终会走到 __sel_registerName
,其代码如下:
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;
}
上述代码逻辑非常清晰:
- 锁操作;
- 字符串有效判断;
- 已经预优化过的不需要再注册,直接返回预优化过后的 SEL;
- 在 namedSelectors 中寻找,找到则返回结果;
- 没有 namedSelectors 则初始化;
- 对 SEL 分配内存空间然后插入到 namedSelectors 中;
总之,SEL 也被分配了内存空间,并且和 Class 一样,以 MapTable
的形式被存储。
至此,SEL 的初始化和注册逻辑就完成了~~~
总结一下,这一步主要做了几件事:
- 将预优化过后的 SEL 复制到 builtins 中,方便后续的 search 操作;
- 预优化后的 SEL 可以节省很多操作;
- 注册 SEL 并以 MapTable 的形式存储;
5. 初始化SideTable
方法相关初始化逻辑完成后,对 SideTable 进行了初始化。SideTable 是 OC 中相当关键的一个存储结构,关于 SideTable 详见:iOS:SideTable,本文就不再赘述了。
6. read_images
上述步骤过后,就到了 read_images
方法的调用。该方法会传递:
- hList:objc 相关 image 的 header 列表;
- hCount:objc 相关 image 的 header 数量(这里是 C 语言数组的常规操作,数组本质是指针。count 是必须要传的,否则无法知道数组的结束点);
- totalClasses:所有 objc 相关的 images 中 class 的总数;
- unoptimizedTotalClasses:未优化的 class 的总数;
至于 read_images
代码,因为过于冗长,下篇再做分析;
网友评论