為什么要給程序瘦身跟磨?
隨著應(yīng)用程序的功能越來越多,實(shí)現(xiàn)越來越復(fù)雜岭辣,第三方庫的引入吱晒,UI體驗(yàn)的優(yōu)化等眾多因素程序中的代碼量成倍的增長甸饱,從而導(dǎo)致應(yīng)用程序包的體積越來越大沦童。當(dāng)程序體積變大后不僅會(huì)出現(xiàn)編譯流程變慢,而且還會(huì)出現(xiàn)運(yùn)行性能問題叹话,會(huì)增加應(yīng)用下載時(shí)長和消耗用戶的移動(dòng)網(wǎng)絡(luò)流量等等偷遗。因此在這些眾多的問題下需要對(duì)應(yīng)用進(jìn)行瘦身處理。
一個(gè)應(yīng)用程序由眾多資源文件和可執(zhí)行程序文件組成驼壶,資源文件的優(yōu)化不在本文探討范圍氏豌。本文主要討論對(duì)可執(zhí)行程序代碼瘦身的方法。
對(duì)可執(zhí)行程序代碼瘦身主要就是想辦法讓程序中不會(huì)被調(diào)用的源代碼不參與編譯或鏈接热凹。我們可以通過一些源代碼分析工具來查找哪些函數(shù)或者類方法沒有被調(diào)用并從代碼中刪除掉來解決編譯鏈接前的瘦身問題泵喘。這些分析工具也不在本文的討論范圍內(nèi)。應(yīng)用程序在編譯時(shí)會(huì)對(duì)工程中的所有代碼都執(zhí)行編譯處理并生成目標(biāo)文件般妙。而在鏈接階段則會(huì)根據(jù)程序代碼中對(duì)符號(hào)的引用關(guān)系來將所有相關(guān)的目標(biāo)文件鏈接為一個(gè)大的可執(zhí)行程序文件纪铺,并且在鏈接階段鏈接器會(huì)優(yōu)化掉所有沒被調(diào)用的C/C++函數(shù)代碼,但是對(duì)于OC類中的沒有調(diào)用的方法則不會(huì)被優(yōu)化掉碟渺。所以為了對(duì)可執(zhí)行程序在編譯鏈接階段進(jìn)行瘦身處理就需要了解源代碼的編譯鏈接規(guī)則鲜锚。這也是本文所要介紹的針對(duì)工程通過靜態(tài)庫的形式進(jìn)行編譯和鏈接的方式來減少可執(zhí)行程序代碼的尺寸。您可以從文章:《深入iOS系統(tǒng)底層之靜態(tài)庫介紹》中詳細(xì)的了解到靜態(tài)庫的編譯鏈接過程,以及相關(guān)的技術(shù)細(xì)節(jié)芜繁。
一個(gè)瘦身的例子旺隙!
為了驗(yàn)證和具體的實(shí)踐,我在github上建立了一個(gè)項(xiàng)目:YSAppSizeTest骏令。您可以從這個(gè)項(xiàng)目中看到如何對(duì)工程進(jìn)行構(gòu)建以實(shí)現(xiàn)程序的瘦身處理蔬捷。
在示例項(xiàng)目中同一個(gè)Workspace中分別建立ThinApp和FatApp兩個(gè)工程,這兩個(gè)工程實(shí)現(xiàn)的功能是一樣榔袋。在整個(gè)應(yīng)用程序中分別定義了CA抠刺、CB、CC摘昌、CD速妖、CE一共5個(gè)OC類,定義了一個(gè)UIView(Test)分類聪黎,還有定義了兩個(gè)C函數(shù):libFoo1和libFoo1罕容。
整個(gè)應(yīng)用程序中只使用了CA和CC兩個(gè)OC類,以及調(diào)用了UIView(Test)分類方法稿饰,以及調(diào)用了libFoo1函數(shù)锦秒,并且同時(shí)都采用導(dǎo)入靜態(tài)庫的形式。因?yàn)檫@兩個(gè)工程對(duì)文件的定義和分布策略不同使得兩個(gè)應(yīng)用程序的最終可執(zhí)行代碼的尺寸是不相同的喉镰。
FatApp中的文件定義和分布策略
- FatApp工程依賴并導(dǎo)入了FatAppLib靜態(tài)庫工程旅择。
- CA,CB兩個(gè)類都定義在主程序工程中。
- CC,CD,CE三個(gè)類侣姆,以及UIView(Test)分類生真,還有l(wèi)ibFoo1,libFoo2兩個(gè)函數(shù)都定義在FatAppLib靜態(tài)庫工程中。
- CC,CD兩個(gè)類定義在同一個(gè)文件中捺宗,CE類則定義在單獨(dú)的文件中柱蟀。
- FatApp工程的Other Linker Flags中設(shè)置了 -ObjC選項(xiàng)。
ThinApp中的文件定義和分布策略
- ThinApp工程依賴并導(dǎo)入了ThinAppLib靜態(tài)庫工程蚜厉。
- 主程序工程就是一個(gè)殼工程长已。
- CA,CB,CC,CD,CE5個(gè)類,以及UIView(Test)分類昼牛,還有l(wèi)ibFoo1,libFoo2兩個(gè)函數(shù)都定義在ThinAppLib靜態(tài)庫工程中术瓮。
- 上述的5個(gè)類都分別定義在不同的文件中。
- ThinApp工程的Other Linker Flags中沒有設(shè)置-ObjC選項(xiàng)贰健。
上述兩個(gè)工程的程序被Archive出來后胞四,F(xiàn)atApp可執(zhí)行程序的尺寸是367KB,而ThinApp可執(zhí)行程序的尺寸是334KB霎烙。通過一些工具比如Mach-O View或者 IDA可以看出:FatApp中5個(gè)OC類的代碼以及l(fā)ibFoo1函數(shù)還有UIView(Test)分類的代碼都被鏈接進(jìn)可執(zhí)行程序中撬讽;而ThinApp中則只有CA,CC兩個(gè)類以及l(fā)ibFoo1函數(shù)還有UIView(Test)分類的代碼被鏈接進(jìn)可執(zhí)行程序中蕊连。在ThinApp中雖然沒有使用-Objc鏈接選項(xiàng),但是靜態(tài)庫中的分類也被鏈接進(jìn)可執(zhí)行程序中游昼。
應(yīng)用程序工程構(gòu)建規(guī)則
根據(jù)對(duì)項(xiàng)目中的文件定義和引用策略以及相關(guān)的理論基礎(chǔ)我們可以按照如下的規(guī)則來構(gòu)建您的應(yīng)用程序:
盡量將所有代碼都移植到靜態(tài)庫中甘苍,而主程序則保留為一個(gè)殼程序。具體操作方法是建立一個(gè)Workspace烘豌,然后主程序工程就只有默認(rèn)創(chuàng)建工程時(shí)的代碼载庭,所有新加入的代碼都建立并存放到靜態(tài)庫工程中去,然后通過工程依賴來引入這些靜態(tài)庫工程廊佩,或者借助一些工程化工具比如Cocoapods來實(shí)現(xiàn)這種拆分和引用處理囚聚。主程序工程中只保留AppDelegate的代碼,其他代碼都一致到靜態(tài)庫中标锄。然后在AppDelegate中的相關(guān)代碼處調(diào)用靜態(tài)庫中定義的業(yè)務(wù)代碼顽铸。
按業(yè)務(wù)組件對(duì)工程進(jìn)行解耦每個(gè)組件是一個(gè)靜態(tài)庫工程。靜態(tài)庫中的每一個(gè)文件中最好只有一個(gè)類的實(shí)現(xiàn)料皇,并且類的分類實(shí)現(xiàn)最好和類實(shí)現(xiàn)編寫在同一個(gè)文件中谓松,相同功能的代碼以及可能都會(huì)被調(diào)用的代碼盡量存放在一個(gè)文件中。
不要在主程序工程中使用-ObjC和-all_load兩個(gè)選項(xiàng)而改為用-force_load 來單獨(dú)指定要執(zhí)行加載的靜態(tài)庫践剂。-ObjC和-all_load選項(xiàng)會(huì)把主程序工程以及所依賴的所有靜態(tài)庫中的工程中的全部代碼都鏈接到可執(zhí)行程序中而不管代碼是否有被調(diào)用過或者使用過鬼譬。而force_load則只會(huì)將指定的靜態(tài)庫中的所有代碼鏈接到可執(zhí)行程序中,當(dāng)然force_load如果沒有必要也盡量不要使用逊脯。
盡量減少在靜態(tài)庫中定義OC類的分類方法优质,如果一定要定義分類方法則可以將分類方法定義在和類定義相同的文件中,或者將分類方法定義在一個(gè)一定會(huì)被調(diào)用和引用的實(shí)現(xiàn)文件中军洼。因?yàn)楦鶕?jù)鏈接規(guī)則靜態(tài)庫中的分類是不會(huì)被鏈接進(jìn)可執(zhí)行程序中的巩螃,除非使用了上述的三個(gè)鏈接選項(xiàng)。如果將分類代碼單獨(dú)的定義在一個(gè)文件中的話則可以通過在分類的頭文件中定義一個(gè)內(nèi)聯(lián)函數(shù)歉眷,內(nèi)聯(lián)函數(shù)調(diào)用分類實(shí)現(xiàn)文件中的一個(gè)dumy函數(shù),這樣只要這個(gè)分類的頭文件被include或者import就會(huì)把整個(gè)分類的實(shí)現(xiàn)鏈接到可執(zhí)行程序中去牺六。一般情況下我們?cè)陟o態(tài)庫中建立分類那就表明一定會(huì)被某個(gè)文件引用這個(gè)分類颤枪,從而實(shí)現(xiàn)整個(gè)文件的鏈接處理汗捡。在分類中定義的這兩個(gè)函數(shù)則因?yàn)闆]有被任何地方調(diào)用,因此會(huì)在鏈接優(yōu)化中將這兩個(gè)函數(shù)給優(yōu)化掉畏纲。這樣就使得即使我們不用-ObjC選項(xiàng)也能將靜態(tài)庫中的分類鏈接到可執(zhí)行程序中去扇住。最后需要注意的是在每個(gè)分類中定義的這兩個(gè)函數(shù)名最好能夠唯一這樣就不會(huì)出現(xiàn)符號(hào)重名沖突的問題了。
//分類文件的頭文件UIView+XXX.h
@interface UIView (XXX)
//分類中定義的方法
@end
/*
通過在分類的頭文件中定義一個(gè)內(nèi)聯(lián)函數(shù)盗胀,內(nèi)聯(lián)函數(shù)調(diào)用分類實(shí)現(xiàn)文件中的一個(gè)dumy函數(shù),這樣只要這個(gè)分類的頭文件被include或者import就會(huì)把
整個(gè)分類的實(shí)現(xiàn)鏈接到可執(zhí)行程序中去艘蹋。一般情況下我們?cè)陟o態(tài)庫中建立分類那就表明一定會(huì)被某個(gè)文件引用這個(gè)分類,從而實(shí)現(xiàn)整個(gè)文件的鏈接處理票灰。
而在分類中定義的這兩個(gè)函數(shù)則因?yàn)闆]有被任何地方調(diào)用女阀,因此會(huì)在鏈接優(yōu)化中將這兩個(gè)函數(shù)給優(yōu)化掉宅荤。這樣就使得即使我們不用-ObjC選項(xiàng)也能
將靜態(tài)庫中的分類鏈接到可執(zhí)行程序中去。最后需要注意的是在每個(gè)分類中定義的這兩個(gè)函數(shù)名最好能夠唯一這樣就不會(huì)出現(xiàn)符號(hào)重名沖突的問題了浸策。
*/
extern void _cat_UIView_XXX_Impl(void);
inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}
------------------------------------------------------------
//分類文件的實(shí)現(xiàn)文件UIView+XXX.m
#import "UIView+XXX.h"
@implementation UIView (XXX)
//分類的實(shí)現(xiàn)代碼
@end
void _cat_UIView_XXX_Impl(void){}
---------------------------------------------------------------
//最后把這個(gè)分類頭文件放入到某個(gè)對(duì)外暴露的頭文件中冯键,比如本例中將分類代碼放入到了ThinAppLib.h文件中
//ThinAppLib.h
#import "UIView+XXX.h"
//其他頭文件
- 除了可以通過-force_load來加載指定靜態(tài)庫中的所有代碼外。我們還可以在構(gòu)建靜態(tài)庫時(shí)庸汗,在靜態(tài)庫的工程的
Build Settings
中將Perform Single-Object Prelink 中的開關(guān)選項(xiàng)打開惫确。當(dāng)這個(gè)開關(guān)打開時(shí),系統(tǒng)會(huì)對(duì)生成的靜態(tài)庫的所有目標(biāo)文件執(zhí)行預(yù)鏈接操作蚯舱,預(yù)鏈接操作會(huì)將所有的目標(biāo)文件組合成為一個(gè)單獨(dú)的大的目標(biāo)文件改化。這樣根據(jù)以文件為單位的鏈接規(guī)則就會(huì)將靜態(tài)庫中的所有代碼全部都鏈接進(jìn)可執(zhí)行程序中去,但是這樣帶來的問題就是最后在dead code stripping時(shí)刪除不掉已經(jīng)鏈接進(jìn)來的那些沒有被任何地方使用過的OC類了枉昏。 - 對(duì)于引入的一些第三方靜態(tài)庫或者第三方的開源庫來說因?yàn)槲覀儫o法去改變其實(shí)現(xiàn)邏輯陈肛。如果這個(gè)靜態(tài)庫中沒有任何分類代碼的定義則正常引用即可,如果靜態(tài)庫中有分類方法的定義則單獨(dú)對(duì)這個(gè)靜態(tài)庫采用-force_load選項(xiàng)兄裂。
總之一句話:為了讓你的程序瘦身燥爷,盡量將代碼放到靜態(tài)庫中,不要使用-Objc和-all_load選項(xiàng)
為了驗(yàn)證上述方法的有效性懦窘,筆者對(duì)項(xiàng)目中的應(yīng)用做了一個(gè)測(cè)試:分別是有帶-ObjC選項(xiàng)和沒有帶-ObjC選項(xiàng)的情況下的應(yīng)用程序包中可執(zhí)行程序的大小從115M減少到95M前翎,減少了20M的尺寸。
歡迎大家訪問歐陽大哥2013的github地址