引言
上一篇我们探索到了dyld的加载流程。了解dyld与objc通信的_objc_init函数。我们了解了load_images赋值给dyld内部的sNotifyObjCInit,并回调执行所有+load方法。_objc_init中的_dyld_objc_notify_register函数仍有两个参数未讲到,本文将探究dyld与objc关联的_objc_init函数内部流程。
_objc_init
_objc_init函数,是引导初始化程序,使用dyld注册我们的image镜像通知者,并由libSystem在库初始化调用。在_objc_init函数内打个断点,观察左边的调用栈,可知道_objc_init的调起过程。

简而言之:
dyld→libSystem→libdispatch →libobjc。交汇点就是_objc_init函数。该函数内部如下(已在对应函数添加了简单说明):
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
1、读取影响运行时的环境变量。如果需要,还可以打印环境变量帮助
environ_init();
2、关于线程key的绑定 - 比如每线程数据的析构函数
tls_init();
3、运行C ++静态构造函数。在dyld调用我们的静态构造函数之前,`libc` 会调用 _objc_init(), 因此我们必须自己做
static_init();
4、runtime运行时环境初始化,里面主要是:unattachedCategories,allocatedClasses
runtime_init();
5、初始化libobjc的异常处理系统
exception_init();
#if __OBJC2__
6、缓存条件初始化
cache_t::init();
#endif
7、启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
_imp_implementationWithBlock_init();
8、注册在映射、取消映射和初始化 objc 的镜像时要调用的处理程序。Dyld 将使用包含 objc-image-info 部分的images镜像数组回调“映射”函数。那些 dylib 的镜像将自动增加 ref-counts,因此 objc 将不再需要对它们调用 dlopen() 以防止它们被卸载。在调用 _dyld_objc_notify_register() 期间,dyld 将使用已加载的 objc image镜像调用“映射”函数。在以后的任何 dlopen() 调用中,dyld 还将调用“映射”函数。当 dyld 在该image镜像中被称为初始值设定项时,Dyld 将调用“init”函数。这是当 objc 调用该image镜像中的任何 +load 方法时。
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
【 environ_init 】
environ_init内部会读取影响运行时的环境变量。如果需要,还可以打印环境变量帮助。可在environ_init内部看到如下代码:

environ_init内部的核心,就是这个for循环所能打印的东西。environ_init调试过程如下:去掉
if (PrintHelp || PrintOptions)条件,即可打印所有环境变量:
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
_objc_inform("%s: %s", opt->env, opt->help);
_objc_inform("%s is set", opt->env);
}
运行结果如下:

环境变量在Xcode对应的位置为:Edit Scheme → Run → Arguments → Environment

environ_init测试1:OBJC_PRINT_LOAD_METHODS:打印+load方法的位置。
按照上图方法,添加OBJC_PRINT_LOAD_METHODS,并勾选。在QLPerson.m中添加+load方法。运行程序。


environ_init测试2:OBJC_DISABLE_NONPOINTER_ISA:关闭nonpointer_isa
1)启用nonpointer_isa:main.m中创建person对象,打个断点并运行:

2)禁用
nonpointer_isa:
运行结果:

3)所有环境变量表:
环境变量名 说明
OBJC_PRINT_OPTIONS 输出OBJC已设置的选项
OBJC_PRINT_IMAGES 输出已load的image信息
OBJC_PRINT_LOAD_METHODS 打印 Class 及 Category 的 + (void)load 方法的调用信息
OBJC_PRINT_INITIALIZE_METHODS 打印 Class 的 + (void)initialize 的调用信息
OBJC_PRINT_RESOLVED_METHODS 打印通过 +resolveClassMethod: 或 +resolveInstanceMethod: 生成的类方法
OBJC_PRINT_CLASS_SETUP 打印 Class 及 Category 的设置过程
OBJC_PRINT_PROTOCOL_SETUP 打印 Protocol 的设置过程
OBJC_PRINT_IVAR_SETUP 打印 Ivar 的设置过程
OBJC_PRINT_VTABLE_SETUP 打印 vtable 的设置过程
OBJC_PRINT_VTABLE_IMAGES 打印 vtable 被覆盖的方法
OBJC_PRINT_CACHE_SETUP 打印方法缓存的设置过程
OBJC_PRINT_FUTURE_CLASSES 打印从 CFType 无缝转换到 NSObject 将要使用的类(如 CFArrayRef 到 NSArray * )
OBJC_PRINT_GC 打印一些垃圾回收操作
OBJC_PRINT_PREOPTIMIZATION 打印 dyld 共享缓存优化前的问候语
OBJC_PRINT_CXX_CTORS 打印类实例中的 C++ 对象的构造与析构调用
OBJC_PRINT_EXCEPTIONS 打印异常处理
OBJC_PRINT_EXCEPTION_THROW 打印所有异常抛出时的 Backtrace
OBJC_PRINT_ALT_HANDLERS 打印 alt 操作异常处理
OBJC_PRINT_REPLACED_METHODS 打印被 Category 替换的方法
OBJC_PRINT_DEPRECATION_WARNINGS 打印所有过时的方法调用
OBJC_PRINT_POOL_HIGHWATER 打印 autoreleasepool 高水位警告
OBJC_PRINT_CUSTOM_RR 打印含有未优化的自定义 retain/release 方法的类
OBJC_PRINT_CUSTOM_AWZ 打印含有未优化的自定义 allocWithZone 方法的类
OBJC_PRINT_RAW_ISA 打印需要访问原始 isa 指针的类
OBJC_DEBUG_UNLOAD 卸载有不良行为的 Bundle 时打印警告
OBJC_DEBUG_FRAGILE_SUPERCLASSES 当子类可能被对父类的修改破坏时打印警告
OBJC_DEBUG_FINALIZERS 警告实现了 -dealloc 却没有实现 -finalize 的类
OBJC_DEBUG_NIL_SYNC 警告 @synchronized(nil) 调用,这种情况不会加锁
OBJC_DEBUG_NONFRAGILE_IVARS 打印突发地重新布置 non-fragile ivars 的行为
OBJC_DEBUG_ALT_HANDLERS 记录更多的 alt 操作错误信息
OBJC_DEBUG_MISSING_POOLS 警告没有 pool 的情况下使用 autorelease,可能内存泄漏
OBJC_DEBUG_DUPLICATE_CLASSES 当出现类重名时停机
OBJC_USE_INTERNAL_ZONE 在一个专用的 malloc 区分配运行时数据
OBJC_DISABLE_GC 强行关闭自动垃圾回收,即使可执行文件需要垃圾回收
OBJC_DISABLE_VTABLES 关闭 vtable 分发
OBJC_DISABLE_PREOPTIMIZATION 关闭 dyld 共享缓存优化前的问候语
OBJC_DISABLE_TAGGED_POINTERS 关闭 NSNumber 等的 tagged pointer 优化
OBJC_DISABLE_NONPOINTER_ISA 关闭 non-pointer isa 字段的访问
来源:style_月月
【 tls_init 】
tls_init是关于线程key的绑定 - 比如每线程数据的析构函数
源码如下:
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
【 static_init 】
这里会运行 C++ 的静态构造函数,在 dyld 调用我们的静态构造函数之前,libc会调用 _objc_init,所以这里我们必须自己来处理,并且这里只会初始化系统内置的 C++ 静态构造函数,我们自己代码里面写的并不会在这里初始化。
换句话说:
static_init主要是运行系统级别的C++静态构造函数,在dyld调用我们自定义的静态构造函数之前,libc调用_objc_init方法,即系统级别的C++构造函数 先于 自定义的C++构造函数 运行
/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors,
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
UnsignedInitializer init(offsets[i]);
init();
}
}
【 runtime_init 】
运行时的初始化,主要分为两个操作:
1、开辟存储分类的表:objc::unattachedCategories.init(32);
2、开辟存储类的表:objc::allocatedClasses.init();
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
【 exception_init 】
主要是初始化libobjc的异常处理系统。由map_image()函数调起。源码如下:
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
1、old_terminate的定义是:static void (*old_terminate)(void) = nil;
2、_objc_terminate是一个函数名,其对应实现如下:
/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*
* The uncaught exception callback is implemented as a std::terminate handler.
* 1. Check if there's an active exception
* 2. If so, check if it's an Objective-C exception
* 3. If so, call our registered callback with the object.
* 4. Finally, call the previous terminate handler.
**********************************************************************/
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
// No current exception.
(*old_terminate)();
}
else {
// There is a current exception. Check if it's an objc exception.
@try {
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}
说明:
a、未捕获的异常回调作为 std::terminate 处理程序实现。
b、检查是否有处于活动状态的异常
c、如果是,请检查它是否是 Objective-C 异常
d、如果是这样,回调到我们已经注册好的回调函数。
e、最后,调用前一个终止处理程序。
当异常发生时,回来到static void _objc_terminate(void)函数,最终由(*uncaught_handler)((id)e);抛出异常。uncaught_handler的探索与dyld中的sNotifyObjCInit类似。由他人传进来
1、全局搜索uncaught_handler ,找到uncaught_handler = fn这个赋值位置。则我们继续找出上层调用即可

2、我们平时抓取异常用到的
NSSetUncaughtExceptionHandler,此处的void QLExceptionHandlers函数,赋值给fn,fn赋值给uncaught_handler。
+ (void)configExceptionHandler{
// uncaught_handler() = fn = QLExceptionHandlers
// objc_setUncaughtExceptionHandler()
NSSetUncaughtExceptionHandler(&QLExceptionHandlers);
}
/// Exception
void QLExceptionHandlers(NSException *exception) {
NSLog(@"%s",__func__);
}
【 cache_t::init() 】
cache_t::init()主要是:缓存条件初始化
【 _imp_implementationWithBlock_init】
启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
void _imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
在某些进程中急切地加载 libobjc-trampolines.dylib。
一些程序(最显着的是旧版本的嵌入式 Chromium 使用的 QtWebEngineProcess)启用了一个高度限制的沙箱配置文件,该配置文件会阻止访问该 dylib。
如果有任何东西调用了 imp_implementationWithBlock(就像 AppKit 开始做的那样),那么我们将在尝试加载它时崩溃。在启用沙箱配置文件并阻止它之前,在此处加载它会对其进行设置。
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
【 _dyld_objc_notify_register】
注册在映射、取消映射和初始化 dyld 的镜像时要调用的处理程序。dyld 将使用包含 objc-image-info 部分的images镜像数组回调“映射”函数。那些 dylib 的镜像将自动增加 ref-counts,因此 objc 将不再需要对它们调用 dlopen() 以防止它们被卸载。在调用 _dyld_objc_notify_register() 期间,dyld 将使用已加载的 objc image镜像调用“映射”函数。在以后的任何 dlopen()调用中,dyld 还将调用“映射”函数。当 dyld 在该image镜像中被称为初始值设定项时,dyld 将调用init函数。这是当 objc 调用该image镜像中的任何 +load 方法时。
load_images
(当dyld把镜像文件初始化时,会调用此函数)
上篇文章,我们探索到dyld通过_dyld_objc_notify_register注册load_images函数,赋值给init,然后在递归初始化中调用notifySingle时回调(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());执行load_images函数,加载所有+load方法。(sNotifyObjCInit = init = load_images)

map_images
(当dyld把镜像文件加载入内存时,会调用此函数)
_dyld_objc_notify_register的第一个参数&map_images传的是map_images函数的指针,原因如下:
因为这个map_images很重要,必须要保证dyld内部加载镜像与map_image的实现保持同步发生变化,内容一致。map_images是映射所有镜像文件,是一个比较耗时的过程,如果两者之间没有发生同步变化,会发生错乱现象,导致整个程序发生错乱。而load_images的流程只是为了回调objc加载+load方法,无需传函数指针。
1、流程(dyld):与load_images一样,dyld通过_dyld_objc_notify_register注册&map_images指针,赋值给sNotifyObjCMapped,并立即调用notifyBatchPartial函数,执行(*sNotifyObjCMapped)(objcImageCount, paths, mhs);回调。同时调用notifyMonitoringDyld,并在notifyMonitoringDyld内部递归监控dyld。

notifyMonitoringDyld源码如下(递归):
static void notifyMonitoringDyld(bool unloading, unsigned imageCount, const struct mach_header* loadAddresses[], const char* imagePaths[])
{
dyld3::ScopedTimer(DBG_DYLD_REMOTE_IMAGE_NOTIFIER, 0, 0, 0);
RemoteNotificationResponder responder;
if (!responder.active()) { return; }
notifyMonitoringDyld(responder, unloading, imageCount, loadAddresses, imagePaths);
}
2、map_images实现(objc):进入源码可见
处理由被dyld 映射的给定的image镜像。
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);
}

map_images_nolock内部为执行所有类注册和修复(或延迟发现丢失的超类等),并调用 +load 方法。
unmap_image
(当dyld把镜像文件删除时,会调用此函数)
处理即将被 dyld 取消映射的给定的镜像文件。与map_images和load_images流程类似,最终dyld在removeImage函数中,回调(*sNotifyObjCUnmapped)(image->getRealPath(), image->machHeader());其内部如下:
处理即将被 dyld 取消映射的给定的镜像文件。
void
unmap_image(const char *path __unused, const struct mach_header *mh)
{
recursive_mutex_locker_t lock(loadMethodLock);
mutex_locker_t lock2(runtimeLock);
unmap_image_nolock(mh);
}
objc中实现删除镜像文件函数unmap_image_nolock:

总结:
dyld像objc暴露了API的函数:_dyld_objc_notify_register,这个函数接收来自objc方的实现,通过map_images,load_images,unmap_image,这三个参数来实现dyld与objc的通信。流程图如下所示。
