自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

得物 iOS 啟動(dòng)優(yōu)化之 Building Closure

移動(dòng)開發(fā) iOS
我們探索了 BuildingClosure 的生成過(guò)程,發(fā)現(xiàn)在Building Closure階段中,可能存在字符串的 Hash 碰撞 引發(fā)循環(huán)次數(shù)大幅增加,進(jìn)而引發(fā)了啟動(dòng)耗時(shí)暴增,進(jìn)而導(dǎo)致啟動(dòng)耗時(shí)的大幅增加。

一、神秘的 BuildingClosure

    1. dyld && BuildingClosure

    2. BuildingClosure 非常耗時(shí)

    3. BuildingClosure 文件解析

二、離奇的啟動(dòng)耗時(shí)暴增事件 

    1. 啟動(dòng)耗時(shí)暴增 200ms

    2. BuildingClosure 耗時(shí)異常變化定位

三、啟動(dòng)優(yōu)化新秘境

    1. Perfect Hash

    2. 向前一步

四、總結(jié)

得物一直重視用戶體驗(yàn),尤其是啟動(dòng)時(shí)長(zhǎng)這一重要指標(biāo)。在近期的啟動(dòng)時(shí)長(zhǎng)跟進(jìn)中,我們發(fā)現(xiàn)了在BuildingClosure 階段的一個(gè)優(yōu)化方式,成功的幫助我們降低了 1/5 的 BuildingClosure 階段的啟動(dòng)耗時(shí)。Building Closure 并非工程的編譯階段(雖然它有一個(gè)building),Building Closure 是應(yīng)用初次啟動(dòng)時(shí)會(huì)經(jīng)歷的階段,因此它會(huì)影響應(yīng)用的啟動(dòng)時(shí)長(zhǎng)。

單就BuildingClosure階段而言,我們觀察到該階段其中一個(gè)函數(shù)從 480ms 暴增到 1200ms 左右(PC 電腦端運(yùn)行 dyld 調(diào)試統(tǒng)計(jì)耗時(shí)數(shù)據(jù)),我們通過(guò)優(yōu)化,將耗時(shí)從1200ms降低到110ms。即使相比最開始的情況,也相當(dāng)于從480ms降低到了110ms,由此可見Building Closure 優(yōu)化是應(yīng)用進(jìn)行啟動(dòng)優(yōu)化必不可少的一個(gè)重要手段。因此在這里我們也和各位讀者進(jìn)行分享,期望能夠?qū)Ω髯皂?xiàng)目有所幫助。

一、神秘的 BuildingClosure

啟動(dòng)優(yōu)化的技術(shù)、實(shí)現(xiàn)方案業(yè)界有不少的文章可以參考學(xué)習(xí),這里不再額外贅述。我們來(lái)探索下啟動(dòng)過(guò)程中非常神秘的 BuildingClosure。

BuildingClosure 是在 System Interface Initialization 階段 dyld 生成的,并且我們也無(wú)法做任何的干預(yù),另外相關(guān)的剖析文章相對(duì)較少,所以說(shuō) BuildingClosure 較為神秘,也是實(shí)至名歸。

BuildingClosure 是由 dyld 在應(yīng)用啟動(dòng)階段執(zhí)行的,所以想要了解 BuildingClosure 還是要從 dyld 開始了解。

dyld && BuildingClosure

Dyld 源碼可以在 Apple GitHub 上查閱 https://github.com/apple-oss-distributions/dyld

相信大家都應(yīng)該了解過(guò),BuildingClosure 是在 iOS 13 引入進(jìn)來(lái)的,對(duì)應(yīng)的 dyld 為 dyld3,目的是為了減少啟動(dòng)環(huán)節(jié)符號(hào)查找、Rebase、Bind 的耗時(shí)。

核心技術(shù)邏輯是將重復(fù)的啟動(dòng)工作只做一次,在 App 首次啟動(dòng)、版本更新、手機(jī)重啟之后的這次啟動(dòng)過(guò)程中,將相關(guān)信息緩存到 Library/Caches/com.app.dyld/xx.dyld 文件中,App 在下次啟動(dòng)時(shí)直接使用緩存好的信息,進(jìn)而優(yōu)化二次啟動(dòng)的速度。

在 iOS 15 Dyld4 中更是引入了 SwiftConformance,進(jìn)一步解決了運(yùn)行時(shí) Swift 中的類型、協(xié)議檢查的耗時(shí)。

圖片圖片

以上優(yōu)化,我們都無(wú)需做任何工作即可享受 dyld 帶來(lái)的啟動(dòng)速度的優(yōu)化,可以感受到 Apple 的開發(fā)人員也在關(guān)心啟動(dòng)速度并為之做了大量的工作。

BuildingClosure 非常耗時(shí)

我們通過(guò) instrument 觀測(cè)到 BuildingClosure 的耗時(shí)占據(jù)了啟動(dòng)耗時(shí)將近 1/3 的時(shí)間。

雖然說(shuō),BuildingClosure 只會(huì)在首次啟動(dòng)、版本更新、手機(jī)重啟的第一次啟動(dòng)生成和耗時(shí),但是對(duì)用戶的體驗(yàn)影響是非常之大的。

圖片圖片

BuildingClosure 文件解析

我們通過(guò)對(duì) dyld 的編譯和搭建模擬手機(jī)環(huán)境,成功模擬器了 dyld 加載可執(zhí)行文件的過(guò)程,也就成功解析了 BuildingClosure 文件。BuildingClosure 文件數(shù)據(jù)格式如下(數(shù)據(jù)格式、注釋僅供參考,并非全部的數(shù)據(jù)格式):

BuildingClosure 文件內(nèi)部結(jié)構(gòu)(數(shù)據(jù)格式、注釋僅供參考)BuildingClosure 文件內(nèi)部結(jié)構(gòu)(數(shù)據(jù)格式、注釋僅供參考)

其中占用比較大的部分主要為 Loader-selectorReferencesFixupsSize SwiftTypeConformance  objcSelector objcClass

二、離奇的啟動(dòng)耗時(shí)暴增事件

如上,我們已經(jīng)對(duì) BuildingClosure 有了基本的了解和對(duì) dyld 的執(zhí)行過(guò)程有了一定的了解。但是這份寧?kù)o在某一天突然被打破。

啟動(dòng)耗時(shí)暴增 200ms

在我們一個(gè)新版本開發(fā)過(guò)程中,例行對(duì)啟動(dòng)耗時(shí)進(jìn)行跟蹤測(cè)試,但是發(fā)現(xiàn)新版本啟動(dòng)耗時(shí)暴增 200ms,可以說(shuō)是災(zāi)難級(jí)別的事情。

我們開始對(duì)最近的出包做了基本的耗時(shí)統(tǒng)計(jì),方式為基于 instrument,統(tǒng)計(jì)出來(lái)啟動(dòng)各個(gè)階段的耗時(shí)數(shù)據(jù)。經(jīng)過(guò)對(duì)比,可以明顯觀測(cè)到,200ms 耗時(shí)的增加表現(xiàn)在 BuildingClosure 這個(gè)環(huán)節(jié)。

但是 BuildingClosure 耗時(shí)的增加既不是階梯式增加,也不是線性增加,并且只在新版本有增加。在排除相關(guān)因素(動(dòng)態(tài)庫(kù)、工程配置、打包腳本、編譯環(huán)境)之后,仍然沒(méi)有定位明確的原因。

在以上定位工作之后,最終確定耗時(shí)確實(shí)在 dyld 的 BuildingClosure 階段耗時(shí),并且懷疑可能是某些代碼觸發(fā)了 Dyld 的隱藏彩蛋。所以我們開始了對(duì) BuildingClosure 更深一步的研究。

BuildingClosure 耗時(shí)異常變化定位

通過(guò)使用 Instrument 對(duì) System Interface Initialization 階段進(jìn)行堆棧分析,最終發(fā)現(xiàn)了耗時(shí)最高的函數(shù):dyld4::PrebuiltObjC::generateHashTables(dyld4::RuntimeState&)

在對(duì)比了新老版本數(shù)據(jù),耗時(shí)變化差異的函數(shù)也是此函數(shù),我們簡(jiǎn)稱為 generateHashTables。這樣使得我們更加確定耗時(shí)為 dyld 過(guò)程中的 BuildingClosure 階段。

圖片

使用 Instrument 分析 BuildingClosure 階段耗時(shí)

三、啟動(dòng)優(yōu)化新秘境

在發(fā)現(xiàn) BuildingClosure 生成過(guò)程中耗時(shí)占比非常大,并且有異常時(shí),起初并沒(méi)有意識(shí)到有什么問(wèn)題,因?yàn)檫@是 dyld 內(nèi)的代碼,并未感覺(jué)會(huì)有什么問(wèn)題。但是一切都指向了該函數(shù),于是開始擼起袖子看代碼。

從代碼中可以看到,此處是為了生成 BuildingClosure 中 objcSelector objcClass objcProtocol 這三個(gè)部分的 HashTable(可以參考上面的 【BuildingClosure 文件解析】部分)。

拿起 dyld 開始對(duì)耗時(shí)異常版本的可執(zhí)行文件進(jìn)行調(diào)試,通過(guò)對(duì)該函數(shù)和內(nèi)部實(shí)現(xiàn)的代碼邏輯閱讀,以及增加耗時(shí)信息打印。最終確定,耗時(shí)的代碼在 make_perfect 這個(gè)函數(shù)中,這個(gè)函數(shù)是對(duì)【輸入的字符串列表】生成一個(gè)【完美 Hash 表】。

void PrebuiltObjC::generateHashTables(RuntimeState& state)
{
    // Write out the class table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::classes, objcImages, classesHashTable, duplicateSharedCacheClassMap, classMap);
    // Write out the protocol table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::protocols, objcImages, protocolsHashTable, duplicateSharedCacheClassMap, protocolMap);
    // If we have closure selectors, we need to make a hash table for them.
    if ( !closureSelectorStrings.empty() ) {
        objc::PerfectHash phash;
        objc::PerfectHash::make_perfect(closureSelectorStrings, phash);
        size_t size = ObjCStringTable::size(phash);
        selectorsHashTable.resize(size);
        //printf("Selector table size: %lld\n", size);
        selectorStringTable = (ObjCStringTable*)selectorsHashTable.begin();
        selectorStringTable->write(phash, closureSelectorMap.array());
    }
}

繼續(xù)深入了解 make_perfect 這個(gè)函數(shù)的實(shí)現(xiàn)。

Perfect Hash

通過(guò)對(duì)研讀代碼邏輯和耗時(shí)分析,最終定位到耗時(shí)代碼部分為PerfectHash.cpp 中 findhash 函數(shù),這個(gè)函數(shù)也是 完美散列函數(shù) 的核心邏輯。

  這里涉及到了一個(gè)概念PerfectHash,PerfectHash 的核心是完美散列函數(shù),我們看下維基百科的解釋:

https://zh.wikipedia.org/wiki/%E5%AE%8C%E7%BE%8E%E6%95%A3%E5%88%97

對(duì)集合S的完美散列函數(shù)是一個(gè)將S的每個(gè)元素映射到一系列無(wú)沖突的整數(shù)的哈希函數(shù)

簡(jiǎn)單來(lái)講 完美散列函數(shù) 是【對(duì)輸入的字符串列表】【為每個(gè)字符串生成一個(gè)唯一整數(shù)】。

for (si=1; ; ++si)
    {
        ub4 rslinit;
        /* Try to find distinct (A,B) for all keys */
        *salt = si * 0x9e3779b97f4a7c13LL; /* golden ratio (arbitrary value) */
        initnorm(keys, *alen, blen, smax, *salt);
        rslinit = inittab(tabb, keys, FALSE);
        if (rslinit == 0)
        {
            /* didn't find distinct (a,b) */
            if (++bad_initkey >= RETRY_INITKEY)
            {
                /* Try to put more bits in (A,B) to make distinct (A,B) more likely */
                if (*alen < maxalen)
                {
                    *alen *= 2;
                }
                else if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                }
                bad_initkey = 0;
                bad_perfect = 0;
            }
            continue;                             /* two keys have same (a,b) pair */
        }
        /* Given distinct (A,B) for all keys, build a perfect hash */
        if (!perfect(tabb, tabh, tabq, smax, scramble, (ub4)keys.count()))
        {
            if (++bad_perfect >= RETRY_PERFECT)
            {
                if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                    --si;               /* we know this salt got distinct (A,B) */
                }
                else
                {
                    return false;
                }
                bad_perfect = 0;
            }
            continue;
        }
        break;
    }

此時(shí)通過(guò)對(duì)比新老版本的數(shù)據(jù)(使用 dyld 分別運(yùn)行新老版本的可執(zhí)行文件對(duì)比打印的日志),發(fā)現(xiàn):

  • 老版本循環(huán)了 31 次成功生成 HashTable
  • 新版本循環(huán)了 92 次成功生成 HashTable

至此,我們距離成功已經(jīng)非常接近了,于是進(jìn)一步研讀 dyld 源碼和增加了更多打印信息代碼,最終找到了相互沖突的函數(shù)字符串名稱。

/*
 * put keys in tabb according to key->b_k
 * check if the initial hash might work
 */
static int inittab_ts(dyld3::OverflowSafeArray<bstuff>& tabb, dyld3::OverflowSafeArray<key>& keys, int complete, int si)
// bstuff   *tabb;                     /* output, list of keys with b for (a,b) */
// ub4       blen;                                            /* length of tabb */
// key      *keys;                               /* list of keys already hashed */
// int       complete;        /* TRUE means to complete init despite collisions */
{
  int  nocollision = TRUE;
  ub4 i;
  memset((void *)tabb.begin(), 0, (size_t)(sizeof(bstuff)*tabb.maxCount()));
  /* Two keys with the same (a,b) guarantees a collision */
  for (i = 0; i < keys.count(); i++) {
    key *mykey = &keys[i];
    key *otherkey;
    for (otherkey=tabb[mykey->b_k].list_b;
     otherkey;
     otherkey=otherkey->nextb_k)
    {
      if (mykey->a_k == otherkey->a_k)
      {
          // 打印沖突的字符串
        std::cout << mykey->name_k << " and " << otherkey->name_k << " has the same ak " << otherkey->a_k << " si is " << si << std::endl;
        nocollision = FALSE;
          /* 屏蔽此處代碼,有沖突的情況下,繼續(xù)執(zhí)行,便于打印所有的沖突
    if (!complete)
      return FALSE;
           */
      }
    }
    ++tabb[mykey->b_k].listlen_b;
    mykey->nextb_k = tabb[mykey->b_k].list_b;
    tabb[mykey->b_k].list_b = mykey;
  }
  /* no two keys have the same (a,b) pair */
  return nocollision;
}

根據(jù)以上信息,我們已經(jīng)了解到在Building Closure階段中,可能存在字符串的 Hash 碰撞 引發(fā)循環(huán)次數(shù)大幅增加,進(jìn)而引發(fā)了啟動(dòng)耗時(shí)暴增。

在經(jīng)過(guò) dyld 調(diào)試的耗時(shí)數(shù)據(jù)、構(gòu)建出包后驗(yàn)證的數(shù)據(jù)驗(yàn)證后,通過(guò)避免 Hash 碰撞,我們完成了啟動(dòng)時(shí)長(zhǎng)的優(yōu)化。

向前一步

其實(shí)從打印的沖突函數(shù)名稱來(lái)看,歷史代碼中已經(jīng)存在了 Hash 碰撞 的現(xiàn)象。

猜想,如果我們解決了所有的字符串的 Hash 碰撞,豈不是不僅可以修復(fù)啟動(dòng)耗時(shí)異常上升的問(wèn)題,還可以進(jìn)一步降低啟動(dòng)耗時(shí),提高啟動(dòng)速度?

于是我們對(duì)每個(gè)有碰撞的函數(shù)名稱進(jìn)行修改,經(jīng)過(guò)出包驗(yàn)證,結(jié)果與我們猜測(cè)的一致,啟動(dòng)耗時(shí)有明顯的下降。

數(shù)據(jù)為 PC 電腦端運(yùn)行 dyld 生成 BuildingClosure 的耗時(shí)數(shù)據(jù),非手機(jī)端數(shù)據(jù)數(shù)據(jù)為 PC 電腦端運(yùn)行 dyld 生成 BuildingClosure 的耗時(shí)數(shù)據(jù),非手機(jī)端數(shù)據(jù)


四、總結(jié)

我們探索了 BuildingClosure 的生成過(guò)程,發(fā)現(xiàn)在Building Closure階段中,可能存在字符串的 Hash 碰撞 引發(fā)循環(huán)次數(shù)大幅增加,進(jìn)而引發(fā)了啟動(dòng)耗時(shí)暴增,進(jìn)而導(dǎo)致啟動(dòng)耗時(shí)的大幅增加。

我們也發(fā)現(xiàn),Building Closure Hash碰撞相關(guān)的啟動(dòng)耗時(shí),其實(shí)與項(xiàng)目配置、編譯環(huán)境、打包腳本等均無(wú)任何關(guān)系,就只是存在了字符串的Hash 碰撞 ,才引發(fā)循環(huán)次數(shù)大幅增加,進(jìn)而導(dǎo)致啟動(dòng)時(shí)長(zhǎng)增加。

責(zé)任編輯:武曉燕 來(lái)源: 得物技術(shù)
相關(guān)推薦

2023-08-30 18:49:05

2024-09-03 16:09:59

2022-11-14 14:53:14

架構(gòu)技術(shù)編輯工具

2023-07-19 22:17:21

Android資源優(yōu)化

2023-05-10 18:34:49

推薦價(jià)格體驗(yàn)優(yōu)化UV

2021-11-23 10:25:35

性能優(yōu)化iOS App 啟動(dòng)優(yōu)化

2024-08-13 15:26:44

2019-12-13 10:25:08

Android性能優(yōu)化啟動(dòng)優(yōu)化

2023-03-30 18:39:36

2022-12-12 18:56:04

2017-01-19 19:07:28

iOS進(jìn)階性能優(yōu)化

2023-04-28 18:37:38

直播低延遲探索

2023-08-21 19:37:21

得物DGraph引擎

2023-10-09 18:35:37

得物Redis架構(gòu)

2025-03-13 06:48:22

2019-09-25 08:03:21

Android加速Google

2024-12-03 11:12:47

2022-12-14 18:40:04

得物染色環(huán)境

2023-11-27 18:38:57

得物商家測(cè)試

2023-02-08 18:33:49

SRE探索業(yè)務(wù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)