程序和進(jìn)程
廣義上的程序就是一個(gè)靜態(tài)的可執(zhí)行文件,是由一個(gè)已經(jīng)編譯好的指令和數(shù)據(jù)集合的一個(gè)文件。就像是我們通過(guò)Xcode編譯好的macho文件。而進(jìn)程則是一個(gè)動(dòng)態(tài)的概念,是程序的運(yùn)行時(shí)的一個(gè)過(guò)程。
虛擬地址空間
每個(gè)進(jìn)程運(yùn)行的時(shí)候都有自己獨(dú)立的虛擬地址空間,這個(gè)空間的大小是由計(jì)算機(jī)的硬件決定的,比如在32位硬件平臺(tái)上,它的尋址空間大小是2^32 - 1,現(xiàn)在iPhone都是64位的,尋址空間為2^64-1 。
冷啟動(dòng)和熱啟動(dòng)
熱啟動(dòng)是由于某種原因,APP的狀態(tài)由running切換為suspend,但是此時(shí)APP并沒(méi)有被系統(tǒng)kill掉,當(dāng)我們?cè)俅伟袮PP切換到前臺(tái)的時(shí)候,APP會(huì)恢復(fù)之前的狀態(tài)繼續(xù)運(yùn)行,這種就是熱啟動(dòng),我們平時(shí)所說(shuō)的APP在后臺(tái)的存活時(shí)間,其實(shí)就是APP能執(zhí)行熱啟動(dòng)的最大時(shí)間間隔。而冷啟動(dòng)則是APP從被加載到內(nèi)存到運(yùn)行的狀態(tài),下面我們要講的主要是冷啟動(dòng)。
孤獨(dú)的main函數(shù)
大概是從我們學(xué)習(xí)編程開(kāi)始就知道main函數(shù)是程序的入口,但是真的是這樣嗎?在平時(shí)的面試過(guò)程中我也有問(wèn)一些面試者這個(gè)問(wèn)題,但是回答的都比較模糊。其實(shí)我們通過(guò)代碼可以看出,在iOS里面 main只是簡(jiǎn)單的返回一個(gè)UIApplicationMain對(duì)象,里面的有一個(gè)重要的參數(shù)就是實(shí)現(xiàn)了UIApplicationDelegate代理的類(lèi)。
// UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nonnull * _Null_unspecified argv, NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([UIAppDelegate class]));
}
}
APP啟動(dòng)流程時(shí)間主要包括兩部分,main函數(shù)之前和main函數(shù)執(zhí)行之后到-(BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執(zhí)行完成。其中main函數(shù)執(zhí)行之后優(yōu)化主要是讓上面的方法盡快執(zhí)行完,不要有什么block主線(xiàn)程的操作。所以我們可以看出,其實(shí)在main里面處理的事情還是比較簡(jiǎn)單的。最重要的還是在main函數(shù)執(zhí)行之前。
概述
從WWDC的視頻我們可以得出簡(jiǎn)單的結(jié)論:系統(tǒng)先讀取App的可執(zhí)行文件,從里面獲得dyld的路徑,然后加載dyld,當(dāng)所有依賴(lài)庫(kù)的初始化后,輪到最后一位(程序可執(zhí)行文件)進(jìn)行初始化,在這時(shí)runtime會(huì)對(duì)項(xiàng)目中所有類(lèi)進(jìn)行類(lèi)結(jié)構(gòu)初始化,然后調(diào)用所有的load方法。最后dyld返回main函數(shù)地址,main函數(shù)被調(diào)用,我們便來(lái)到了熟悉的程序入口。
啟動(dòng)時(shí)間
在Xcode中可以通過(guò)設(shè)置DYLD_PRINT_STATISTICS環(huán)境變量來(lái)查看APP的啟動(dòng)時(shí)間詳細(xì)信息:

然后就可以在控制臺(tái)看到如下信息:
Total pre-main time: 282.69 milliseconds (100.0%)
dylib loading time: 107.37 milliseconds (37.9%)
rebase/binding time: 44.92 milliseconds (15.8%)
ObjC setup time: 64.72 milliseconds (22.8%)
initializer time: 65.56 milliseconds (23.1%)
slowest intializers :
libSystem.dylib : 7.98 milliseconds (2.8%)
libMainThreadChecker.dylib : 23.55 milliseconds (8.3%)
AFNetworking : 19.46 milliseconds (6.8%)
從上面可以看出時(shí)間區(qū)域主要分為下面幾個(gè)部分:
- dylib loading time
- rebase/binding time
- ObjC setup time
- initializer time
dyld
(the dynamic link editor)動(dòng)態(tài)鏈接器,是一個(gè)專(zhuān)門(mén)用來(lái)加載動(dòng)態(tài)鏈接庫(kù)的庫(kù),它是開(kāi)源的,源碼在這里。在 xnu 內(nèi)核為程序啟動(dòng)做好準(zhǔn)備后,執(zhí)行由內(nèi)核態(tài)切換到用戶(hù)態(tài),由dyld完成后面的加載工作,dyld的主要是初始化運(yùn)行環(huán)境,開(kāi)啟緩存策略,加載程序依賴(lài)的動(dòng)態(tài)庫(kù)(其中也包含我們的可執(zhí)行文件),并對(duì)這些庫(kù)進(jìn)行鏈接(主要是rebaseing和binding),最后調(diào)用每個(gè)依賴(lài)庫(kù)的初始化方法,在這一步,runtime被初始化。

ImageLoader是用于加載可執(zhí)行文件格式的類(lèi),程序中對(duì)應(yīng)實(shí)例可簡(jiǎn)稱(chēng)為image(如程序可執(zhí)行文件macho,F(xiàn)ramework,bundle等)。
Rebasing 和 Binding
ASLR(Address Space Layout Randomization),地址空間布局隨機(jī)化。在A(yíng)SLR技術(shù)出現(xiàn)之前,程序都是在固定的地址加載的,這樣hacker可以知道程序里面某個(gè)函數(shù)的具體地址,植入某些惡意代碼,修改函數(shù)的地址等,帶來(lái)了很多的危險(xiǎn)性。ASLR就是為了解決這個(gè)的,程序每次啟動(dòng)后地址都會(huì)隨機(jī)變化,這樣程序里所有的代碼地址都需要需要重新對(duì)進(jìn)行計(jì)算修復(fù)才能正常訪(fǎng)問(wèn)。rebasing這一步主要就是調(diào)整鏡像內(nèi)部指針的指向。
Binding:將指針指向鏡像外部的內(nèi)容。
ObjC setup
上面最后一步調(diào)用的objc_init方法,這個(gè)事runtime的初始化方法,在這個(gè)方法里面主要的操作就是加載類(lèi):
/***********************************************************************
* _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);
}
_dyld_objc_notify_register(&map_images, load_images, unmap_image);向dyld注冊(cè)了一個(gè)通知事件,當(dāng)有新的image加載到內(nèi)存的時(shí)候,就會(huì)觸發(fā)load_images方法,這個(gè)方法里面就是加載對(duì)應(yīng)image里面的類(lèi),并調(diào)用load方法。
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first.
* Category +load methods are not called until after the parent class's +load.
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
* (a) there are more classes to load, OR
* (b) there are some potential category +loads that have
* still never been attempted.
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
*
* Locking: loadMethodLock must be held by the caller
* All other locks must not be held.
**********************************************************************/
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;
}
如果有繼承的類(lèi),那么會(huì)先調(diào)用父類(lèi)的load方法,然后調(diào)用子類(lèi)的,但是在load里面不能調(diào)用[super load]。最后才是調(diào)用category的load方法。所以在這一步,所有的load都會(huì)被調(diào)用到。
C++ initializer
在這一步,如果我們代碼里面使用了clang的__attribute__((constructor))構(gòu)造方法,都會(huì)調(diào)用到。
優(yōu)化點(diǎn)
那么如何盡可能的減少pre-main花費(fèi)的時(shí)間呢,主要就從上面給出的幾個(gè)階段下手:
動(dòng)態(tài)庫(kù)加載的時(shí)間優(yōu)化。每個(gè)App都進(jìn)行動(dòng)態(tài)庫(kù)加載,其中系統(tǒng)級(jí)別的動(dòng)態(tài)庫(kù)占據(jù)了絕大數(shù),而針對(duì)系統(tǒng)級(jí)別的動(dòng)態(tài)庫(kù)都是經(jīng)過(guò)系統(tǒng)高度優(yōu)化的,不用擔(dān)心時(shí)間的花費(fèi)。開(kāi)發(fā)者應(yīng)該關(guān)注于自己集成到App的那些動(dòng)態(tài)庫(kù),這也是最能消耗加載時(shí)間的地方。對(duì)此Apple建議減少在A(yíng)pp里開(kāi)發(fā)者的動(dòng)態(tài)庫(kù)集成或者有可能地將其多個(gè)動(dòng)態(tài)庫(kù)最終集成一個(gè)動(dòng)態(tài)庫(kù)后進(jìn)行導(dǎo)入, 盡量保證將App現(xiàn)有的非系統(tǒng)級(jí)的動(dòng)態(tài)庫(kù)個(gè)數(shù)保證在6個(gè)以?xún)?nèi);
(Rebase/binding)時(shí)間優(yōu)化。減少App的Objective-C類(lèi),分類(lèi)和Selector的個(gè)數(shù)。這樣做主要是為了加快程序的整個(gè)動(dòng)態(tài)鏈接, 在進(jìn)行動(dòng)態(tài)庫(kù)的重定位和綁定(Rebase/binding)過(guò)程中減少指針修正的使用,加快程序機(jī)器碼的生成;
objc init 優(yōu)化。用+initialize方法替換+load方法,從而加快所有類(lèi)文件的加載速度。