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代理的類(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ì)信息:

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è)專(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被初始化。

obj_init.png

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)文件的加載速度。

refrence

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容