本文为L_Ares个人写作,以任何形式转载请表明原文出处。
关于dyld
怎么关联到了objc
上面,就要先明白dyld
是什么?objc
又是什么?dyld加载流程中已经有过介绍。可以了解到dyld
是一个链接器,主要的作用还是链接动态库。
那为什么好好的动态库,你非要链接它?
- 第一是因为动态库所要负责的功能和思想都是有差别的,为了更好的模块化管理,所以不可能把代码都写在一个动态库里面。
- 第二个原因就比较主观,这么多的功能,一个人完成是很难的,一个组完成也是很难的,所以这是协作开发,那么每个组做的东西都不一样,但是功能需要有衔接性。
所以就需要dyld
这么一个动态链接器,把这些动态库都可以合成到项目中来,也即是加载到内存中来,供我们使用。
从这些条件也可以看出来,一个APP需要多个动态库就需要dyld
,那么APP的加载也就需要先把动态库搞定,所以APP的加载流程也是和dyld
息息相关的。
从dyld加载流程,发现了
- 在执行初始化主可执行程序
initializeMainExecutable
中,会执行recursiveInitialization
,进行通知的注册
和遍历循环初始化镜像(image)的实例
。 - 并且在
dyld通知的注册(notifySingle)
的时候利用一个函数指针sNotifyObjCInit
来链接到load_images(libobjc.A.dylib)
在这里关联上_objc_init
流程。 - 从而和
libobjc
动态库进行交互。然后进行doInitialization
,递归实例化镜像(image)
,并且必须第一个初始化libSystem
,然后按照libdispatch
--->libobjc
的顺序完成初始化。 - 根据上述的探索,知道了这些
镜像文件(images)
是从dyld
链接过来的。找到了_objc_init
中map_images(镜像映射)
、load_images(镜像加载)
,把动态库的内容加载到内存中以表的形式存储。
这个流程构成了dyld
和libobjc
之间的通讯。
本节总结一下dyld
到底是如何和libobjc
完成联动的。
一、_objc_init
根据上述的思路,想知道images
是怎么加载的,就要看libobjc
在程序加载的过程中是怎么去做的。所以就要从最开始的_objc_init
步骤开始。
还是要用objc4-781源码。搜索_objc_init
。
_objc_init
源码实现 :
/**
引导程序的初始化,dyld会注册镜像通知
在libobjc初始化的之前,_objc_init被libSystem唤醒
*/
void _objc_init(void)
{
//一堆判断条件,判断是否初始化了libobjc
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
//环境变量的初始化。读取可以影响运行时的环境变量,如果有需要的话,可以看环境变量的帮助文件export OBJC_HRLP = 1
environ_init();
//关于线程键(key)的一些绑定,设置静态键的析构函数等操作
tls_init();
//C++静态构造函数的运行,因为dyld在下面_dyld_objc_notify_register才会把镜像加载进来,才有静态构造函数进入
//但是_objc_init在这之前就被调用了,又需要C++的静态构造函数,所以自己先做了
static_init();
//runtime运行时环境的初始化。里面是unattachedCategories和allocatedClasses
//就是没有附着的分类和用objc_allocateClassPair分配的所有类(和元类)的表的初始化
runtime_init();
//libobjc的异常处理系统的初始化,比如下面会有一个注册异常的回调处理函数,用这个回调函数实现监控异常
exception_init();
//缓存系统的初始化
cache_init();
//回调机制的初始化。一般不会做什么,因为一般的初始化都是懒加载的,但是有一些进程不是,它们就很需要靠回调
_imp_implementationWithBlock_init();
/**
这个见过的,在`dyld`里面注册的一个回调函数(*sNotifyObjCInit这个函数指针要拿镜像)
- 这个函数仅仅供objc运行时使用
- 注册处理的程序。在映射(map_images)、取消映射(unmap_image)和初始化objc的镜像的时候调用
- dyld通过里面的函数指针把和objc_image_info相关的镜像文件数组回调给map函数
param:
(1) map_images : 映射镜像。在dyld将镜像文件加载到内存的时候,会调用map_images
(2) load_images: 加载镜像。在dyld初始化镜像文件的时候会调用
(3) unmap_image: 取消镜像的映射。在dyld将镜像移除的时候会调用。
*/
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
有很详细的注释了。
分别说一下前面这一堆的初始化,有一些是我们平常都用过的。
1. environ_init();
环境变量的初始化。读取可以影响运行时的环境变量。
两种方法可以获取OBJC
的环境变量。
- 在
终端(terminal)
里面输入export OBJC_HELP=1
可以看到相关的手册。如图 :
- 进入
environ_init();
,把红框里面的循环打印直接去掉判断条件,放到外面来,也可以打印出来,不过还是第一个方法正常一点。
效果 :
这些环境变量都是我们可以进行配置的,可以通过打开xcode
的Edit Scheme -- Run --Arguments -- Environment Variables
进行配置。
例子 :
比如拿一个nonpointerisa
来做个例子。在刚才控制台输出的环境变量中搜索nonpointer
,然后你会找到OBJC_DISABLE_NONPOINTER_ISA
。
nonpointer_isa
是纯净的isa
就是除了类的地址,其他的类信息,对象引用计数等等信息都是不加进去了。
因为isa
是union
,共用体,(不太清楚这个的可以看第二节isa),所以验证这个OBJC_DISABLE_NONPOINTER_ISA
会被使用的条件就是 :
将OBJC_DISABLE_NONPOINTER_ISA
设置成YES
,如果一个实例对象的isa
地址和它的类的地址一样,那么就证明这个environ_init();
的确是做了环境的动态加载。
先在
main.m
的main()
函数中初始化一个继承于NSObject
的自定义子类JDPerson
,并且初始化一个JDPerson
的实例对象person
。p/x JDPerson.class
查看JDPerson
的十六进制地址。x.4gx person
查看person
对象的前4个十六进制地址段存储的内容,拿到第一个内存段中的isa
地址。
- 修改
OBJC_DISABLE_NONPOINTER_ISA
为YES
,然后把xcode
缓存清理一下,免得有问题。
重新执行程序,继续查看JDPerson
类的内存地址和person
对象的isa
地址。
纯净的指针了吧。这样子是可以做到一定程度的优化内存的效果的。
其他的环境变量我会在下面的附录中贴出来,也会写几个常见的环境变量的设置和其作用。
2. tls_init();
关于线程键(key)的一些绑定,创建线程静态键(key),设置静态键的析构函数等操作。
void tls_init(void)
{
//这里面的宏都是libc库为我们保留的一些线程的键(key)
//这个宏是1
#if SUPPORT_DIRECT_THREAD_KEYS
//设置静态键的析构函数,比如说pthread_key_create()这个函数创建的是静态键
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
//创建线程静态键(key)
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
3. static_init();
系统级
C++
静态构造函数的运行。因为dyld
在下面的_dyld_objc_notify_register
才会把镜像加载进来,才有静态构造函数进入,但是_objc_init在这之前就被调用了,又需要C++
的静态构造函数,所以自己先初始化。
这里主要初始化的不是我们自己在代码写的那些C++
或者OC
的变量的初始化。而是系统级C++
构造函数,因为系统级的C++
会在我们自定义的函数之前就运行,所以有必要提前进行构造函数的运行,进行初始化。
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
}
4. runtime_init();
runtime
运行时环境的初始化。里面是unattachedCategories
和allocatedClasses
。即没有附着的分类和用objc_allocateClassPair
分配的所有类(和元类)的表的初始化。
void runtime_init(void)
{
//没有attach的分类的初始化
objc::unattachedCategories.init(32);
//使用`objc_allocateClassPair`进行分配的所有的类和元类的表,初始化
objc::allocatedClasses.init();
}
5. exception_init();
初始化
libobjc
的异常处理系统。注册异常处理的回调,从而监控对异常的处理。会被map_images
唤醒。
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
我们进入那个_objc_terminate
来看看这个传入的函数指针是什么。
也就是说,异常最开始是置空的。并且这个函数官方有注释告诉了我们,没有捕获到的异常回调由C++的terminate handler完成。
如何捕获异常信息,是经过如下判断 :
- 检查是否有活动异常。
- 如果有活动异常,检查它是否是
Objective-C
异常。 - 如果是
Objective-C
异常,就用该对象调用我们注册的回调。也就是第二个红框。 - 最后,调用前面的
terminate
处理程序。
从第三步可以看出来,那个e
调用的就是objc
注册的回调,跟进去看是不找到什么有用的线索的,全局搜索查找到 :
可以看到fn
是外界传进来的一个处理异常的函数,这个函数应该由app层面传过来,然后把这个uncaught_handler
指针指向fn
,这样就会把内部捕获的异常由外部传来的fn
处理掉。
5.1 Crash的分类 :
crash
发生的主要原因是因为接收到了有异常未处理的信号。那么未处理的异常一般来自于三个方面 :
kernel内核
其他进程
APP自己
所以Crash
的分类也是三种 :
Mach异常
: 这是最底层的内核异常对应的Crash
。用户态的开发者可以通过Mach API
设置thread
,task
,host的异常端口
来捕获Mach异常
。Unix信号异常
: 也称BSD信号
,如果开发者没有捕获到Mach异常
,那么host
层的ux_exception()
函数会将异常转换成相对应的Unix信号
,然后将通过threadSignal()
将这个异常的Unix信号
投递到出错的线程。信号的捕获则由signal(x, SignalHandler)
完成。NSException 应用级异常
: 它是未被捕获的Objective-C异常
,异常向自身发送SIGABRT
信号导致了Crash
。未捕获的Objective-C异常
可以通过try catch
进行捕获,也可以通过NSSetUncaughtExceptionHandler()
进行捕获,即是利用NSSetUncaughtExceptionHandler
实现线程保活,然后收集并上传崩溃信息的日志。
其中,第三种NSException
是我们在开发中可以进行crash
的拦截处理。通过在代码中加入NSSetUncaughtExceptionHandler
,利用它给系统传一个函数,比如我们定义一个函数getCrash
,然后NSSetUncaughtExceptionHandler(&getCrash)
,getCrash
函数可以是线程保活并且上传崩溃信息日志的函数,这样就可以在app
层进行处理。
6. cache_init();
缓存系统的初始化。
void cache_init()
{
//arm64架构下一定会初始化
#if HAVE_TASK_RESTARTABLE_RANGES
mach_msg_type_number_t count = 0;
kern_return_t kr;
while (objc_restartableRanges[count].location) {
count++;
}
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
if (kr == KERN_SUCCESS) return;
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
7. _imp_implementationWithBlock_init();
回调机制的初始化。一般不会做什么事情,因为一般的初始化都是懒加载的。但是有一些进程不一样,它们就很需要靠回调。
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
8. _dyld_objc_notify_register(&map_images, load_images, unmap_image);
上一节见过了。
在
dyld
里面注册的一个回调函数(*sNotifyObjCInit
这个函数指针要拿镜像)。
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
- 这个函数仅仅供
objc
运行时使用。 - 这是一个注册处理的程序。在映射(
map_images
)、取消映射(unmap_image
)和初始化objc
的镜像的时候调用 -
dyld
通过里面的函数指针把和objc_image_info
相关的镜像文件数组回调给map
函数
参数是三个函数指针。
-
map_images
: 映射镜像。在dyld
将镜像文件加载到内存的时候,会调用map_images
。 -
load_images
: 加载镜像。在dyld
初始化镜像文件的时候会调用。 -
unmap_image
: 取消镜像的映射。在dyld
将镜像移除的时候会调用。比如程序发生异常了,或者说程序停止了。
二、dyld与objc的关联
在(一)中,我们最后的一句代码,想要被调用起来,就需要dyld
那边有人调用,这样被传过去的map_images
的指针指向的函数和load_images
指针指向的函数才会被调用。这个上节就见过了,在notifySingle
里面,在往上走就是dyld
的主可执行文件的执行。也就是说,libobjc
进入到动态链接器是从_dyld_objc_notify_register
开始的。
那么从_dyld_objc_notify_register
就又回到了dyld的加载流程。
看_dyld_objc_notify_register
在libobjc
中的内容。
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
只有函数名,没有函数实现,但是在dyld
中搜索
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
找到了实现。而且可以把registerObjCNotifiers
就看作是_dyld_objc_notify_register
,因为_dyld_objc_notify_register
只有这一句代码。
于是又回到上一节的,只不过这次反推回去。
搜索registerObjCNotifiers
看红框里面的三个是什么。
和参数的类型完全一致。再看参数类型到底是什么。
函数指针,只不过是类型重命名定义。也就是说 :
-
sNotifyObjCMapped
有着mapped
的函数实现,等于有着map_images
的函数实现。 -
sNotifyObjCInit
有着init
的函数实现,等于有着load_images
的函数实现。 -
sNotifyObjCUnmapped
有着unmapped
的函数实现,等于有着unmap_image
的函数实现。
map_images
、load_images
、unmapped
是_dyld_objc_notify_register
的三个参数,是libobjc
传过来的。
sNotifyObjCMapped即map_images在dyld的调用
直接搜*sNotifyObjCMapped
。就这一个,带着*
才是使用了函数指针。不带*
的那是把函数指针给换了。
知道了是在notifyBatchPartial
中调用。
再看谁调用了notifyBatchPartial
,全局搜notifyBatchPartial
。看过的就不要看了,不要一个流程里面转圈,一共就4个,找了一下,找到了notifyBatch
。
再看谁调用了notifyBatch(
。
看红框,回到了dyld
加载的入口_main
。而且发现是在initializeMainExecutable
初始化主可执行程序之前就调用了,那么就说明*sNotifyObjCMapped
指向的map_images
在initializeMainExecutable
执行。
而上一节说过,initializeMainExecutable
--->runInitializers
--->processInitializers
--->recursiveInitialization
才找到了notifySingle
,而notifySingle
中才找到了*sNotifyObjCInit
,也就是才找到了load_images
。
那么就得出一个结论 :
libobjc
中的map_images
在load_images
前执行。
于是我们可以得到一个dyld
和libobjc
的联动关系图 :
附录 :环境变量
先说几个可能会是常用的。
DYLD_PRINT_STATISTICS
: 如设置为YES
。则控制台打印APP
加载的时长,包含整体加载时长和动态库加载时长。即是main
函数之前的启动时间(也就是pre-main耗时),知道这个可以尝试启动优化
。OBJC_DISABLE_NONPOINTER_ISA
: 如设置为YES
。则nonpointer = 0
,表示纯isa
,isa
共用体只有类的内存地址,不包含类的一些信息、对象的引用计数等信息。OBJC_PRINT_LOAD_METHODS
: 如设置为YES
。则打印Class
和Category
的+ (void)load
的调用信息。就是都有哪些类或者分类调用了+ (void)load
方法。NSDoubleLocalizedStrings
: 如设置为YES
。则可以查看翻译之后的文字的UI
是什么样子。NSShowNonLocalizedStrings
: 如设置为YES
。则经过翻译后的项目,依然没有被翻译的字符串会变成大写。
下面全是环境变量。
环境变量名 | 说明 |
---|---|
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 字段的访问 |