在我们的App代码中,XCode会自动创建一个main.m文件,其中定义了main函数
这里的main
函数是我们整个App的入口,它的调用时机甚至会早于AppDelegate
中的didFinishLaunching
回调。
因此我们会说,main函数是我们App程序的入口点函数
。
那么,我们App所运行的第一个函数,真的是main函数
吗?如果我们在XCode中设置符号断点void _objc_init(void)
,则会发现,在进入main函数
之前,其实系统还会调用void _objc_init(void)
方法:
这里的_objc_init
方法,实际上是runtime的入口函数。
void _objc_init(void)
也就是说,在App的main函数之前,系统会首先对App的runtime运行环境,做了一系列的初始化操作
。
而这个runtime入口函数,又是被谁调用起来的呢?答案是苹果的动态链接器dyld(the dynamic link editor)
。dyld是一个操作系统级的组件,它会负责iOS系统中每个App启动时的环境初始化以及动态库加载到内存等一系列操作。
在系统内核做好程序准备工作之后,交由dyld负责余下的工作。
在这里再重申一遍,runtime的入口函数是_objc_init
,它是在main函数之前被dyld调用的。而+load()方法,则是在main函数前被_objc_init
调用。今天,我们就来看一下,在main函数之前,runtime究竟做了哪些初始化工作。
Mach-O格式
在深入了解_objc_init
的实现之前,我们需要先了解iOS系统中可执行文件的文件格式:Mach-O格式。关于Mach-O格式,我们在Objective-C runtime机制(前传)——Mach-O格式,Objective-C runtime机制(前传2)——Mach-O格式和runtime中以及介绍过。
我们可以在XCode 工程 product文件夹下找到RuntimeEnter.app工程,用finder打开所在目录,其实RuntimeEnter.app是一个压缩包,用鼠标右键选择show Package Contents ,可以看到下面有这些文件,其中和我们工程同名的可运行程序就是Mach-O格式的可运行文件:
_objc_init
我们在iOS App中设置符号断点_objc_init
,则在App启动时(进入main函数之前),会进入如下调用堆栈:
可以看到,其底层是由dyld调用起来的。关于dyld我们不去多说,让我们看一下runtime中_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?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
除去上面一堆init方法,我们重点关注
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
_dyld_objc_notify_register
方法注册了对dyld中关于加载images的事件回调:
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
分别注册了那些事件呢?根据注释,我们可以知道,共注册了三个事件的回调:
- _dyld_objc_notify_mapped : OC image被加载映射到内存(+load()方法在此时被调用)
- _dyld_objc_notify_init : OC image被init时
- _dyld_objc_notify_unmapped : OC image被移除内存时
以上三个回调类型是用的函数指针,定义为
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);
_dyld_objc_notify_mapped
当image被dyld加载到内存后,会调用回调_dyld_objc_notify_mapped
。在runtime中,对应的函数是:
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
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;
// Perform first-time initialization if necessary.
// This function is called before ordinary library initializers.
// fixme defer initialization until an objc-using image is found?
if (firstTime) {
preopt_init();
}
if (PrintImages) {
_objc_inform("IMAGES: processing %u newly-mapped images...\n", mhCount);
}
// Find all images with Objective-C metadata.
hCount = 0;
// 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];
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
// Size some data structures based on main executable's size
#if __OBJC2__
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
#else
_getObjcSelectorRefs(hi, &selrefCount);
#endif
#if SUPPORT_GC_COMPAT
// Halt if this is a GC app.
if (shouldRejectGCApp(hi)) {
_objc_fatal_w