隨着應用程序的功能愈來愈多,實現愈來愈複雜,第三方庫的引入,UI體驗的優化等衆多因素程序中的代碼量成倍的增加,從而致使應用程序包的體積愈來愈大。當程序體積變大後不只會出現編譯流程變慢,並且還會出現運行性能問題,會增長應用下載時長和消耗用戶的移動網絡流量等等。所以在這些衆多的問題下須要對應用進行瘦身處理。git
一個應用程序由衆多資源文件和可執行程序文件組成,資源文件的優化不在本文探討範圍。本文主要討論對可執行程序代碼瘦身的方法。github
對可執行程序代碼瘦身主要就是想辦法讓程序中不會被調用的源代碼不參與編譯或連接。咱們能夠經過一些源代碼分析工具來查找哪些函數或者類方法沒有被調用並從代碼中刪除掉來解決編譯連接前的瘦身問題。這些分析工具也不在本文的討論範圍內。應用程序在編譯時會對工程中的全部代碼都執行編譯處理並生成目標文件。而在連接階段則會根據程序代碼中對符號的引用關係來將全部相關的目標文件連接爲一個大的可執行程序文件,而且在連接階段連接器會優化掉全部沒被調用的C/C++函數代碼,可是對於OC類中的沒有調用的方法則不會被優化掉。因此爲了對可執行程序在編譯連接階段進行瘦身處理就須要瞭解源代碼的編譯連接規則。這也是本文所要介紹的針對工程經過靜態庫的形式進行編譯和連接的方式來減小可執行程序代碼的尺寸。您能夠從文章:《深刻iOS系統底層之靜態庫介紹》中詳細的瞭解到靜態庫的編譯連接過程,以及相關的技術細節。bash
爲了驗證和具體的實踐,我在github上創建了一個項目:YSAppSizeTest。您能夠從這個項目中看到如何對工程進行構建以實現程序的瘦身處理。網絡
在示例項目中同一個Workspace中分別創建ThinApp和FatApp兩個工程,這兩個工程實現的功能是同樣。在整個應用程序中分別定義了CA、CB、CC、CD、CE一共5個OC類,定義了一個UIView(Test)分類,還有定義了兩個C函數:libFoo1和libFoo1。函數
整個應用程序中只使用了CA和CC兩個OC類,以及調用了UIView(Test)分類方法,以及調用了libFoo1函數,而且同時都採用導入靜態庫的形式。由於這兩個工程對文件的定義和分佈策略不一樣使得兩個應用程序的最終可執行代碼的尺寸是不相同的。工具
上述兩個工程的程序被Archive出來後,FatApp可執行程序的尺寸是367KB,而ThinApp可執行程序的尺寸是334KB。經過一些工具好比Mach-O View或者 IDA能夠看出:FatApp中5個OC類的代碼以及libFoo1函數還有UIView(Test)分類的代碼都被連接進可執行程序中;而ThinApp中則只有CA,CC兩個類以及libFoo1函數還有UIView(Test)分類的代碼被連接進可執行程序中。在ThinApp中雖然沒有使用-Objc連接選項,可是靜態庫中的分類也被連接進可執行程序中。性能
根據對項目中的文件定義和引用策略以及相關的理論基礎咱們能夠按照以下的規則來構建您的應用程序:測試
儘可能將全部代碼都移植到靜態庫中,而主程序則保留爲一個殼程序。具體操做方法是創建一個Workspace,而後主程序工程就只有默認建立工程時的代碼,全部新加入的代碼都創建並存放到靜態庫工程中去,而後經過工程依賴來引入這些靜態庫工程,或者藉助一些工程化工具好比Cocoapods來實現這種拆分和引用處理。主程序工程中只保留AppDelegate的代碼,其餘代碼都一致到靜態庫中。而後在AppDelegate中的相關代碼處調用靜態庫中定義的業務代碼。優化
按業務組件對工程進行解耦每一個組件是一個靜態庫工程。靜態庫中的每個文件中最好只有一個類的實現,而且類的分類實現最好和類實現編寫在同一個文件中,相同功能的代碼以及可能都會被調用的代碼儘可能存放在一個文件中。ui
不要在主程序工程中使用-ObjC和-all_load兩個選項而改成用-force_load 來單獨指定要執行加載的靜態庫。-ObjC和-all_load選項會把主程序工程以及所依賴的全部靜態庫中的工程中的所有代碼都連接到可執行程序中而無論代碼是否有被調用過或者使用過。而force_load則只會將指定的靜態庫中的全部代碼連接到可執行程序中,固然force_load若是沒有必要也儘可能不要使用。
儘可能減小在靜態庫中定義OC類的分類方法,若是必定要定義分類方法則能夠將分類方法定義在和類定義相同的文件中,或者將分類方法定義在一個必定會被調用和引用的實現文件中。由於根據連接規則靜態庫中的分類是不會被連接進可執行程序中的,除非使用了上述的三個連接選項。若是將分類代碼單獨的定義在一個文件中的話則能夠經過在分類的頭文件中定義一個內聯函數,內聯函數調用分類實現文件中的一個dumy函數,這樣只要這個分類的頭文件被include或者import就會把整個分類的實現連接到可執行程序中去。通常狀況下咱們在靜態庫中創建分類那就代表必定會被某個文件引用這個分類,從而實現整個文件的連接處理。在分類中定義的這兩個函數則由於沒有被任何地方調用,所以會在連接優化中將這兩個函數給優化掉。這樣就使得即便咱們不用-ObjC選項也能將靜態庫中的分類連接到可執行程序中去。最後須要注意的是在每一個分類中定義的這兩個函數名最好可以惟一這樣就不會出現符號重名衝突的問題了。
//分類文件的頭文件UIView+XXX.h
@interface UIView (XXX)
//分類中定義的方法
@end
/*
經過在分類的頭文件中定義一個內聯函數,內聯函數調用分類實現文件中的一個dumy函數,這樣只要這個分類的頭文件被include或者import就會把
整個分類的實現連接到可執行程序中去。通常狀況下咱們在靜態庫中創建分類那就代表必定會被某個文件引用這個分類,從而實現整個文件的連接處理。
而在分類中定義的這兩個函數則由於沒有被任何地方調用,所以會在連接優化中將這兩個函數給優化掉。這樣就使得即便咱們不用-ObjC選項也能
將靜態庫中的分類連接到可執行程序中去。最後須要注意的是在每一個分類中定義的這兩個函數名最好可以惟一這樣就不會出現符號重名衝突的問題了。
*/
extern void _cat_UIView_XXX_Impl(void);
inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}
------------------------------------------------------------
//分類文件的實現文件UIView+XXX.m
#import "UIView+XXX.h"
@implementation UIView (XXX)
//分類的實現代碼
@end
void _cat_UIView_XXX_Impl(void){}
---------------------------------------------------------------
//最後把這個分類頭文件放入到某個對外暴露的頭文件中,好比本例中將分類代碼放入到了ThinAppLib.h文件中
//ThinAppLib.h
#import "UIView+XXX.h"
//其餘頭文件
複製代碼
Build Settings
中將Perform Single-Object Prelink 中的開關選項打開。當這個開關打開時,系統會對生成的靜態庫的全部目標文件執行預連接操做,預連接操做會將全部的目標文件組合成爲一個單獨的大的目標文件。這樣根據以文件爲單位的連接規則就會將靜態庫中的全部代碼所有都連接進可執行程序中去,可是這樣帶來的問題就是最後在dead code stripping時刪除不掉已經連接進來的那些沒有被任何地方使用過的OC類了。總之一句話:爲了讓你的程序瘦身,儘可能將代碼放到靜態庫中,不要使用-Objc和-all_load選項
爲了驗證上述方法的有效性,筆者對項目中的應用作了一個測試:分別是有帶-ObjC選項和沒有帶-ObjC選項的狀況下的應用程序包中可執行程序的大小從115M減小到95M,減小了20M的尺寸。
歡迎你們訪問歐陽大哥2013的github地址