App进入Main之前做的事情

我们知道,程序的”启动入口”在main函数,但这是从我们的程序逻辑来看的。实际上操作系统在加载我们的二进制可执行文件时,执行到main之前做了不少事情。比如oc的runtime运行库环境就是在main之前建立好的。
我们来一起看看在main之前到底做了什么。

先从OC的runtime的入口函数着手,_objc_init,我们符号断点一下,可以看到

可以看到入口在_dyld_start,dyld是开源的,代码在这

dyld

dyld在苹果里有介绍,在这里也有介绍,wiki在这里
简单来说dyld是来负责iOS/MacOSX应用程序动态链接库的运作的,类似windows上的dll技术。

ImageLoader

这里的ImageLoader指的是镜像加载,负责从镜像文件中取出各种我们需要的信息,主要是决议了一些需要在动态链接时的符号。
我们的程序在运行的时候除了自己的可执行镜像,还会依赖许多其他的库,有些在我们编译链接时给link进去了,这是静态链接,比如Podfile里面配置的第三方库(framework,.a文件,.so文件),有些是我们的程序启动时链接的库,比如最常见的libobjc.A.dylib,里面包含了objc的runtime。

那么我们的iOS/MacOSX应用程序是如何动态加载的呢?还是从刚才的堆栈和对照dyld源码runtime源码探索

搜索_dyld_start发现启动的部分在文件dyldStartup.s,里面是一坨汇编,就不细看了。
循着堆栈的踪迹,看到在函数dyld::_main这里,根据源码中的注释可以看到,dyld::_main是dyld的入口,内核在加载dyld时,从 dyld_start -> dyld::main ``dyld:main做一些注册操作并返回真正的应用程序的main函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
....
}

dyld在加载到libdispatch.dylib时,会调用到_objc_init,看下这个函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//objc-os.mm
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();

// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized,
0/*not batch*/, &load_images);
}

前面是一些环境的初始化,重点看后面的dyld_xxx方法,这几个方法向dyld注册了回调

  1. _dyld_register_func_for_remove_image(&unmap_image);就如注释所说,先注册一个remove_image的方法,为了避免用户在+(void)load里面去unmap一些东西。
  2. dyld_register_image_state_change_handler这里注册了一个回调,当dyld_image状态为dyld_image_state_bound时,触发回调。
  3. dyld_register_image_state_change_handler 这里注册了一个回调,当dyld_image状态为dyld_image_state_dependents_initialized时,触发回调。
    当我们在category或者类中添加+(void) load方法时,会在这个时机调用。
    具体我们在load_images中可以看到有如下代码片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
  const char *
load_images(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
bool found;

// Return without taking locks if there are no +load methods here.
found = false;
//这里会找所有的class以及category的列表里面是否有+load方法
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);

// Discover load methods
// 搜集所有的+load方法
{
rwlock_writer_t lock2(runtimeLock);
found = load_images_nolock(state, infoCount, infoList);
}
//调用+load方法
if (found) {
call_load_methods();
}
...
}

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;
// 搜集镜像中的load方法
prepare_load_methods(mhdr);
found = YES;
}

return found;
}



void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;

runtimeLock.assertWriting();

//1. 先搜集class中的+load方法
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}

category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
//2. 再搜集category中的+load方法
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);
}
}

//递归实现,可以看到这里是先找父类中的+load方法,并放入到一个列表里面,所以在最终的class的load方法列表里面,父类的+load方法是先于子类的+load方法的
//大家可以看下add_class_to_loadable_list的实现,里面的分配内存策略使用了指数增长策略
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize

if (cls->data()->flags & RW_LOADED) return;

// Ensure superclass-first ordering
schedule_class_load(cls->superclass);

add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

为什么要重复调用call_class_loadscall_category_loads呢,答案是由于这个函数被设计为可重入的,重入时这个函数直接return。这样当我们在+load函数中又去map了其他的镜像,这个镜像又有许多其他新的包含+load的类和分类,这里就是为了保证新添加的+load可以调用到。
call_class_loads中也可以看到,是从之前保存类的+load方法的列表头部开始一个一个取出来并执行的,所有我们的父类的+load要先于子类的+load执行。
call_load_methods执行完毕后,所有+load方法就执行完毕了。

在上面第二步有一个回调map_2_images,我们来看看这个做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
  const char *
map_images_nolock(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
static bool firstTime = YES;
static bool wantsGC = NO;
uint32_t i;
header_info *hi;
header_info *hList[infoCount];
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();
}

// Find all images with Objective-C metadata.
hCount = 0;
i = infoCount;
//收集infoList中的所有header信息并存储在一个单链表
while (i--) {
const headerType *mhdr = (headerType *)infoList[i].imageLoadAddress;

hi = addHeader(mhdr);
if (!hi) {
// no objc data in this entry
continue;
}

if (mhdr->filetype == MH_EXECUTE) {
// Size some data structures based on main executable's size
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
}

hList[hCount++] = hi;

}

if (firstTime) {
//注册一些我们常用的函数,比如load,alloc,dealloc等等
//并使用selrefCount的值 去创建一个capacity为这个值的全局Map
sel_init(wantsGC, selrefCount);
//初始化AutoreleasePool,和一个SideTable,SideTable是干啥的也没弄清楚,暂时不管,这里不重要
arr_init();
}
//重点是这个,这个会去读取hList里面的所有的image
//中的classes,categories,protocols并存储在内存中
//使我们之后使用runtime方法提供了前提,比如方法替换,消息转发等等
_read_images(hList, hCount);

firstTime = NO;

return NULL;
}

看完回调的实现后,回到dyld在加载libdipsatch.dylib后,再加载其他镜像时,就会跑到这些回调,所有这些镜像内部的OC对象,方法,分类,协议等信息都会在runtime中存储好。一切就绪后,就开始跑到我们main函数了。