引言
上一篇我们探索到了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
的通信。流程图如下所示。