抖音品質(zhì)建設(shè) - iOS啟動(dòng)優(yōu)化《原理篇》

來源作者:字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)

前言
啟動(dòng)是 App 給用戶的第一印象,啟動(dòng)越慢用戶流失的概率就越高风宁,良好的啟動(dòng)速度是用戶體驗(yàn)不可缺少的一環(huán)洁墙。啟動(dòng)優(yōu)化涉及到的知識(shí)點(diǎn)非常多面也很廣,一篇文章難以包含全部杀糯,所以拆分成兩部分:原理和實(shí)踐扫俺。
本文從基礎(chǔ)知識(shí)出發(fā),先回顧一些核心概念固翰,為后續(xù)章節(jié)做鋪墊狼纬;接下來介紹 IPA 構(gòu)建的基本流程,以及這個(gè)流程里可用于啟動(dòng)優(yōu)化的點(diǎn)骂际;最后大篇幅講解 dyld3 的啟動(dòng) pipeline疗琉,因?yàn)閱?dòng)優(yōu)化的重點(diǎn)還在運(yùn)行時(shí)。
基本概念
啟動(dòng)的定義
啟動(dòng)有兩種定義:

  • 廣義:點(diǎn)擊圖標(biāo)到首頁數(shù)據(jù)加載完畢
  • 狹義:點(diǎn)擊圖標(biāo)到 Launch Image 完全消失第一幀

本人自己整理的一些面試進(jìn)階資料歉铝,iOS底層視頻+BAT面試專題PDF+核心筆記等資料+2020年《大廠最新常問iOS面試題+答案》盈简。希望能夠幫助到大家提升技術(shù)。

面試資料分享:資料內(nèi)容包括逆向安防太示、算法柠贤、架構(gòu)設(shè)計(jì)、Swift类缤、多線程臼勉,網(wǎng)絡(luò)進(jìn)階,還有底層餐弱、音視頻宴霸、Flutter等等
不同產(chǎn)品的業(yè)務(wù)形態(tài)不一樣囱晴,對(duì)于抖音來說,首頁的數(shù)據(jù)加載完成就是視頻的第一幀播放瓢谢;對(duì)其他首頁是靜態(tài)的 App 來說畸写,Launch Image 消失就是首頁數(shù)據(jù)加載完成。由于標(biāo)準(zhǔn)很難對(duì)齊氓扛,所以我們一般使用狹義的啟動(dòng)定義:即啟動(dòng)終點(diǎn)為啟動(dòng)圖完全消失的第一幀枯芬。
以抖音為例,用戶感受到的啟動(dòng)時(shí)間:

image

Tips:啟動(dòng)最佳時(shí)間是 400ms 以內(nèi)采郎,因?yàn)閱?dòng)動(dòng)畫時(shí)長是 400ms破停。

這是從用戶感知維度定義啟動(dòng),那么代碼上如何定義啟動(dòng)呢尉剩?Apple 在 MetricKit 中給出了官方計(jì)算方式:

  • 起點(diǎn):進(jìn)程創(chuàng)建的時(shí)間
  • 終點(diǎn):第一個(gè)CA::Transaction::commit()

Tips:CATransaction 是 Core Animation 提供的一種事務(wù)機(jī)制,把一組 UI 上的修改打包毅臊,一起發(fā)給 Render Server 渲染理茎。

啟動(dòng)的種類
根據(jù)場景的不同,啟動(dòng)可以分為三種:冷啟動(dòng)管嬉,熱啟動(dòng)和回前臺(tái)皂林。

  • 冷啟動(dòng):系統(tǒng)里沒有任何進(jìn)程的緩存信息,典型的是重啟手機(jī)后直接啟動(dòng) App
  • 熱啟動(dòng):如果把 App 進(jìn)程殺了蚯撩,然后立刻重新啟動(dòng)础倍,這次啟動(dòng)就是熱啟動(dòng),因?yàn)檫M(jìn)程緩存還在
  • 回前臺(tái):大多數(shù)時(shí)候不會(huì)被定義為啟動(dòng)胎挎,因?yàn)榇藭r(shí) App 仍然活著沟启,只不過處于 suspended 狀態(tài)

那么,線上用戶的冷啟動(dòng)多還是熱啟動(dòng)多呢犹菇?
答案是和產(chǎn)品形態(tài)有關(guān)系德迹,打開頻次越高,熱啟動(dòng)比例就越高揭芍。
Mach-O
Mach-O 是 iOS 可執(zhí)行文件的格式胳搞,典型的 Mach-O 是主二進(jìn)制和動(dòng)態(tài)庫。Mach-O 可以分為三部分:

  • Header
  • Load Commands
  • Data
image

Header 的最開始是 Magic Number称杨,表示這是一個(gè) Mach-O 文件肌毅,除此之外還包含一些 Flags,這些 flags 會(huì)影響 Mach-O 的解析姑原。
Load Commands 存儲(chǔ) Mach-O 的布局信息悬而,比如 Segment command 和 Data 中的 Segment/Section 是一一對(duì)應(yīng)的。除了布局信息之外页衙,還包含了依賴的動(dòng)態(tài)庫等啟動(dòng) App 需要的信息摊滔。
Data 部分包含了實(shí)際的代碼和數(shù)據(jù)阴绢,Data 被分割成很多個(gè) Segment,每個(gè) Segment 又被劃分成很多個(gè) Section艰躺,分別存放不同類型的數(shù)據(jù)呻袭。
標(biāo)準(zhǔn)的三個(gè) Segment 是 TEXT,DATA腺兴,LINKEDIT左电,也支持自定義:

  • TEXT,代碼段页响,只讀可執(zhí)行篓足,存儲(chǔ)函數(shù)的二進(jìn)制代碼(__text),常量字符串(__cstring)闰蚕,Objective C 的類/方法名等信息
  • DATA栈拖,數(shù)據(jù)段,讀寫没陡,存儲(chǔ) Objective C 的字符串(__cfstring)涩哟,以及運(yùn)行時(shí)的元數(shù)據(jù):class/protocol/method…
  • LINKEDIT,啟動(dòng) App 需要的信息盼玄,如 bind & rebase 的地址贴彼,代碼簽名,符號(hào)表…

dyld
dyld 是啟動(dòng)的輔助程序埃儿,是 in-process 的器仗,即啟動(dòng)的時(shí)候會(huì)把 dyld 加載到進(jìn)程的地址空間里,然后把后續(xù)的啟動(dòng)過程交給 dyld童番。dyld 主要有兩個(gè)版本:dyld2 和 dyld3精钮。
dyld2 是從 iOS 3.1 引入,一直持續(xù)到 iOS 12妓盲。dyld2 有個(gè)比較大的優(yōu)化是dyld shared cache杂拨,什么是 shared cache 呢?

  • shared cache 就是把系統(tǒng)庫(UIKit 等)合成一個(gè)大的文件悯衬,提高加載性能的緩存文件弹沽。

iOS 13 開始 Apple 對(duì)三方 App 啟用了 dyld3,dyld3 的最重要的特性就是啟動(dòng)閉包筋粗,閉包里包含了啟動(dòng)所需要的緩存信息策橘,從而提高啟動(dòng)速度。
虛擬內(nèi)存
內(nèi)存可以分為虛擬內(nèi)存和物理內(nèi)存娜亿,其中物理內(nèi)存是實(shí)際占用的內(nèi)存丽已,虛擬內(nèi)存是在物理內(nèi)存之上建立的一層邏輯地址,保證內(nèi)存訪問安全的同時(shí)為應(yīng)用提供了連續(xù)的地址空間买决。
物理內(nèi)存和虛擬內(nèi)存以頁為單位映射沛婴,但這個(gè)映射關(guān)系不是一一對(duì)應(yīng)的:一頁物理內(nèi)存可能對(duì)應(yīng)多頁虛擬內(nèi)存吼畏;一頁虛擬內(nèi)存也可能不占用物理內(nèi)存。

image

iPhone 6s 開始嘁灯,物理內(nèi)存的 Page 大小是 16K泻蚊,6 和之前的設(shè)備都是 4K,這是 iPhone 6 相比 6s 啟動(dòng)速度斷崖式下降的原因之一丑婿。
mmap
mmap 的全稱是 memory map性雄,是一種內(nèi)存映射技術(shù),可以把文件映射到虛擬內(nèi)存的地址空間里羹奉,這樣就可以像直接操作內(nèi)存那樣來讀寫文件秒旋。當(dāng)讀取虛擬內(nèi)存,其對(duì)應(yīng)的文件內(nèi)容在物理內(nèi)存中不存在的時(shí)候诀拭,會(huì)觸發(fā)一個(gè)事件:File Backed Page In迁筛,把對(duì)應(yīng)的文件內(nèi)容讀入物理內(nèi)存
啟動(dòng)的時(shí)候耕挨,Mach-O 就是通過 mmap 映射到虛擬內(nèi)存里的(如下圖)瑰煎。下圖中部分頁被標(biāo)記為 zero fill,是因?yàn)槿肿兞康某跏贾低际?0俗孝,那么這些 0 就沒必要存儲(chǔ)在二進(jìn)制里,增加文件大小魄健。操作系統(tǒng)會(huì)識(shí)別出這些頁赋铝,在 Page In 之后對(duì)其置為 0,這個(gè)行為叫做 zero fill沽瘦。

image

Page In
啟動(dòng)的路徑上會(huì)觸發(fā)很多次 Page In革骨,其實(shí)也比較容易理解,因?yàn)閱?dòng)的會(huì)讀寫二進(jìn)制中的很多內(nèi)容析恋。Page In 會(huì)占去啟動(dòng)耗時(shí)的很大一部分良哲,我們來看看單個(gè) Page In 的過程:

image
  • MMU 找到空閑的物理內(nèi)存頁面
  • 觸發(fā)磁盤 IO,把數(shù)據(jù)讀入物理內(nèi)存
  • 如果是 TEXT 段的頁助隧,要進(jìn)行解密
  • 對(duì)解密后的頁筑凫,進(jìn)行簽名驗(yàn)證

其中解密是大頭速客,IO 其次胖笛。
為什么要解密呢?因?yàn)?iTunes Connect 會(huì)對(duì)上傳 Mach-O 的 TEXT 段進(jìn)行加密东涡,防止 IPA 下載下來就直接可以看到代碼哩牍。這也就是為什么逆向里會(huì)有個(gè)概念叫做“砸殼”棚潦,砸的就是這一層 TEXT 段加密。iOS 13 對(duì)這個(gè)過程進(jìn)行了優(yōu)化膝昆,Page In 的時(shí)候不需要解密了丸边。
二進(jìn)制重排
既然 Page In 耗時(shí)叠必,有沒有什么辦法優(yōu)化呢?啟動(dòng)具有局部性特征妹窖,即只有少部分函數(shù)在啟動(dòng)的時(shí)候用到纬朝,這些函數(shù)在二進(jìn)制中的分布是零散的,所以 Page In 讀入的數(shù)據(jù)利用率并不高嘱吗。如果我們可以把啟動(dòng)用到的函數(shù)排列到二進(jìn)制的連續(xù)區(qū)間玄组,那么就可以減少 Page In 的次數(shù),從而優(yōu)化啟動(dòng)時(shí)間:
以下圖為例谒麦,方法 1 和方法 3 是啟動(dòng)的時(shí)候用到的俄讹,為了執(zhí)行對(duì)應(yīng)的代碼,就需要兩次 Page In绕德。假如我們把方法 1 和 3 排列到一起患膛,那么只需要一次 Page In,從而提升啟動(dòng)速度耻蛇。

image.png

鏈接器 ld 有個(gè)參數(shù)-order_file 支持按照符號(hào)的方式排列二進(jìn)制踪蹬。獲取啟動(dòng)時(shí)候用到的符號(hào)的有很多種方式,感興趣的同學(xué)可以看看抖音之前的文章:基于二進(jìn)制文件重排的解決方案 APP 啟動(dòng)速度提升超 15%臣咖。
IPA 構(gòu)建
pipeline
既然要構(gòu)建跃捣,那么必然會(huì)有一些地方去定義如何構(gòu)建,對(duì)應(yīng) Xcode 中的兩個(gè)配置項(xiàng):

  • Build Phase:以 Target 為維度定義了構(gòu)建的流程夺蛇【纹幔可以在 Build Phase 中插入腳本,來做一些定制化的構(gòu)建刁赦,比如 CocoaPod 的拷貝資源就是通過腳本的方式完成的娶聘。
  • Build Settings:配置編譯和鏈接相關(guān)的參數(shù)。特別要提到的是 other link flags 和 other c flags甚脉,因?yàn)榫幾g和鏈接的參數(shù)非常多丸升,有些需要手動(dòng)在這里配置。很多項(xiàng)目用的 CocoaPod 做的組件化牺氨,這時(shí)候編譯選項(xiàng)在對(duì)應(yīng)的.xcconfig 文件里狡耻。

以單 Target 為例,我們來看下構(gòu)建流程:

image
  • 源文件(.m/.c/.swift 等)是單獨(dú)編譯的猴凹,輸出對(duì)應(yīng)的目標(biāo)文件(.o)
  • 目標(biāo)文件和靜態(tài)庫/動(dòng)態(tài)庫一起酝豪,鏈接出最后的 Mach-O
  • Mach-O 會(huì)被裁剪,去掉一些不必要的信息
  • 資源文件如 storyboard精堕,asset 也會(huì)編譯孵淘,編譯后加載速度會(huì)變快
  • Mach-O 和資源文件一起,打包出最后的.app
  • 對(duì).app 簽名歹篓,防篡改

編譯
編譯器可以分為兩大部分:前端和后端瘫证,二者以 IR(中間代碼)作為媒介揉阎。這樣前后端分離,使得前后端可以獨(dú)立的變化背捌,互不影響毙籽。C 語言家族的前端是 clang,swift 的前端是 swiftc毡庆,二者的后端都是 llvm坑赡。

  • 前端負(fù)責(zé)預(yù)處理,詞法語法分析么抗,生成 IR
  • 后端基于 IR 做優(yōu)化毅否,生成機(jī)器碼


    image.png

那么如何利用編譯優(yōu)化啟動(dòng)速度呢?
代碼數(shù)量會(huì)影響啟動(dòng)速度蝇刀,為了提升啟動(dòng)速度螟加,我們可以把一些無用代碼下掉。那怎么統(tǒng)計(jì)哪些代碼沒有用到呢吞琐?可以利用 LLVM 插樁來實(shí)現(xiàn)捆探。
LLVM 的代碼優(yōu)化流程是一個(gè)一個(gè) Pass,由于 LLVM 是開源的站粟,我們可以添加一個(gè)自定義的 Pass黍图,在函數(shù)的頭部插入一些代碼,這些代碼會(huì)記錄這個(gè)函數(shù)被調(diào)用了奴烙,然后把統(tǒng)計(jì)到的數(shù)據(jù)上傳分析雌隅,就可以知道哪些代碼是用不到的了 。
Facebook 給 LLVM 提的order_file的 feature 就是實(shí)現(xiàn)了類似的插樁缸沃。
鏈接
經(jīng)過編譯后,我們有很多個(gè)目標(biāo)文件修械,接著這些目標(biāo)文件會(huì)和靜態(tài)庫趾牧,動(dòng)態(tài)庫一起,鏈接出一個(gè) Mach-O肯污。鏈接的過程并不產(chǎn)生新的代碼翘单,只會(huì)做一些移動(dòng)和補(bǔ)丁。

image
  • tbd 的全稱是 text-based stub library蹦渣,是因?yàn)殒溄拥倪^程中只需要符號(hào)就可以了哄芜,所以 Xcode 6 開始,像 UIKit 等系統(tǒng)庫就不提供完整的 Mach-O柬唯,而是提供一個(gè)只包含符號(hào)等信息的 tbd 文件认臊。

舉一個(gè)基于鏈接優(yōu)化啟動(dòng)速度的例子:
最開始講解 Page In 的時(shí)候,我們提到 TEXT 段的頁解密很耗時(shí)锄奢,有沒有辦法優(yōu)化呢失晴?
可以通過 ld 的-rename_section剧腻,把 TEXT 段中的內(nèi)容,比如字符串移動(dòng)到其他的段(啟動(dòng)路徑上難免會(huì)讀很多字符串)涂屁,從而規(guī)避這個(gè)解密的耗時(shí)书在。

image.png

抖音的重命名方案:
"-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring", "-Wl,-rename_section,__TEXT,__const,__RODATA,__const", "-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab", "-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname", "-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname", "-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype" 復(fù)制代碼
裁剪
編譯完 Mach-O 之后會(huì)進(jìn)行裁剪(strip),是因?yàn)槔锩嬗行┬畔⒉鹩郑缯{(diào)試符號(hào)儒旬,是不需要帶到線上去的。裁剪有多種級(jí)別帖族,一般的配置如下:

  • All Symbols栈源,主二進(jìn)制
  • Non-Global Symbols,動(dòng)態(tài)庫
  • Debugging Symbols盟萨,二方靜態(tài)庫

為什么二方庫在出靜態(tài)庫的時(shí)候要選擇 Debugging Symbols 呢凉翻?是因?yàn)橄?order_file 等鏈接期間的優(yōu)化是基于符號(hào)的,如果把符號(hào)裁剪掉捻激,那么這些優(yōu)化也就不會(huì)生效了制轰。
簽名 & 上傳
裁剪完二進(jìn)制后,會(huì)和編譯好的資源文件一起打包成.app 文件胞谭,接著對(duì)這個(gè)文件進(jìn)行簽名垃杖。簽名的作用是保證文件內(nèi)容不多不少,沒有被篡改過丈屹。接著會(huì)把包上傳到 iTunes Connect调俘,上傳后會(huì)對(duì)__TEXT段加密,加密會(huì)減弱 IPA 的壓縮效果旺垒,增加包大小彩库,也會(huì)降低啟動(dòng)速度 (iOS 13 優(yōu)化了加密過程,不會(huì)對(duì)包大小和啟動(dòng)耗時(shí)有影響)先蒋。
dyld3 啟動(dòng)流程
Apple 在 iOS 13 上對(duì)第三方 App 啟用了 dyld3骇钦,官方數(shù)據(jù)顯示,過去四年新發(fā)布的設(shè)備中有 93%的設(shè)備是 iOS 13竞漾,所以我們重點(diǎn)看下 dyld3 的啟動(dòng)流程眯搭。
Before dyld
用戶點(diǎn)擊圖標(biāo)之后,會(huì)發(fā)送一個(gè)系統(tǒng)調(diào)用 execve 到內(nèi)核业岁,內(nèi)核創(chuàng)建進(jìn)程鳞仙。接著會(huì)把主二進(jìn)制 mmap 進(jìn)來,讀取 load command 中的 LC_LOAD_DYLINKER笔时,找到 dyld 的的路徑棍好。然后 mmap dyld 到虛擬內(nèi)存,找到 dyld 的入口函數(shù)_dyld_start,把 PC 寄存器設(shè)置成_dyld_start梳玫,接下來啟動(dòng)流程交給了 dyld爹梁。
注意這個(gè)過程都是在內(nèi)核態(tài)完成的,這里提到了 PC 寄存器提澎,PC 寄存器存儲(chǔ)了下一條指令的地址姚垃,程序的執(zhí)行就是不斷修改和讀取 PC 寄存器來完成的。
dyld
創(chuàng)建啟動(dòng)閉包
dyld 會(huì)首先創(chuàng)建啟動(dòng)閉包盼忌,閉包是一個(gè)緩存积糯,用來提升啟動(dòng)速度的。既然是緩存谦纱,那么必然不是每次啟動(dòng)都創(chuàng)建的看成,只有在重啟手機(jī)或者更新/下載 App 的第一次啟動(dòng)才會(huì)創(chuàng)建。閉包存儲(chǔ)在沙盒的 tmp/com.apple.dyld 目錄跨嘉,清理緩存的時(shí)候切記不要清理這個(gè)目錄川慌。
閉包是怎么提升啟動(dòng)速度的呢?我們先來看一下閉包里都有什么內(nèi)容:

  • dependends祠乃,依賴動(dòng)態(tài)庫列表
  • fixup:bind & rebase 的地址
  • initializer-order:初始化調(diào)用順序
  • optimizeObjc: Objective C 的元數(shù)據(jù)
  • 其他:main entry, uuid…

動(dòng)態(tài)庫的依賴是樹狀的結(jié)構(gòu)梦重,初始化的調(diào)用順序是先調(diào)用樹的葉子結(jié)點(diǎn),然后一層層向上亮瓷,最先調(diào)用的是 libSystem琴拧,因?yàn)樗撬幸蕾嚨脑搭^。

image

為什么閉包能提高啟動(dòng)速度呢嘱支?
因?yàn)檫@些信息是每次啟動(dòng)都需要的蚓胸,把信息存儲(chǔ)到一個(gè)緩存文件就能避免每次都解析,尤其是 Objective C 的運(yùn)行時(shí)數(shù)據(jù)(Class/Method...)解析非常慢除师。
fixup
有了閉包之后沛膳,就可以用閉包啟動(dòng) App 了。這時(shí)候很多動(dòng)態(tài)庫還沒有加載進(jìn)來汛聚,會(huì)首先對(duì)這些動(dòng)態(tài)庫 mmap 加載到虛擬內(nèi)存里锹安。接著會(huì)對(duì)每個(gè) Mach-O 做 fixup,包括 Rebase 和 Bind贞岭。

  • Rebase:修復(fù)內(nèi)部指針。這是因?yàn)?Mach-O 在 mmap 到虛擬內(nèi)存的時(shí)候搓侄,起始地址會(huì)有一個(gè)隨機(jī)的偏移量 slide瞄桨,需要把內(nèi)部的指針指向加上這個(gè) slide。
  • Bind:修復(fù)外部指針讶踪。這個(gè)比較好理解芯侥,因?yàn)橄?printf 等外部函數(shù),只有運(yùn)行時(shí)才知道它的地址是什么,bind 就是把指針指向這個(gè)地址柱查。

舉個(gè)例子:一個(gè) Objective C 字符串@"1234"廓俭,編譯到最后的二進(jìn)制的時(shí)候是會(huì)存儲(chǔ)在兩個(gè) section 里的

  • __TEXT,__cstring唉工,存儲(chǔ)實(shí)際的字符串"1234"
  • __DATA研乒,__cfstring,存儲(chǔ) Objective C 字符串的元數(shù)據(jù)淋硝,每個(gè)元數(shù)據(jù)占用 32Byte雹熬,里面有兩個(gè)指針:內(nèi)部指針,指向__TEXT谣膳,__cstring中字符串的位置竿报;外部指針 isa,指向類對(duì)象的继谚,這就是為什么可以對(duì) Objective C 的字符串字面量發(fā)消息的原因烈菌。

如下圖,編譯的時(shí)候花履,字符串 1234 在__cstring的 0x10 處芽世,所以 DATA 段的指針指向 0x10。但是 mmap 之后有一個(gè)偏移量 slide=0x1000臭挽,這時(shí)候字符串在運(yùn)行時(shí)的地址就是 0x1010捂襟,那么 DATA 段的指針指向就不對(duì)了。Rebase 的過程就是把指針從 0x10欢峰,加上 slide 變成 0x1010葬荷。運(yùn)行時(shí)類對(duì)象的地址已經(jīng)知道了,bind 就是把 isa 指向?qū)嶋H的內(nèi)存地址纽帖。

image

LibSystem Initializer
Bind & Rebase 之后宠漩,首先會(huì)執(zhí)行 LibSystem 的 Initializer,做一些最基本的初始化:

  • 初始化 libdispatch
  • 初始化 objc runtime懊直,注冊(cè) sel扒吁,加載 category

注意這里沒有初始化 objc 的類方法等信息,是因?yàn)閱?dòng)閉包的緩存數(shù)據(jù)已經(jīng)包含了 optimizeObjc室囊。
Load & Static Initializer
接下來會(huì)進(jìn)行 main 函數(shù)之前的一些初始化雕崩,主要包括+load 和 static initializer。這兩類初始化函數(shù)都有個(gè)特點(diǎn):調(diào)用順序不確定融撞,和對(duì)應(yīng)文件的鏈接順序有關(guān)系盼铁。那么就會(huì)存在一個(gè)隱藏的坑:有些注冊(cè)邏輯在+load 里,對(duì)應(yīng)會(huì)有一些地方讀取這些注冊(cè)的數(shù)據(jù)尝偎,如果在+load 中讀取饶火,很有可能讀取的時(shí)候還沒有注冊(cè)鹏控。
那么,如何找到代碼里有哪些 load 和 static initializer 呢肤寝?
在 Build Settings 里可以配置 write linkmap当辐,這樣在生成的 linkmap 文件里就可以找到有哪些文件里包含 load 或者 static initializer:

  • __mod_init_func,static initializer
  • __objc_nlclslist鲤看,實(shí)現(xiàn)+load 的類
  • __objc_nlcatlist缘揪,實(shí)現(xiàn)+load 的 Category

load 舉例
如果+load 方法里的內(nèi)容很簡單,會(huì)影響啟動(dòng)時(shí)間么刨摩?比如這樣的一個(gè)+load 方法寺晌?

  • (void)load { printf("1234"); } 復(fù)制代碼
    編譯完了之后,這個(gè)函數(shù)會(huì)在二進(jìn)制中的 TEXT 兩個(gè)段存在:__text存函數(shù)二進(jìn)制澡刹,cstring存儲(chǔ)字符串 1234呻征。為了執(zhí)行函數(shù),首先要訪問__text觸發(fā)一次 Page In 讀入物理內(nèi)存罢浇,為了打印字符串陆赋,要訪問__cstring,還會(huì)觸發(fā)一次 Page In嚷闭。
  • 為了執(zhí)行這個(gè)簡單的函數(shù)攒岛,系統(tǒng)要額外付出兩次 Page In 的代價(jià),所以 load 函數(shù)多了胞锰,page in 會(huì)成為啟動(dòng)性能的瓶頸灾锯。

[圖片上傳失敗...(image-44cf4d-1603780816666)]

static initializer 產(chǎn)生的條件
靜態(tài)初始化是從哪來的呢?以下幾種代碼會(huì)導(dǎo)致靜態(tài)初始化

  • __attribute__((constructor))
  • static class object
  • static object in global namespace

注意嗅榕,并不是所有的 static 變量都會(huì)產(chǎn)生靜態(tài)初始化顺饮,編譯器很智能,對(duì)于在編譯期間就能確定的變量是會(huì)直接 inline凌那。
//會(huì)產(chǎn)生靜態(tài)初始化 class Demo{ static const std::string var_1; }; const std::string var_2 = "1234"; static Logger logger; //不會(huì)產(chǎn)生靜態(tài)初始化 static const int var_3 = 4; static const char * var_4 = "1234"; 復(fù)制代碼
std::string 會(huì)合成 static initializer 是因?yàn)槌跏蓟臅r(shí)候必須執(zhí)行構(gòu)造函數(shù)兼雄,這時(shí)候編譯器就不知道怎么做了,只能延遲到運(yùn)行時(shí)~
UIKit Init
+load 和 static initializer 執(zhí)行完畢之后帽蝶,dyld 會(huì)把啟動(dòng)流程交給 App赦肋,開始執(zhí)行 main 函數(shù)。main 函數(shù)里要做的最重要的事情就是初始化 UIKit励稳。UIKit 主要會(huì)做兩個(gè)大的初始化:

  • 初始化 UIApplication
  • 啟動(dòng)主線程的 Runloop

由于主線程的 dispatch_async 是基于 runloop 的佃乘,所以在+load 里如果調(diào)用了 dispatch_async 會(huì)在這個(gè)階段執(zhí)行。
Runloop
線程在執(zhí)行完代碼就會(huì)退出驹尼,很明顯主線程是不能退出的趣避,那么就需要一種機(jī)制:事件來的時(shí)候執(zhí)行任務(wù),否則讓線程休眠扶欣,Runloop 就是實(shí)現(xiàn)這個(gè)功能的鹅巍。
Runloop 本質(zhì)上是一個(gè) While 循環(huán),在圖中橙色部分的 mach_msg_trap 就是觸發(fā)一個(gè)系統(tǒng)調(diào)用料祠,讓線程休眠骆捧,等待事件到來,喚醒 Runloop髓绽,繼續(xù)執(zhí)行這個(gè) while 循環(huán)敛苇。
Runloop 主要處理幾種任務(wù):Source0,Source1顺呕,Timer枫攀,GCD MainQueue,Block株茶。在循環(huán)的合適時(shí)機(jī)来涨,會(huì)以 Observer 的方式通知外部執(zhí)行到了哪里。

image

那么启盛,Runloop 與啟動(dòng)又有什么關(guān)系呢蹦掐?

  • App 的 LifeCycle 方法是基于 Runloop 的 Source0 的
  • 首幀渲染是基于 Runloop Block 的
image

Runloop 在啟動(dòng)上主要有幾點(diǎn)應(yīng)用:

  • 精準(zhǔn)統(tǒng)計(jì)啟動(dòng)時(shí)間
  • 找到一個(gè)時(shí)機(jī),在啟動(dòng)結(jié)束去執(zhí)行一些預(yù)熱任務(wù)
  • 利用 Runloop 打散耗時(shí)的啟動(dòng)預(yù)熱任務(wù)

Tips: 會(huì)有一些邏輯要在啟動(dòng)之后 delay 一小段時(shí)間再回到主線程上執(zhí)行僵闯,對(duì)于性能較差的設(shè)備卧抗,主線程 Runloop 可能一直處于忙的狀態(tài),所以這個(gè) delay 的任務(wù)并不一定能按時(shí)執(zhí)行鳖粟。

AppLifeCycle
UIKit 初始化之后社裆,就進(jìn)入了我們熟悉的 UIApplicationDelegate 回調(diào)了,在這些會(huì)調(diào)里去做一些業(yè)務(wù)上的初始化:

  • willFinishLaunch

  • didFinishLaunch

  • didFinishLaunchNotification

要特別提一下 didFinishLaunchNotification向图,是因?yàn)榇蠹以诼顸c(diǎn)的時(shí)候通常會(huì)忽略還有這個(gè)通知的存在泳秀,導(dǎo)致把這部分時(shí)間算到 UI 渲染里。
First Frame Render
一般會(huì)用 Root Controller 的 viewDidApper 作為渲染的終點(diǎn)张漂,但其實(shí)這時(shí)候首幀已經(jīng)渲染完成一小段時(shí)間了晶默,Apple 在 MetricsKit 里對(duì)啟動(dòng)終點(diǎn)定義是第一個(gè)CA::Transaction::commit()
什么是 CATransaction 呢航攒?我們先來看一下渲染的大致流程

image

iOS 的渲染是在一個(gè)單獨(dú)的進(jìn)程 RenderServer 做的磺陡,App 會(huì)把 Render Tree 編碼打包給 RenderServer,RenderServer 再調(diào)用渲染框架(Metal/OpenGL ES)來生成 bitmap漠畜,放到幀緩沖區(qū)里币他,硬件根據(jù)時(shí)鐘信號(hào)讀取幀緩沖區(qū)內(nèi)容,完成屏幕刷新憔狞。CATransaction 就是把一組 UI 上的修改蝴悉,合并成一個(gè)事務(wù),通過 commit 提交瘾敢。
渲染可以分為四個(gè)步驟

  • Layout(布局)拍冠,源頭是 Root Layer 調(diào)用[CALayer layoutSubLayers]尿这,這時(shí)候 UIViewControllerviewDidLoadLayoutSubViews 會(huì)調(diào)用,autolayout 也是在這一步生效
  • Display(繪制)庆杜,源頭是 Root Layer 調(diào)用[CALayer display]射众,如果 View 實(shí)現(xiàn)了 drawRect 方法,會(huì)在這個(gè)階段調(diào)用
  • Prepare(準(zhǔn)備)晃财,這個(gè)過程中會(huì)完成圖片的解碼
  • Commit(提交)叨橱,打包 Render Tree 通過 XPC 的方式發(fā)給 Render Server
image

啟動(dòng) Pipeline
詳細(xì)回顧下整個(gè)啟動(dòng)過程,以及各個(gè)階段耗時(shí)的影響因素:

image
  1. 點(diǎn)擊圖標(biāo)断盛,創(chuàng)建進(jìn)程
  2. mmap 主二進(jìn)制罗洗,找到 dyld 的路徑
  3. mmap dyld,把入口地址設(shè)為_dyld_start
  4. 重啟手機(jī)/更新/下載 App 的第一次啟動(dòng)钢猛,會(huì)創(chuàng)建啟動(dòng)閉包
  5. 把沒有加載的動(dòng)態(tài)庫 mmap 進(jìn)來伙菜,動(dòng)態(tài)庫的數(shù)量會(huì)影響這個(gè)階段
  6. 對(duì)每個(gè)二進(jìn)制做 bind 和 rebase,主要耗時(shí)在 Page In命迈,影響 Page In 數(shù)量的是 objc 的元數(shù)據(jù)
  7. 初始化 objc 的 runtime仇让,由于閉包已經(jīng)初始化了大部分,這里只會(huì)注冊(cè) sel 和裝載 category
  8. +load 和靜態(tài)初始化被調(diào)用躺翻,除了方法本身耗時(shí)丧叽,這里還會(huì)引起大量 Page In
  9. 初始化 UIApplication,啟動(dòng) Main Runloop
  10. 執(zhí)行 will/didFinishLaunch公你,這里主要是業(yè)務(wù)代碼耗時(shí)
  11. Layout踊淳,viewDidLoadLayoutsubviews 會(huì)在這里調(diào)用,Autolayout 太多會(huì)影響這部分時(shí)間
  12. Display陕靠,drawRect 會(huì)調(diào)用
  13. Prepare迂尝,圖片解碼發(fā)生在這一步
  14. Commit,首幀渲染數(shù)據(jù)打包發(fā)給 RenderServer剪芥,啟動(dòng)結(jié)束

dyld2
dyld2 和 dyld3 的主要區(qū)別就是沒有啟動(dòng)閉包垄开,就導(dǎo)致每次啟動(dòng)都要:

  • 解析動(dòng)態(tài)庫的依賴關(guān)系
  • 解析 LINKEDIT,找到 bind & rebase 的指針地址税肪,找到 bind 符號(hào)的地址
  • 注冊(cè) objc 的 Class/Method 等元數(shù)據(jù)溉躲,對(duì)大型工程來說,這部分耗時(shí)會(huì)很長

總結(jié)
本文回顧了 Mach-O益兄,虛擬內(nèi)存锻梳,mmap,Page In净捅,Runloop 等基礎(chǔ)概念疑枯,接下來介紹了 IPA 的構(gòu)建流程,以及兩個(gè)典型的利用編譯器來優(yōu)化啟動(dòng)的方案蛔六,最后詳細(xì)的講解了 dyld3 的啟動(dòng) pipeline荆永。
之所以花這么大篇幅講原理废亭,是因?yàn)槿魏蝺?yōu)化都一樣,只有深入理解系統(tǒng)運(yùn)作的原理具钥,才能找到性能的瓶頸滔以,下一篇我們會(huì)介紹下如何利用這些原理解決實(shí)際問題。

本人自己整理的一些面試進(jìn)階資料氓拼,iOS底層視頻+BAT面試專題PDF+核心筆記等資料+2020年《大廠最新常問iOS面試題+答案》希望能夠幫助到大家提升技術(shù)抵碟。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末桃漾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拟逮,更是在濱河造成了極大的恐慌撬统,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敦迄,死亡現(xiàn)場離奇詭異恋追,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)罚屋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門苦囱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脾猛,你說我怎么就攤上這事撕彤。” “怎么了猛拴?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵羹铅,是天一觀的道長。 經(jīng)常有香客問我愉昆,道長职员,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任跛溉,我火速辦了婚禮焊切,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芳室。我一直安慰自己蛛蒙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布渤愁。 她就那樣靜靜地躺著牵祟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抖格。 梳的紋絲不亂的頭發(fā)上诺苹,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天咕晋,我揣著相機(jī)與錄音,去河邊找鬼收奔。 笑死掌呜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坪哄。 我是一名探鬼主播质蕉,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翩肌!你這毒婦竟也來了模暗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤念祭,失蹤者是張志新(化名)和其女友劉穎兑宇,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粱坤,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡隶糕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了站玄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枚驻。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖株旷,靈堂內(nèi)的尸體忽然破棺而出测秸,到底是詐尸還是另有隱情,我是刑警寧澤灾常,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布霎冯,位于F島的核電站,受9級(jí)特大地震影響钞瀑,放射性物質(zhì)發(fā)生泄漏沈撞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一雕什、第九天 我趴在偏房一處隱蔽的房頂上張望缠俺。 院中可真熱鬧,春花似錦贷岸、人聲如沸壹士。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躏救。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盒使,已是汗流浹背崩掘。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留少办,地道東北人苞慢。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像英妓,于是被迫代替她去往敵國和親挽放。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353