背景
隨著需求爆發(fā),代碼和圖片資源越來(lái)越多分瘦,包體積越來(lái)越大蘸泻,用戶下載成本越來(lái)越高,瘦包迫在眉睫嘲玫,要想瘦包蟋恬,就需要知道包由何組成,每個(gè)組成部分又是怎么來(lái)的趁冈,這就必須了解編譯過(guò)程,當(dāng)然有人會(huì)說(shuō)拜马,不就瘦包嘛渗勘,網(wǎng)上有教程呀,巴拉巴拉照著做就行了俩莽!
嗯~那證明app的包還不夠復(fù)雜旺坠,當(dāng)你需要引用上百個(gè)自研或是開(kāi)源庫(kù)的時(shí)候自會(huì)明白,我們來(lái)看下文件組成
iOS應(yīng)用的文件目錄結(jié)構(gòu)
一個(gè)app通常有如下幾個(gè)模塊組成:源碼編譯后的二進(jìn)制(靜態(tài)庫(kù)包含在其中)扮超、動(dòng)態(tài)庫(kù)取刃、bundle等資源文件、plist等配置文件出刷、代碼簽名文件CodeResources
源碼編譯后的二進(jìn)制
靜態(tài)庫(kù)是單獨(dú)編譯的璧疗,在源碼文件編譯完成后會(huì)由靜態(tài)鏈接器一起打包生成最終的Mach-O格式的二進(jìn)制文件,這就意味著多份相同的靜態(tài)庫(kù)是沒(méi)有辦法鏈接通過(guò)的馁龟,有一種辦法可以解決這個(gè)問(wèn)題崩侠,就是同一份靜態(tài)庫(kù)包裝到不同的動(dòng)態(tài)庫(kù)中以形成隔離,因?yàn)閯?dòng)態(tài)庫(kù)在app編譯完成后不會(huì)一起進(jìn)行靜態(tài)鏈接并且符號(hào)表也是與app分開(kāi)的坷檩,在集成三方庫(kù)的時(shí)候可能會(huì)遇到這個(gè)問(wèn)題動(dòng)態(tài)庫(kù)
動(dòng)態(tài)庫(kù)也是單獨(dú)編譯的却音,我們程序中用到的所有系統(tǒng)級(jí)別的framework都是動(dòng)態(tài)庫(kù),它們集成在操作系統(tǒng)中矢炼,不會(huì)占用app的存儲(chǔ)空間系瓢,自研的動(dòng)態(tài)庫(kù)是集成在app目錄下的Frameworks中,編譯時(shí)句灌,動(dòng)態(tài)庫(kù)的物理路徑需要配置到app的編譯設(shè)置的搜索路徑夷陋,在App編譯的時(shí)候就會(huì)被拷貝到app的壓縮包里的Frameworks文件夾下,符號(hào)表單獨(dú)保存并且需要上傳到蘋(píng)果的后臺(tái)bundle
程序的圖片資源文件plist配置文件
程序的標(biāo)識(shí)符以及系統(tǒng)服務(wù)的訪問(wèn)權(quán)限等都配置在里面代碼簽名
就是存儲(chǔ)通過(guò)證書(shū)以及加密算法生成簽名密鑰的文件
可執(zhí)行文件的物理結(jié)構(gòu)
可執(zhí)行文件包含了機(jī)器指令代碼、數(shù)據(jù)肌稻、符號(hào)表清蚀、調(diào)試信息等,目標(biāo)文件以段的形式存儲(chǔ)以上信息爹谭,目標(biāo)文件通常包括:文件頭枷邪、text段、data段诺凡、bss段
文件頭
描述整個(gè)文件的屬性(包含目標(biāo)機(jī)器架構(gòu)东揣、是動(dòng)態(tài)鏈接還是靜態(tài)鏈接以及是否是可執(zhí)行文件)和一個(gè)段表
段表就是存儲(chǔ)段的數(shù)組,描述了各個(gè)段在文件中的偏移量以及段本身的屬性text段
源代碼編譯后的機(jī)器指令被存放在text段腹泌,又叫做代碼段data段
初始化的全局變量和靜態(tài)變量被放在data段嘶卧,又叫做數(shù)據(jù)段bss段
未初始化的全局變量和靜態(tài)變量被放在bss段,和data段一起被稱(chēng)為數(shù)據(jù)段
符號(hào)表
符號(hào)通常指函數(shù)和變量(還有其他符號(hào)比如段名凉袱、行號(hào)信息等)芥吟,編譯的最后一個(gè)階段靜態(tài)鏈接,就是符號(hào)的處理過(guò)程专甩,每個(gè)目標(biāo)文件都有一個(gè)符號(hào)表钟鸵,記錄了目標(biāo)文件里面所有的符號(hào)以及對(duì)應(yīng)的符號(hào)值,對(duì)于函數(shù)和變量來(lái)講符號(hào)值就是他們的地址涤躲,符號(hào)表在程序編譯之初就創(chuàng)建棺耍,在整個(gè)編譯過(guò)程中都起著至關(guān)重要的作用,在靜態(tài)鏈接完成之后會(huì)生成最終的符號(hào)表种樱,動(dòng)態(tài)庫(kù)由于不參與主可執(zhí)行文件的靜態(tài)鏈接過(guò)程蒙袍,所以符號(hào)表是抽離的
調(diào)試信息
編譯器支持源代碼級(jí)別的調(diào)試,debug環(huán)境下可以在程序中打斷點(diǎn)調(diào)試嫩挤,release環(huán)境下編譯器會(huì)過(guò)濾掉調(diào)試信息害幅,即編譯后的二進(jìn)制里面沒(méi)有調(diào)試代碼,因此斷點(diǎn)是無(wú)效的
可執(zhí)行程序二進(jìn)制(包含了靜態(tài)庫(kù))俐镐、動(dòng)態(tài)庫(kù)都是編譯器的產(chǎn)物矫限,那么為什么需要編譯呢,源碼不可以直接執(zhí)行嗎
Object-C為什么需要編譯
- 解釋型語(yǔ)言
解釋型語(yǔ)言運(yùn)行時(shí)實(shí)時(shí)被解釋器解析為機(jī)器碼并且執(zhí)行一次就需要解析一次佩抹,腳本語(yǔ)言如JavaScript等都是解釋型語(yǔ)言叼风,優(yōu)點(diǎn)是省去了編譯過(guò)程,但是運(yùn)行效率低棍苹,雖然當(dāng)代瀏覽器解釋器經(jīng)過(guò)深度優(yōu)化无宿,但是相比直接運(yùn)行可執(zhí)行二進(jìn)制來(lái)講也會(huì)慢很多 - 編譯型語(yǔ)言
有一個(gè)復(fù)雜又耗時(shí)的編譯過(guò)程,最終生成的二進(jìn)制機(jī)器碼枢里,可以被處理器直接識(shí)別并執(zhí)行孽鸡,相比解釋性語(yǔ)言來(lái)說(shuō)運(yùn)行時(shí)效率高了很多
Object-C是一門(mén)編譯型語(yǔ)言蹂午,底層通過(guò)c、c++彬碱、匯編實(shí)現(xiàn)豆胸,上層架設(shè)了一層語(yǔ)法糖,配合強(qiáng)大的運(yùn)行時(shí)庫(kù)使用
編譯原理
一句話概括:從源碼生成二進(jìn)制機(jī)器碼的過(guò)程就是編譯過(guò)程巷疼,整個(gè)過(guò)程分為四個(gè)階段:預(yù)編譯晚胡、編譯、匯編嚼沿、靜態(tài)鏈接
- 預(yù)編譯:
進(jìn)行宏替換估盘、頭文件包含、條件編譯識(shí)別等工作 - 詞法分析:
將源代碼的字符序列分割成一些列的記號(hào)骡尽,包含關(guān)鍵字遣妥、標(biāo)識(shí)符、字面量攀细、符號(hào)箫踩,同時(shí)將標(biāo)識(shí)符存放到符號(hào)表,常量存放到文字表以備后續(xù)使用 - 語(yǔ)法分析:
把上一步產(chǎn)生的記號(hào)解析成一個(gè)以表達(dá)式為節(jié)點(diǎn)的抽象語(yǔ)法樹(shù)(AST)谭贪,這個(gè)階段會(huì)進(jìn)行表達(dá)式合法性校驗(yàn)班套,比如缺少操作符、括弧不匹配等編譯器會(huì)直接給出錯(cuò)誤提示故河,當(dāng)然xcode在沒(méi)有bulid之前也會(huì)進(jìn)行語(yǔ)法檢測(cè),這也是衡量一款開(kāi)發(fā)工具是否合格的基本要求 - 語(yǔ)義分析:
編譯器只能分析靜態(tài)語(yǔ)義吆豹,即編譯期間就可以確定的語(yǔ)義鱼的,通常包括類(lèi)型匹配、轉(zhuǎn)換痘煤,這個(gè)階段編譯器會(huì)給出類(lèi)型不匹配等警告凑阶,不會(huì)報(bào)錯(cuò),因?yàn)檫\(yùn)行時(shí)基本類(lèi)型不匹配實(shí)際會(huì)進(jìn)行精度取舍衷快,指針類(lèi)型不匹配會(huì)以指針?biāo)赶虻恼鎸?shí)對(duì)象去調(diào)用方法宙橱,這些都是動(dòng)態(tài)語(yǔ)義,不在編譯器的管理范疇蘸拔,當(dāng)然也沒(méi)有這個(gè)能力 - 中間代碼生成:
由于直接在語(yǔ)法樹(shù)上做優(yōu)化比較困難师郑,編譯器會(huì)將整個(gè)語(yǔ)法樹(shù)轉(zhuǎn)換成中間代碼,它是語(yǔ)法樹(shù)的順序表示调窍,至此已經(jīng)生成接近目標(biāo)代碼的匯編代碼宝冕,只是還與目標(biāo)機(jī)器的運(yùn)行時(shí)環(huán)境無(wú)關(guān),比如機(jī)器字長(zhǎng)邓萨、變量地址地梨、寄存器名稱(chēng)等菊卷,因此中間代碼是編譯前后端的分割線,前端負(fù)責(zé)產(chǎn)生中間代碼宝剖,后端負(fù)責(zé)將其轉(zhuǎn)換為目標(biāo)代碼洁闰,這也使得編譯器跨平臺(tái)成為可能 - 目標(biāo)代碼生成與優(yōu)化:
目標(biāo)代碼生成器會(huì)根據(jù)目標(biāo)機(jī)器的運(yùn)行環(huán)境將中間代碼轉(zhuǎn)換為目標(biāo)代碼,目標(biāo)代碼優(yōu)化器會(huì)對(duì)匯編碼進(jìn)行優(yōu)化万细,比如循環(huán)優(yōu)化扑眉、多余指令刪除、尋址優(yōu)化雅镊、用位移操作代替乘法運(yùn)算等 - 匯編:
匯編器是編譯后端的后端襟雷,負(fù)責(zé)將匯編碼轉(zhuǎn)化為機(jī)器碼 - 靜態(tài)鏈接:
編譯器會(huì)把源代碼編譯成一個(gè)一個(gè)的目標(biāo)文件.o文件,定義在A.o文件中的變量及函數(shù)在B.o文件中是無(wú)法得到地址的仁烹,編譯器會(huì)用0填充耸弄,待到鏈接的時(shí)候由鏈接器進(jìn)行修正,這個(gè)過(guò)程叫做重定位卓缰,最終靜態(tài)鏈接器把編譯產(chǎn)生的所有.o文件和靜態(tài)庫(kù)一起打包生成一個(gè)Mach-O格式的二進(jìn)制文件
減小包體積
我們以目標(biāo)為導(dǎo)向计呈,瘦包需要瘦哪些模塊:可執(zhí)行二進(jìn)制文件、動(dòng)態(tài)庫(kù)征唬、Bundle資源
- 第一招:??
前面優(yōu)化圖片資源會(huì)帶來(lái)很多驚喜捌显,掃描無(wú)用資源文件直接刪除和壓縮Bundle中的圖片,可能分分鐘降下來(lái)幾十兆总寒,一期目標(biāo)直接達(dá)成扶歪,接下來(lái)就要對(duì)代碼部分動(dòng)刀了 - 第二招:?
經(jīng)過(guò)上面的編譯原理我們知道不同架構(gòu)的目標(biāo)機(jī)器編譯出來(lái)的目標(biāo)代碼是不一樣的,iOS的app是一個(gè)胖架構(gòu)包摄闸,可以包含很多架構(gòu)在里面善镰,打包送上應(yīng)用商店后蘋(píng)果會(huì)對(duì)包進(jìn)行拆分,不同架構(gòu)的手機(jī)下載對(duì)應(yīng)架構(gòu)的包年枕,因此減少app兼容的架構(gòu)是不能夠減小包體積的 - 第三招:???參半
掃描刪減無(wú)用代碼炫欺,合并類(lèi)似功能的冗余代碼,這招對(duì)于源文件熏兄、動(dòng)態(tài)庫(kù)都會(huì)奏效品洛,對(duì)于靜態(tài)庫(kù)來(lái)講未必奏效,因?yàn)殪o態(tài)庫(kù)在鏈接的時(shí)候只有用到的部分才會(huì)打包到可執(zhí)行二進(jìn)制 - 第四招:???需要根據(jù)包本身的體積衡量
多個(gè)動(dòng)態(tài)庫(kù)引用了相同一份靜態(tài)庫(kù)會(huì)分別在動(dòng)態(tài)庫(kù)中鏈接到使用的部分摩桶,換句話說(shuō)靜態(tài)庫(kù)被動(dòng)態(tài)庫(kù)使用的部分會(huì)打包到動(dòng)態(tài)庫(kù)中桥状,因此多個(gè)動(dòng)態(tài)庫(kù)就打包了多份靜態(tài)庫(kù)(當(dāng)然只有使用到的部分),這里可以做個(gè)優(yōu)化把靜態(tài)庫(kù)變成動(dòng)態(tài)庫(kù)硝清,這樣就是所有動(dòng)態(tài)庫(kù)引用一份動(dòng)態(tài)庫(kù)岛宦,不會(huì)出現(xiàn)多份冗余的情況,當(dāng)然要考慮到庫(kù)包本身的大小耍缴,如果太大做成動(dòng)態(tài)庫(kù)反而會(huì)增大包體積砾肺,因?yàn)閯?dòng)態(tài)庫(kù)是全量拷貝到app包內(nèi)的