你真的了解 load 方法么?

2019 年 12 月 10 日

你真的了解 load 方法么?

关注仓库,及时获得更新:iOS-Source-Code-Analyze


因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 x86_64 架构下运行的,对于在 arm64 中运行的代码会特别说明。


写在前面


文章的标题与其说是问各位读者,不如说是问笔者自己:真的了解 + load 方法么?


+ load 作为 Objective-C 中的一个方法,与其它方法有很大的不同。它只是一个在整个文件被加载到运行时,在 main 函数调用之前被 ObjC 运行时调用的钩子方法。其中关键字有这么几个:


  • 文件刚加载

  • main 函数之前

  • 钩子方法


我在阅读 ObjC 源代码之前,曾经一度感觉自己对 + load 方法的作用非常了解,直到看了源代码中的实现,才知道以前的以为,只是自己的以为罢了。


这篇文章会假设你知道:


  • 使用过 + load 方法

  • 知道 + load 方法的调用顺序(文章中会简单介绍)


在这篇文章中并不会用大篇幅介绍 + load 方法的作用其实也没几个作用,关注点主要在以下两个问题上:


  • + load 方法是如何被调用的

  • + load 方法为什么会有这种调用顺序


load 方法的调用栈


首先来通过 load 方法的调用栈,分析一下它到底是如何被调用的。


下面是程序的全部代码:


Objective-C


// main.m#import <Foundation/Foundation.h>
@interface XXObject : NSObject @end
@implementation XXObject
+ (void)load { NSLog(@"XXObject load");}
@end
int main(int argc, const char * argv[]) { @autoreleasepool { } return 0;}
复制代码


代码总共只实现了一个 XXObject+ load 方法,主函数中也没有任何的东西:



虽然在主函数中什么方法都没有调用,但是运行之后,依然打印了 XXObject load 字符串,也就是说调用了 + load 方法。


使用符号断点


使用 Xcode 添加一个符号断点 +[XXObject load]


注意这里 +[ 之间没有空格



为什么要加一个符号断点呢?因为这样看起来比较高级。


重新运行程序。这时,代码会停在 NSLog(@"XXObject load"); 这一行的实现上:



左侧的调用栈很清楚的告诉我们,哪些方法被调用了:


Objective-C


0  +[XXObject load]1  call_class_loads()2  call_load_methods3  load_images4  dyld::notifySingle(dyld_image_states, ImageLoader const*)11 _dyld_start
复制代码


dyld 是 the dynamic link editor 的缩写,它是苹果的_动态链接器_。

在系统内核做好程序准备工作之后,交由 dyld 负责余下的工作。本文不会对其进行解释


每当有新的镜像加载之后,都会执行 3 load_images 方法进行回调,这里的回调是在整个运行时初始化时 _objc_init 注册的(会在之后的文章中具体介绍):


Objective-C


dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
复制代码


有新的镜像被加载到 runtime 时,调用 load_images 方法,并传入最新镜像的信息列表 infoList


Objective-C


const char *load_images(enum dyld_image_states state, uint32_t infoCount,            const struct dyld_image_info infoList[]){    bool found;
found = false; for (uint32_t i = 0; i < infoCount; i++) { if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) { found = true; break; } } if (!found) return nil;
recursive_mutex_locker_t lock(loadMethodLock);
{ rwlock_writer_t lock2(runtimeLock); found = load_images_nolock(state, infoCount, infoList); }
if (found) { call_load_methods(); }
return nil;}
复制代码


什么是镜像


这里就会遇到一个问题:镜像到底是什么,我们用一个断点打印出所有加载的镜像:



从控制台输出的结果大概就是这样的,我们可以看到镜像并不是一个 Objective-C 的代码文件,它应该是一个 target 的编译产物。


Objective-C


...(const dyld_image_info) $52 = {  imageLoadAddress = 0x00007fff8a144000  imageFilePath = 0x00007fff8a144168 "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"  imageFileModDate = 1452737802}(const dyld_image_info) $53 = {  imageLoadAddress = 0x00007fff946d9000  imageFilePath = 0x00007fff946d9480 "/usr/lib/liblangid.dylib"  imageFileModDate = 1452737618}(const dyld_image_info) $54 = {  imageLoadAddress = 0x00007fff88016000  imageFilePath = 0x00007fff88016d40 "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation"  imageFileModDate = 1452737917}(const dyld_image_info) $55 = {  imageLoadAddress = 0x0000000100000000  imageFilePath = 0x00007fff5fbff8f0 "/Users/apple/Library/Developer/Xcode/DerivedData/objc-dibgivkseuawonexgbqssmdszazo/Build/Products/Debug/debug-objc"  imageFileModDate = 0}
复制代码


这里面有很多的动态链接库,还有一些苹果为我们提供的框架,比如 Foundation、 CoreServices 等等,都是在这个 load_images 中加载进来的,而这些 imageFilePath 都是对应的二进制文件的地址。


但是如果进入最下面的这个目录,会发现它是一个可执行文件,它的运行结果与 Xcode 中的运行结果相同:



准备 + load 方法


我们重新回到 load_images 方法,如果在扫描镜像的过程中发现了 + load 符号:


Objective-C


for (uint32_t i = 0; i < infoCount; i++) {    if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {        found = true;        break;    }}
复制代码


就会进入 load_images_nolock 来查找 load 方法:


Objective-C


bool load_images_nolock(enum dyld_image_states state,uint32_t infoCount,                   const struct dyld_image_info infoList[]){    bool found = NO;    uint32_t i;
i = infoCount; while (i--) { const headerType *mhdr = (headerType*)infoList[i].imageLoadAddress; if (!hasLoadMethods(mhdr)) continue;
prepare_load_methods(mhdr); found = YES; }
return found;}
复制代码


调用 prepare_load_methodsload 方法的调用进行准备(将需要调用 load 方法的类添加到一个列表中,后面的小节中会介绍):


Objective-C


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; // category for ignored weak-linked class realizeClass(cls); assert(cls->ISA()->isRealized()); add_category_to_loadable_list(cat); }}
复制代码


通过 _getObjc2NonlazyClassList 获取所有的类的列表之后,会通过 remapClass 获取类对应的指针,然后调用 schedule_class_load 递归地安排当前类和没有调用 + load 父类进入列表。


Objective-C


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);}
复制代码


在执行 add_class_to_loadable_list(cls) 将当前类加入加载列表之前,会先把父类加入待加载的列表,保证父类在子类前调用 load 方法。


调用 + load 方法


在将镜像加载到运行时、对 load 方法的准备就绪之后,执行 call_load_methods,开始调用 load 方法:


Objective-C


void call_load_methods(void){    ...
do { while (loadable_classes_used > 0) { call_class_loads(); }
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
...}
复制代码


方法的调用流程大概是这样的:



其中 call_class_loads 会从一个待加载的类列表 loadable_classes 中寻找对应的类,然后找到 @selector(load) 的实现并执行。


Objective-C


static void call_class_loads(void){    int i;
struct loadable_class *classes = loadable_classes; 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_method)(cls, SEL_load); }
if (classes) free(classes);}
复制代码


这行 (*load_method)(cls, SEL_load) 代码就会调用 +[XXObject load] 方法。


我们会在下面介绍 loadable_classes 列表是如何管理的。


到现在,我们回答了第一个问题:


Q:load 方法是如何被调用的?


A:当 Objective-C 运行时初始化的时候,会通过 dyld_register_image_state_change_handler 在每次有新的镜像加入_运行时_的时候,进行回调。执行 load_images 将所有包含 load 方法的文件加入列表 loadable_classes ,然后从这个列表中找到对应的 load 方法的实现,调用 load 方法。


加载的管理


ObjC 对于加载的管理,主要使用了两个列表,分别是 loadable_classesloadable_categories


方法的调用过程也分为两个部分,准备 load 方法和调用 load 方法,我更觉得这两个部分比较像生产者与消费者:



add_class_to_loadable_list 方法负责将类加入 loadable_classes 集合,而 call_class_loads 负责消费集合中的元素。


而对于分类来说,其模型也是类似的,只不过使用了另一个列表 loadable_categories


“生产” loadable_class


在调用 load_images -> load_images_nolock -> prepare_load_methods -> schedule_class_load -> add_class_to_loadable_list 的时候会将未加载的类添加到 loadable_classes 数组中:


Objective-C


void add_class_to_loadable_list(Class cls){    IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod(); if (!method) return;
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++;}
复制代码


方法刚被调用时:


  1. 会从 class 中获取 load 方法: method = cls->getLoadMethod();

  2. 判断当前 loadable_classes 这个数组是否已经被全部占用了:loadable_classes_used == loadable_classes_allocated

  3. 在当前数组的基础上扩大数组的大小:realloc

  4. 把传入的 class 以及对应的方法的实现加到列表中


另外一个用于保存分类的列表 loadable_categories 也有一个类似的方法 add_category_to_loadable_list


Objective-C


void add_category_to_loadable_list(Category cat){    IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
if (!method) return;
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++;}
复制代码


实现几乎与 add_class_to_loadable_list 完全相同。


到这里我们完成了对 loadable_classes 以及 loadable_categories 的提供,下面会开始消耗列表中的元素。


“消费” loadable_class


调用 load 方法的过程就是“消费” loadable_classes 的过程,load_images -> call_load_methods -> call_class_loads 会从 loadable_classes 中取出对应类和方法,执行 load


Objective-C


void call_load_methods(void){    static bool loading = NO;    bool more_categories;
loadMethodLock.assertLocked();
if (loading) return; loading = YES;
void *pool = objc_autoreleasePoolPush();
do { while (loadable_classes_used > 0) { call_class_loads(); }
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;}
复制代码


上述方法对所有在 loadable_classes 以及 loadable_categories 中的类以及分类执行 load 方法。


Objective-C


do {    while (loadable_classes_used > 0) {        call_class_loads();    }
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
复制代码


调用顺序如下:


  1. 不停调用类的 + load 方法,直到 loadable_classes 为空

  2. 调用一次 call_category_loads 加载分类

  3. 如果有 loadable_classes 或者更多的分类,继续调用 load 方法


相比于类 load 方法的调用,分类中 load 方法的调用就有些复杂了:


Objective-C


static bool call_category_loads(void){    int i, shift;    bool new_categories_added = NO;    // 1. 获取当前可以加载的分类列表    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()) { // 2. 如果当前类是可加载的 `cls && cls->isLoadable()` 就会调用分类的 load 方法 (*load_method)(cls, SEL_load); cats[i].cat = nil; } }
// 3. 将所有加载过的分类移除 `loadable_categories` 列表 shift = 0; for (i = 0; i < used; i++) { if (cats[i].cat) { cats[i-shift] = cats[i]; } else { shift++; } } used -= shift;
// 4. 为 `loadable_categories` 重新分配内存,并重新设置它的值 new_categories_added = (loadable_categories_used > 0); 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]; }
if (loadable_categories) free(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;}
复制代码


这个方法有些长,我们来分步解释方法的作用:


  1. 获取当前可以加载的分类列表

  2. 如果当前类是可加载的 cls && cls->isLoadable() 就会调用分类的 load 方法

  3. 将所有加载过的分类移除 loadable_categories 列表

  4. loadable_categories 重新分配内存,并重新设置它的值


调用的顺序


你过去可能会听说过,对于 load 方法的调用顺序有两条规则:


  1. 父类先于子类调用

  2. 类先于分类调用


这种现象是非常符合我们的直觉的,我们来分析一下这种现象出现的原因。


第一条规则是由于 schedule_class_load 有如下的实现:


Objective-C


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);}
复制代码


这里通过这行代码 schedule_class_load(cls->superclass) 总是能够保证没有调用 load 方法的父类先于子类加入 loadable_classes 数组,从而确保其调用顺序的正确性。


类与分类中 load 方法的调用顺序主要在 call_load_methods 中实现:


Objective-C


do {    while (loadable_classes_used > 0) {        call_class_loads();    }
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
复制代码


上面的 do while 语句能够在一定程度上确保,类的 load 方法会先于分类调用。但是这里不能完全保证调用顺序的正确。


如果分类的镜像在类的镜像之前加载到运行时,上面的代码就没法保证顺序的正确了,所以,我们还需要在 call_category_loads 中判断类是否已经加载到内存中(调用 load 方法):


Objective-C


if (cls  &&  cls->isLoadable()) {    (*load_method)(cls, SEL_load);    cats[i].cat = nil;}
复制代码


这里,检查了类是否存在并且是否可以加载,如果都为真,那么就可以调用分类的 load 方法了。


load 的应用


load 可以说我们在日常开发中可以接触到的调用时间最靠前的方法,在主函数运行之前,load 方法就会调用。


由于它的调用不是_惰性_的,且其只会在程序调用期间调用一次,最最重要的是,如果在类与分类中都实现了 load 方法,它们都会被调用,不像其它的在分类中实现的方法会被覆盖,这就使 load 方法成为了方法调剂的绝佳时机。


但是由于 load 方法的运行时间过早,所以这里可能不是一个理想的环境,因为某些类可能需要在在其它类之前加载,但是这是我们无法保证的。不过在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的。


参考资料



关注仓库,及时获得更新:iOS-Source-Code-Analyze


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/load


2019 年 12 月 10 日 17:58103

评论

发布
暂无评论
发现更多内容

netdata安装到redhat7.6最简手册

橙子冰

netdata

Apache Pulsar 社区周报:08-15 ~ 08-21

Apache Pulsar

云原生 Apache Pulsar 消息系统 消息中间件

云上度假村木莲庄酒店助你远离城市的喧嚣

InfoQ_967a83c6d0d7

白板技术实践:在线教育平台如何保障课件数据安全

ZEGO即构

加密解密 OSS 鉴权

用户密码验证函数

周冬辉

加密

奈学:Executor线程池的概述

奈学教育

线程池 Executor

Docker 之常见应用部署

哈喽沃德先生

Docker 容器 微服务 容器技术 容器化

【华为云数据库技术大公开】机房失火后,还能拯救你的数据吗?

华为云开发者社区

数据库 机房 华为云 数据存储 云数据库

奈学:Executor线程池的概述

古月木易

线程池 Executor

实用!教学白板跨国低时延互动技术实现指南

ZEGO即构

OSS 全站加速 集群

浅谈业务系统设计哲学

滴滴普惠出行

一周信创舆情观察(8.10~8.23)

统小信uos

java安全编码指南之:拒绝Denial of Service

程序那些事

Java 安全编码指南 java安全编码 DOS攻击 zip炸弹

Docker 镜像构建之 docker commit

哈喽沃德先生

Docker 容器 微服务 容器技术 容器化

第 0 期架构师训练营第3周作业1

傅晶

组合模式

安全架构和高可用系统的架构

周冬辉

高可用系统的架构

Newbe.Claptrap 框架入门,第四步 —— 利用 Minion,商品下单

newbe36524

云计算 微服务 dock .net core ASP.NET Core

漫画解读:唐僧师徒如何帮助大唐官网打造CDN+OSS完美架构?

巨侠说

Week11作业1

熊威

有了MDL锁视图,业务死锁从此一目了然

华为云开发者社区

MySQL 数据库 华为云 MDL锁视图 元数据

如何在3秒内打开一个网址

BabyKing

第十一周命题作业

菲尼克斯

Week11总结

熊威

架构师 0 期第十一周命题作业

何伟敏

技术揭秘:华为云DLI背后的核心计算引擎

华为云开发者社区

大数据 spark 数据湖 华为云 DLI

前端训练营(15)-动画

罗思雨

前端进阶训练营

数据隔离、访问授权,用好大数据为什么这么难?

华为云开发者社区

大数据 数据湖 华为云 DLI 数据隔离

第十一周学习总结

菲尼克斯

90%的开发都没搞懂的CI和CD!

ci DevOps 持续集成 持续交付 持续部署

市值管理机器人,刷量机器人,做市机器人

WX13823153201

市值管理机器人

CUDA,cuDNN,pytorch 在win10环境下的下载安装

Qx

教程 PyTorch

你真的了解 load 方法么?-InfoQ