起底 iOS 應(yīng)用啟動(dòng)-Dyld篇

總覽

  1. 利用已經(jīng)被內(nèi)核映射到內(nèi)存中的可執(zhí)行文件,instantiateFromLoadedImage生成 ImageLoader
  2. 將依賴庫(kù)加載進(jìn)內(nèi)存,生成對(duì)應(yīng)的 ImageLoader(loadInsertedDylib
  3. 鏈接可執(zhí)行文件(link
  4. 鏈接依賴庫(kù)(link
  5. 調(diào)用所有 Image 的初始化方法 Initializers,包括動(dòng)態(tài)庫(kù)和可執(zhí)行文件,核心系統(tǒng)庫(kù)、objc自舉(initializeMainExecutable
  6. 返回程序入口函數(shù) main 的地址(sMainExecutable->getMain()

dyld 源碼地址

簡(jiǎn)化過的代碼如下:

//
// 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)
{
    ......

    // 1. instantiate ImageLoader for main executable
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

    ......

    // 2. load any inserted libraries
    if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
        for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) {
            loadInsertedDylib(*lib);
        }
    }

    ......

    // 3. link main executable
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));

    ......

    // 4. link any inserted libraries
    // do this after linking main executable so that any dylibs pulled in by inserted
    // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
    if ( sInsertedDylibCount > 0 ) {
        for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
            ImageLoader* image = sAllImages[i+1];
            link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
            image->setNeverUnloadRecursive();
        }
    }


    // 5. run all initializers
    initializeMainExecutable();

    // 6. main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
    result = (uintptr_t)sMainExecutable->getMain();

    return result;
}

可執(zhí)行文件和依賴庫(kù)的鏈接

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
    // 遞歸加載
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
    // 遞歸Rebase 修復(fù) ASLR 造成的地址錯(cuò)位的問題,增加一個(gè)偏移量
    // 主要是 IO 操作
    this->recursiveRebase(context);
    // 遞歸符號(hào)綁定,將指針指向 image 外部的內(nèi)容。需要查詢符號(hào)表,性能消耗主要是 CPU 計(jì)算
    this->recursiveBind(context, forceLazysBound, neverUnload);
}

Initializers

void initializeMainExecutable()
{
        ......
        
        // 首先執(zhí)行動(dòng)態(tài)庫(kù)的 initialzers
        // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[sAllImages.size()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }

        // 隨后執(zhí)行可執(zhí)行文件的 initialzers
    // run initializers for main executable and everything it brings up 
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

        // 進(jìn)行終止化
        (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
        
        // 如果設(shè)置了環(huán)境變量 DYLD_PRINT_STATISTICS,可以在 Xcode debug 的時(shí)候在控制臺(tái)打印
    // dump info if requested
    if ( sEnv.DYLD_PRINT_STATISTICS )
        ImageLoaderMachO::printStatistics((unsigned int)sAllImages.size(), initializerTimes[0]);
}

這里的 initialzers 注意不是 Objective-C 中的 initialzers 方法,而是 C++靜態(tài)對(duì)象初始化構(gòu)造器。

在 Xcode 中,可以通過設(shè)置環(huán)境變量 DYLD_PRINT_STATISTICS 打印所有 initialzers 方法。

設(shè)置打印

運(yùn)行 App 后,可以看到控制臺(tái)打印

控制臺(tái)打印

通過打印可以發(fā)現(xiàn),其中排第一個(gè)的是 libSystem.B.dylib,排在最后的是可執(zhí)行文件的方法。

在 libSystem.B.dylib 的 initialzers 函數(shù)里的 libdispatch_init 調(diào)用到了 Runtime 初始化方法 _objc_init。

通過在 Xcode 中設(shè)置 _objc_init 符號(hào)斷點(diǎn),可以看到在控制臺(tái)打印 dyld: calling initializer function 0x1ba93e7c0 in /usr/lib/libSystem.B.dylib 后, _objc_init 被調(diào)用了,不過調(diào)用棧將中間的調(diào)用過程全部隱去了。

objc 源碼部分

查看 libdispatch源碼,可以發(fā)現(xiàn) objc 的蹤跡。

libdispatch_init -> _os_object_init -> _objc_init

void
libdispatch_init(void)
{
    ......
    _os_object_init();
    .....
}

void
_os_object_init(void)
{
    _objc_init();
        ......
}

這下終于來到了熟悉的 objc4 的源碼,這里我們重點(diǎn)關(guān)注在 dyld 中注冊(cè)了的三個(gè)回調(diào)。
三個(gè)回調(diào)時(shí)機(jī)依次是 objc image 的 mapped、initialized、unmapped 三個(gè)階段

void _objc_init(void)
{
    // so many init
    ......
    // 在 dyld 中注冊(cè)了三個(gè)回調(diào)
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

map_images

當(dāng) dyld 完成映射(mapped)后,開始執(zhí)行。其中核心函數(shù)是 _read_images
完成的工作主要是:

  1. 從庫(kù)中對(duì)應(yīng)的 segment 讀取 class、protocol、category 信息,載入到 Runtime。
  2. 將 class、protocol、category 信息向 Runtime 注冊(cè)結(jié)構(gòu)
  3. 將 category 需要加到對(duì)應(yīng)的 class 上。
  4. realized 類(重新確定布局,在 class_ro_t 基礎(chǔ)上創(chuàng)建 class_rw_t)

其中需要說明的是 class 中 class_ro_t 為 unrealized class,class_rw_t 為 realized class。
如果想要使用 class,必須是 class_rw_t。class_ro_t 經(jīng)過 resolve 后,會(huì)轉(zhuǎn)化為 class_rw_t。non-lazy class 要求在初始化進(jìn)行 realize。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    // 注冊(cè) selector 到一個(gè)全局表中
    for(......) {
      SEL sel = sel_registerNameNoLock(name, isBundle);
    }
    
    // 獲取 classes,并 realize
    classref_t const *classlist = _getObjc2ClassList(hi, &count);
    ......
    Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
  
    // protocol 
    protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
    for (i = 0; i < count; i++) {
            // 注冊(cè)結(jié)構(gòu)
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
     }

    // categories
    for() {
        // 在 class 中注冊(cè) category,
        // 如果 class 已經(jīng) realize,重建 class
        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
    }

    // Realize non-lazy classes (for +load methods and static instances)
    classref_t const *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
    for(......) {
        // 將類添加到 table 中
        addClassTableEntry(cls);
        realizeClassWithoutSwift(cls, nil);
    }

    // Realize newly-resolved future classes
    for(......) {
        realizeClassWithoutSwift(cls, nil);
    }
}

realizeClass 干的事情包括:

  1. 遞歸 Realize 父類和元類
  2. 重新設(shè)置 class、superclass、metaclass 之間的關(guān)系
  3. 重新計(jì)算 instance variable layout
  4. 將 class 中的編譯時(shí)已經(jīng)確定的 class_ro_t 的內(nèi)容(method、property、protocol)放到 class_rw_t 結(jié)構(gòu)體上,并增加 category 的部分
static Class realizeClass(Class cls)
{    
    // realize 父類和元類
    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));

     // 重新設(shè)置關(guān)系
    cls->superclass = supercls;
    cls->initClassIsa(metacls);

    // 重新計(jì)算 layout
    // 如果 superclass 和原 class 空間重疊,需要對(duì)原 class 的實(shí)例重新計(jì)算位置并調(diào)整
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Connect this class to its superclass's subclass lists
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
    methodizeClass(cls);
}

methodizeClass 函數(shù)干的事包括:

  1. 將 class 中的 class_ro_t 的內(nèi)容(method、property、protocol)放到 class_rw_t 結(jié)構(gòu)體上,
  2. 增加 category 的部分
static void methodizeClass(Class cls)
{
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;
    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    // 方法增加
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    property_list_t *proplist = ro->baseProperties;
    // 屬性增加
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        // 協(xié)議增加
        rw->protocols.attachLists(&protolist, 1);
    }
    // Attach categories,將 method、property、protocol 插入
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}

load_images

是 dyld 的第二個(gè)注冊(cè)回調(diào),其核心是調(diào)用 load 方法。

  1. 查找并保存所有類,主類存在 loadable_classes 中,其排列順序是父類在前、子類在后,分類存在 loadable_categories 中。
  2. 先調(diào)用 loadable_classes 中類的 load 方法,然后調(diào)用 loadable_categories 中分類的 load 方法
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    
    prepare_load_methods((const headerType *)mh);

    call_load_methods();
}

void call_load_methods(void)
{
    ......

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. 首先調(diào)用類的 load
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 調(diào)用分類的 load
        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);

   ......
}

// 調(diào)用 load 方法,并沒有走消息發(fā)送機(jī)制,而是直接進(jìn)行調(diào)用
(*load_method)(cls, @selector(load));

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

目前為止是 App 啟動(dòng)時(shí),pre-main 部分,也就是還沒有走到 App 入口的 main 函數(shù)。

pre-main

load dylibs

iOS App 一般需要加載 100~400 個(gè) dylibs。其中包括了系統(tǒng)和開發(fā)者引入的。加載 dylibs 會(huì)消耗 App 的啟動(dòng)時(shí)間。可以做優(yōu)化的部分是自己引入的 dylib。Apple 在 WWDC 上建議,建議盡量將第三方 dylibs 個(gè)數(shù)控制在 6 個(gè)以內(nèi)。

優(yōu)化方案:

  1. 使用靜態(tài)庫(kù)替代動(dòng)態(tài)庫(kù),如果使用 Cocoapods 管理第三方庫(kù)的話,可以將 podfile 中的 use_frameworks! 注釋掉,然后 pod install 來將動(dòng)態(tài)庫(kù)變更為靜態(tài)庫(kù)。
  2. 合并動(dòng)態(tài)庫(kù),減少動(dòng)態(tài)庫(kù)的數(shù)量,這一塊筆者沒有進(jìn)行嘗試。

Rebase/Binding

這兩步主要是對(duì) image 內(nèi)部指針的修復(fù)。因此只要指針數(shù)量越少,修復(fù)的耗時(shí)就會(huì)變少,其中的關(guān)鍵是減少 _DATA 段中指針的數(shù)量。

優(yōu)化方案:

  1. 減少類、方法的數(shù)量,比如刪除廢棄的類、方法
  2. 使用 Swift 的 struct(WWDC 介紹結(jié)構(gòu)內(nèi)部有做優(yōu)化,指針數(shù)量少)
  3. 減少 C++ 虛函數(shù)的數(shù)量(這塊可優(yōu)化的空間較小)

參考

libSystem

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373