聊一聊啟動優(yōu)化吧

啟動優(yōu)化乾巧,在不影響業(yè)務(wù)的前提條件下句喜,怎么提高啟動的速度预愤,這是我們要考慮的事情。

在這咳胃,根據(jù)系統(tǒng)打印提示信息這條主線植康,看下啟動過程中,每個階段都做的什么展懈,在這些階段我們能做哪些優(yōu)化的事情销睁。

添加Xcode的打印環(huán)境DYLD_PRINT_STATISTICS

Total pre-main time: 390.79 milliseconds (100.0%)
         dylib loading time: 228.22 milliseconds (58.4%)
        rebase/binding time:   5.04 milliseconds (1.2%)
            ObjC setup time:  21.47 milliseconds (5.4%)
           initializer time: 135.91 milliseconds (34.7%)
           slowest intializers :
             libSystem.B.dylib :   3.75 milliseconds (0.9%)
    libMainThreadChecker.dylib :  41.48 milliseconds (10.6%)
                  xxxx : 155.96 milliseconds (39.9%)

這是main函數(shù)之前系統(tǒng)啟動過程中,也就是pre-main存崖,經(jīng)過的一些階段冻记,咱們針對每個階段來詳細說。

dylib loading

這是庫加載的時間来惧,包括共享緩存庫冗栗、我們自己的動態(tài)庫,如果進行了越獄開發(fā)供搀,還包含了我們插入的動態(tài)庫贞瞒。
系統(tǒng)的共享緩存庫(UIKit、Foundation)這些趁曼,系統(tǒng)是做了優(yōu)化的,加載時間一般都是固定的棕洋,浪費時間的點一般在我們自己的動態(tài)庫挡闰,我們自己的動態(tài)庫怎么優(yōu)化呢,超過6個最好合并一下掰盘。使用靜態(tài)庫等摄悯。

rebase/binding

rebase :這個是因為我們可執(zhí)行文件加載到虛擬內(nèi)存的時候,都會使用ASLR的技術(shù)愧捕,也就是在內(nèi)存的最前邊加一個隨機的偏移值奢驯,來保證我們應(yīng)用的安全。所以次绘,我們使用的系統(tǒng)的函數(shù)瘪阁、我們自定義的類、變量這些指針邮偎,都需要加上這個偏移值管跺,才是真正在內(nèi)存中的位置。
換句話說禾进,類越少豁跑、方法越少,需要修正的就越少泻云。
那么就可以使用一些工具檢測你APP中的無用類艇拍,無用的方法等狐蜕。

binding:這個是指針對外綁定,上邊提到了共享緩存卸夕,也就是系統(tǒng)的庫层释,是不加載到我們虛擬內(nèi)存中的,他是在公共內(nèi)存中娇哆,供大家都使用的湃累,那么我們寫了系統(tǒng)的函數(shù),怎么最終找到這個真是的函數(shù)地址碍讨,就需要binding這個操作治力,當(dāng)然,這個分為普通綁定和lazy綁定勃黍,這一部分其實沒什么需要優(yōu)化的空間宵统。

ObjC setup

這一部分是加載類信心,初始化類覆获,加載類信息马澈,是從Mach-O的_DATA段中加載一系列類相關(guān)的信息,然后初始化弄息,這部分先加載所有的類痊班,插入到一個大表中,加載方法摹量、protocol涤伐、property等,然后就是初始化類缨称,設(shè)置類的superClass指針(這一步是遞歸創(chuàng)建的)凝果,設(shè)置類的ro/ rw相關(guān)的字段,然后合并Category相關(guān)的信息睦尽,將Category的方法器净、協(xié)議、property按照規(guī)則当凡,插入到生成的類中山害,方法協(xié)議這些都是二維數(shù)組的形式存放,后編譯的Category沿量,插入的位置越靠前粗恢。這個想了解細節(jié)的可以看下源碼。
那這部分的優(yōu)化欧瘪,還是在類的數(shù)量眷射,方法的數(shù)量,協(xié)議的數(shù)量這些。

initializer time

這部分主要做的事情妖碉,加載load方法涌庭,C++靜態(tài)初始化函數(shù)(attribute((constructor))),attribute((constructor)) 的調(diào)用是在load的后邊欧宜,這個可以了解一下坐榆。
這部分的優(yōu)化:減少load方法,盡量將一些操作進行懶加載冗茸,也就是放到+(void)initialize 函數(shù)中荷鼠,這個函數(shù)是在類在第一次被調(diào)用的時候進行調(diào)用的祭犯,注意的是洋丐,這個方法可能會被多次調(diào)用吊圾,最好加dispatch_once做一下防護。
attribute((constructor))這種方法最好就是不用挂绰。

這是系統(tǒng)打印的階段流程屎篱,pre-main我們能做的事情和系統(tǒng)能做的事情就是這些。
在這里拓展一下系統(tǒng)詳細的啟動流程葵蒂,可以比對上述階段做下了解交播。

應(yīng)用的啟動流程

  • 內(nèi)核加載我們的Mach-O的可執(zhí)行文件
  • 從Mach-O文件中,找到dyld的路徑并加載
    從最開始的dyld start開始uintptr_t start()
  • slide 首先生成一個ASLR的隨機值 (這就是那個偏移值)
  • rebaseDyld 開始符號的綁定過程 (將我們的指針開始rebase操作)
  • // allow dyld to use mach messaging
    mach_init();
  • dyld main (這是dyld的main函數(shù))
    • 配置環(huán)境變量 (Xcode相關(guān)的一些環(huán)境變量的讀燃丁)
    • load shared cache(加載共享緩存秦士,系統(tǒng)動態(tài)庫UIKit等)
      • 這里邊有幾個判斷
        • 緩存庫是不是只用在這個程序
        • 是否存在reuseExistingCache,加載過就什么都不做
        • 不存在緩存中永高,加載mapCacheSystemWide
    • 加載主程序Mach-O(實例化主程序)
      • 這是第一個被加載的image
    • load inserted libraries 插入動態(tài)庫(越獄開發(fā)修改這個字段伍宦,插入自己的動態(tài)庫),等于問你插入哪些動態(tài)庫,并且加載
    • link 鏈接程序乏梁,這是一個遞歸的過程,鏈接我們依賴系統(tǒng)的動態(tài)庫关贵,這里邊也有rebase操作(指針修復(fù)遇骑,前邊加一個偏移)
      • binding 符號的綁定
    • link 我們inset的dylib
    • binding 過程,weak binding等
  • initializeMainExecutable()初始化主程序開始了
    • 最終dyld調(diào)用到了notifySingle()//后續(xù)就到了runtime去初始化了
  • 初始化以后揖曾,會將一個函數(shù)指針load_images落萎,傳給dyld,讓dyld調(diào)用炭剪,load_images這個函數(shù)里邊調(diào)用了load方法练链。do while循環(huán)調(diào)用所有l(wèi)oad
  • attribute((constructor)) void func(){
    }
    也就是執(zhí)行C++的構(gòu)造函數(shù)
  • 調(diào)用真正的main入口

runtime中objc所作的預(yù)處理,因為在runtime中放了兩個回調(diào)函數(shù)供dyld去調(diào)用

  • objc - setup
    • 從Mach-O中讀取類信息奴拦,注冊到一個全局的表中
    • 讀取Mach-O方法信息(sel)媒鼓,注冊到全局的一個表中。
    • 創(chuàng)建類,信息的組裝realizeClassWithoutSwift
      • ISA指針
      • superclass指針
      • cache
      • bits相關(guān)
        • 生成rw
          • 添加方法绿鸣、協(xié)議疚沐、property等
          • 處理category,方法潮模、協(xié)議拷貝進bits中亮蛔。(注意這個二維數(shù)組的形式)

以上過程包含兩部分,dyld的程序初始化過程擎厢,有一部分需要objc runtime的支持究流。

整個pre-main就這些,也可以自己進行了解后动遭,看些是否還有更多需要注意的地方芬探。

有了pre-mian,就有 main之后相關(guān)沽损。

我這里認定灯节,從main到第一個頁面的viewDidAppear是main之后。
這里邊都是具體業(yè)務(wù)相關(guān)绵估,真正可以節(jié)省時間的點炎疆,都在這里邊;那么啟動前的代碼應(yīng)該怎么寫呢国裳?
個人理解形入,原則上到viewDidAppear所有的函數(shù),都應(yīng)該只是為了服務(wù)第一個頁面的創(chuàng)建及渲染缝左,等第一個頁面出來后亿遂,在初始化其他相關(guān)的內(nèi)容;當(dāng)然這也只是美好的愿望渺杉,有些庫的初始化和一些其他操作蛇数,可能不僅是為了第一個頁面服務(wù),并且還必須要放到這里去做是越。
這其實可以叫做分階段啟動耳舅。
必須要做的:

  • 埋點功能、
  • Crash 采集
  • 網(wǎng)絡(luò)配置

可以延后的:各種SDK的初始化倚评、配置文件的請求等浦徊。其實地理位置的請求也可以延后,之前看過美團還是哪個公司天梧,他們會先用上次緩存的地理位置請求盔性,等到首頁加載完以后,如果地理位置不一樣呢岗,再重新刷新冕香。

第二種思路
可以考慮多線程啟動蛹尝,目前手機都是多核,可以開2-3個線程暂筝,是可以加快初始化速度的箩言。
使用這個方案有幾個注意點:

  1. 如果主線程忙完,子線程還沒忙完焕襟,這時候會黑屏陨收,所以啟動的時候,就要加一個假的圖片鸵赖,等每個線程結(jié)束务漩,都去問一下其他線程狀態(tài)怎么樣了,都結(jié)束了它褪,假的圖片消失饵骨,顯示首頁。
  2. 不是所有的SDK初始化都適合放到子線程茫打,需要測試居触,有些依賴主線程的執(zhí)行,有些內(nèi)部使用了UIKit相關(guān)的老赤。

其實目前大部分APP都有廣告圖轮洋,我們可以在顯示廣告的時候,去初始化其他的東西抬旺。

main之后也就這些弊予。

去年的時候,抖音發(fā)布了一個二進制重排的技術(shù)進行優(yōu)化开财,這里也研究了一下汉柒。
我們APP加載進內(nèi)存,其實是一個虛擬內(nèi)存责鳍,不是真正的物理內(nèi)存碾褂,代碼的執(zhí)行是要在真正的物理內(nèi)存進行的,這時候历葛,又有一個中間的映射表出現(xiàn)正塌,我們虛擬內(nèi)存的數(shù)據(jù),通過映射表啃洋,映射到真正的物理內(nèi)存,然后進行代碼的執(zhí)行屎鳍,為什么要有中間的映射表呢宏娄?因為他可以使用頁的形式分塊加載進物理內(nèi)存,不必將整個APP全部加載進內(nèi)存逮壁,每一頁的大小是16KB孵坚,也就是代碼執(zhí)行到哪,就通過映射表將這16KB的數(shù)據(jù)映射到物理內(nèi)存去執(zhí)行,這樣可以充分利用物理內(nèi)存卖宠,保證整個物理內(nèi)存基本都是高效率運轉(zhuǎn)巍杈,并且安全性也有保證。
原理就是這樣扛伍,如果每次啟動時候的代碼筷畦,分散在虛擬內(nèi)存中,可能會引發(fā)多次的映射過程(缺頁異常)刺洒,如果能把啟動是的代碼鳖宾,放到一個16KB頁中,是不是映射一次就可以了逆航,當(dāng)然這是理想的情況鼎文,整個啟動的過程大約4000次左右的映射過程大約0.2ms左右,如果進行優(yōu)化因俐,能減少百分之10左右拇惋。
我們可以使用Instruments 中的system trace 進行查看page fault的次數(shù)。

具體怎么將啟動的函數(shù)聚合到一起抹剩,兩種方案:

  • 抖音使用的是HOOK objc_msgSend撑帖,找到調(diào)用的所有函數(shù),有幾個缺點吧兔,靜態(tài)方法hook不到磷仰,swift的結(jié)構(gòu)體和枚舉都是值類型、我們自己寫的C函數(shù)境蔼、block這些都hook不到灶平。(具體hook方法可以使用fishkook,一個fb開源的工具箍土,但是還要用到匯編相關(guān)逢享,不過肯定很多開源的)
  • 可以考慮使用Clang編譯器帶的函數(shù),一般叫做Clang插樁吴藻,Clang編譯器會在每個函數(shù)執(zhí)行過程中插入代碼瞒爬,打印當(dāng)前函數(shù),這樣找到的方法比較全面沟堡。
    __sanitizer_cov_trace_pc_guard
    __sanitizer_cov_trace_pc_guard_init配置這兩個函數(shù)侧但,然后將啟動方法輸出到一個文件中,配置到在Xcode中order-file航罗。這樣禀横,系統(tǒng)在編譯的時候,會按照我們制定的順序加載粥血。默認的編譯順序是按照Build Phases中的Compile source里邊加載的柏锄。

搞完酿箭,我們可以通過輸出link map查看symbols的編譯順序。

這里涉及到一點趾娃,冷啟動和熱啟動缭嫡;我們通過page fault的次數(shù)可以看出,如果啟動過的APP抬闷,被殺掉以后妇蛀,下一次啟動page fault的次數(shù)減少了很多,說明啟動過饶氏,內(nèi)存中這些數(shù)據(jù)就已經(jīng)存在了(內(nèi)存中的數(shù)據(jù)銷毀讥耗,其實就是被覆蓋,如果這個時候疹启,我們啟動很多其他的應(yīng)用古程,再次啟動這個應(yīng)用,還是會減緩啟動時間)喊崖。
冷啟動就是內(nèi)存中挣磨,完全不存在這個APP,熱啟動就是被殺掉荤懂,然后接著啟動茁裙,這時候,內(nèi)存中很多數(shù)據(jù)已經(jīng)有了节仿,不需要操作了晤锥。

工具拓展:
怎么監(jiān)控啟動時間呢?
前滴滴出行技術(shù)專家戴銘廊宪,他實現(xiàn)了hook objc_msgSend矾瘾,然后在這個里邊添加開始調(diào)用函數(shù)和函數(shù)調(diào)用結(jié)束的標(biāo)識,從而了解每一個函數(shù)的執(zhí)行時間箭启。
粗顆粒壕翩,可以使用一些定時工具,計算從啟動傅寡,到啟動完放妈,或者某個階段的時間(BLStopwatch可以看下)
Instruments 也是很好的工具 Timer 、system trace都可以顯示每個函數(shù)的執(zhí)行時間荐操,他的實現(xiàn)原理好像是固定時間抓取函數(shù)的調(diào)用棧芜抒,粗略的算下執(zhí)行時間,就和微信開源的性能檢測工具Matrix托启,定時抓取調(diào)用棧信息宅倒,比對棧的頭部,看這個調(diào)用棧出現(xiàn)了幾次驾中,次數(shù)多了唉堪,就認為是卡頓,優(yōu)點類似肩民。

當(dāng)然唠亚,整個流程還有一些細節(jié)點,如果要考慮特別完善持痰,要考慮網(wǎng)絡(luò)灶搜、首頁渲染、數(shù)據(jù)讀取等一些細節(jié)工窍,看有沒有優(yōu)化的空間割卖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市患雏,隨后出現(xiàn)的幾起案子鹏溯,更是在濱河造成了極大的恐慌,老刑警劉巖淹仑,帶你破解...
    沈念sama閱讀 212,949評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丙挽,死亡現(xiàn)場離奇詭異,居然都是意外死亡匀借,警方通過查閱死者的電腦和手機颜阐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,772評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吓肋,“玉大人凳怨,你說我怎么就攤上這事∈枪恚” “怎么了肤舞?”我有些...
    開封第一講書人閱讀 158,419評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屑咳。 經(jīng)常有香客問我萨赁,道長,這世上最難降的妖魔是什么兆龙? 我笑而不...
    開封第一講書人閱讀 56,812評論 1 285
  • 正文 為了忘掉前任杖爽,我火速辦了婚禮,結(jié)果婚禮上紫皇,老公的妹妹穿的比我還像新娘慰安。我一直安慰自己,他們只是感情好聪铺,可當(dāng)我...
    茶點故事閱讀 65,927評論 6 386
  • 文/花漫 我一把揭開白布化焕。 她就那樣靜靜地躺著,像睡著了一般铃剔。 火紅的嫁衣襯著肌膚如雪撒桨。 梳的紋絲不亂的頭發(fā)上查刻,一...
    開封第一講書人閱讀 50,102評論 1 291
  • 那天,我揣著相機與錄音凤类,去河邊找鬼穗泵。 笑死,一個胖子當(dāng)著我的面吹牛谜疤,可吹牛的內(nèi)容都是我干的佃延。 我是一名探鬼主播,決...
    沈念sama閱讀 39,171評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼夷磕,長吁一口氣:“原來是場噩夢啊……” “哼履肃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坐桩,我...
    開封第一講書人閱讀 37,921評論 0 268
  • 序言:老撾萬榮一對情侶失蹤尺棋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绵跷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陡鹃,經(jīng)...
    沈念sama閱讀 44,366評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,675評論 2 327
  • 正文 我和宋清朗相戀三年抖坪,在試婚紗的時候發(fā)現(xiàn)自己被綠了萍鲸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,820評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡擦俐,死狀恐怖脊阴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚯瞧,我是刑警寧澤嘿期,帶...
    沈念sama閱讀 34,523評論 4 335
  • 正文 年R本政府宣布,位于F島的核電站埋合,受9級特大地震影響备徐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜甚颂,卻給世界環(huán)境...
    茶點故事閱讀 40,162評論 3 317
  • 文/蒙蒙 一蜜猾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧振诬,春花似錦蹭睡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,885評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春清钥,著一層夾襖步出監(jiān)牢的瞬間琼锋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,126評論 1 267
  • 我被黑心中介騙來泰國打工祟昭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留斩例,地道東北人。 一個月前我還...
    沈念sama閱讀 46,647評論 2 362
  • 正文 我出身青樓从橘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親础钠。 傳聞我的和親對象是個殘疾皇子恰力,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,732評論 2 351