iOS app 啟動(dòng)時(shí)間優(yōu)化分析

程序和進(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ì)信息:

statistics.png

然后就可以在控制臺(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被初始化蚀瘸。

obj_init.png

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方法,從而加快所有類文件的加載速度闯第。

refrence

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市缀拭,隨后出現(xiàn)的幾起案子咳短,更是在濱河造成了極大的恐慌,老刑警劉巖蛛淋,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咙好,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡褐荷,警方通過(guò)查閱死者的電腦和手機(jī)敷扫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)诚卸,“玉大人葵第,你說(shuō)我怎么就攤上這事『夏纾” “怎么了卒密?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)棠赛。 經(jīng)常有香客問(wèn)我哮奇,道長(zhǎng),這世上最難降的妖魔是什么睛约? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任鼎俘,我火速辦了婚禮辩涝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捉邢。我一直安慰自己,他們只是感情好商膊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布晕拆。 她就那樣靜靜地躺著藐翎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吝镣。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音未舟,去河邊找鬼裕膀。 笑死,一個(gè)胖子當(dāng)著我的面吹牛寸齐,可吹牛的內(nèi)容都是我干的抄谐。 我是一名探鬼主播蛹含,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼浦箱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼酷窥!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起妆棒,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤募逞,失蹤者是張志新(化名)和其女友劉穎馋评,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體留特,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜕青,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年右核,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菱鸥。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氮采,死狀恐怖鹊漠,靈堂內(nèi)的尸體忽然破棺而出躯概,到底是詐尸還是另有隱情畔师,我是刑警寧澤茉唉,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布度陆,位于F島的核電站懂傀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏恃泪。R本人自食惡果不足惜犀斋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一叽粹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挽拔,春花似錦螃诅、人聲如沸状囱。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宣鄙。三九已至,卻和暖如春苇羡,著一層夾襖步出監(jiān)牢的瞬間鼻弧,已是汗流浹背攘轩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工度帮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笨篷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像冕臭,于是被迫代替她去往敵國(guó)和親浴韭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子念颈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 應(yīng)用啟動(dòng)時(shí)間榴芳,直接影響用戶對(duì)一款應(yīng)用的判斷和使用體驗(yàn)窟感。頭條主app本身就包含非常多并且復(fù)雜度高的業(yè)務(wù)模塊(如新聞、...
    hgl閱讀 438評(píng)論 0 0
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,072評(píng)論 25 707
  • 丁酉雞年的第一天躏嚎,2016年的第四周,大年初一重荠,電視里正在演著北京春晚吧虚茶,外面的鞭炮聲也不多了嘹叫,今年貌似鞭炮聲沒(méi)有...
    趙自律閱讀 239評(píng)論 0 0
  • 最近看書(shū)進(jìn)度很慢待笑,《出走》看完后,這周都是斷斷續(xù)續(xù)的看一些書(shū)寞缝,因?yàn)榧磳⒊蔀槟赣H的原因仰泻,所以開(kāi)始關(guān)注一些教育類書(shū)籍集侯,...
    Lylian_啦啦啦閱讀 297評(píng)論 0 0
  • 上周六加班了。這還是第一次周末上班泡挺,偌大的辦公區(qū)塞滿了格子間命浴,窗戶敞亮著生闲,沒(méi)有開(kāi)燈,這樣我正好喜歡悬蔽,因?yàn)殡娔X屏幕不...
    WaiWaii閱讀 209評(píng)論 0 0