程序和進(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
代理的類故俐。
// 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主線程的操作辑舷。所以我們可以看出,其實(shí)在main
里面處理的事情還是比較簡(jiǎn)單的槽片。最重要的還是在main
函數(shù)執(zhí)行之前何缓。
概述
從WWDC的視頻我們可以得出簡(jiǎn)單的結(jié)論:系統(tǒng)先讀取App的可執(zhí)行文件,從里面獲得dyld的路徑还栓,然后加載dyld碌廓,當(dāng)所有依賴庫(kù)的初始化后,輪到最后一位(程序可執(zhí)行文件)進(jìn)行初始化剩盒,在這時(shí)runtime會(huì)對(duì)項(xiàng)目中所有類進(jìn)行類結(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è)專門用來(lái)加載動(dòng)態(tài)鏈接庫(kù)的庫(kù)异袄,它是開(kāi)源的,源碼在這里玛臂。在 xnu 內(nèi)核為程序啟動(dòng)做好準(zhǔn)備后烤蜕,執(zhí)行由內(nèi)核態(tài)切換到用戶態(tài),由dyld完成后面的加載工作迹冤,dyld的主要是初始化運(yùn)行環(huán)境讽营,開(kāi)啟緩存策略,加載程序依賴的動(dòng)態(tài)庫(kù)(其中也包含我們的可執(zhí)行文件)叁巨,并對(duì)這些庫(kù)進(jìn)行鏈接(主要是rebaseing和binding)斑匪,最后調(diào)用每個(gè)依賴庫(kù)的初始化方法,在這一步,runtime被初始化蚀瘸。
ImageLoader
是用于加載可執(zhí)行文件格式的類狡蝶,程序中對(duì)應(yīng)實(shí)例可簡(jiǎn)稱為image(如程序可執(zhí)行文件macho,F(xiàn)ramework贮勃,bundle等)贪惹。
Rebasing 和 Binding
ASLR(Address Space Layout Randomization),地址空間布局隨機(jī)化寂嘉。在ASLR技術(shù)出現(xiàn)之前奏瞬,程序都是在固定的地址加載的,這樣hacker可以知道程序里面某個(gè)函數(shù)的具體地址泉孩,植入某些惡意代碼硼端,修改函數(shù)的地址等,帶來(lái)了很多的危險(xiǎn)性寓搬。ASLR就是為了解決這個(gè)的珍昨,程序每次啟動(dòng)后地址都會(huì)隨機(jī)變化,這樣程序里所有的代碼地址都需要需要重新對(duì)進(jìn)行計(jì)算修復(fù)才能正常訪問(wèn)句喷。rebasing這一步主要就是調(diào)整鏡像內(nèi)部指針的指向镣典。
Binding:將指針指向鏡像外部的內(nèi)容。
ObjC setup
上面最后一步調(diào)用的objc_init
方法唾琼,這個(gè)事runtime的初始化方法兄春,在這個(gè)方法里面主要的操作就是加載類:
/***********************************************************************
* _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里面的類,并調(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;
}
如果有繼承的類涌乳,那么會(huì)先調(diào)用父類的load
方法,然后調(diào)用子類的甜癞,但是在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建議減少在App里開(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è)以內(nèi)酸些;
(Rebase/binding)時(shí)間優(yōu)化宰译。減少App的Objective-C類,分類和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方法,從而加快所有類文件的加載速度闯第。