iOS應用程式瘦身的靜態庫解決方案

歐陽大哥2013發表於2019-05-08

為什麼要給程式瘦身?

隨著應用程式的功能越來越多,實現越來越複雜,第三方庫的引入,UI體驗的優化等眾多因素程式中的程式碼量成倍的增長,從而導致應用程式包的體積越來越大。當程式體積變大後不僅會出現編譯流程變慢,而且還會出現執行效能問題,會增加應用下載時長和消耗使用者的行動網路流量等等。因此在這些眾多的問題下需要對應用進行瘦身處理。

一個應用程式由眾多資原始檔和可執行程式檔案組成,資原始檔的優化不在本文探討範圍。本文主要討論對可執行程式程式碼瘦身的方法。

對可執行程式程式碼瘦身主要就是想辦法讓程式中不會被呼叫的原始碼不參與編譯或連結。我們可以通過一些原始碼分析工具來查詢哪些函式或者類方法沒有被呼叫並從程式碼中刪除掉來解決編譯連結前的瘦身問題。這些分析工具也不在本文的討論範圍內。應用程式在編譯時會對工程中的所有程式碼都執行編譯處理並生成目標檔案。而在連結階段則會根據程式程式碼中對符號的引用關係來將所有相關的目標檔案連結為一個大的可執行程式檔案,並且在連結階段連結器會優化掉所有沒被呼叫的C/C++函式程式碼,但是對於OC類中的沒有呼叫的方法則不會被優化掉。所以為了對可執行程式在編譯連結階段進行瘦身處理就需要了解原始碼的編譯連結規則。這也是本文所要介紹的針對工程通過靜態庫的形式進行編譯和連結的方式來減少可執行程式程式碼的尺寸。您可以從文章:《深入iOS系統底層之靜態庫介紹》中詳細的瞭解到靜態庫的編譯連結過程,以及相關的技術細節。

一個瘦身的例子!

為了驗證和具體的實踐,我在github上建立了一個專案:YSAppSizeTest。您可以從這個專案中看到如何對工程進行構建以實現程式的瘦身處理。

在示例專案中同一個Workspace中分別建立ThinApp和FatApp兩個工程,這兩個工程實現的功能是一樣。在整個應用程式中分別定義了CA、CB、CC、CD、CE一共5個OC類,定義了一個UIView(Test)分類,還有定義了兩個C函式:libFoo1和libFoo1。

整個應用程式中只使用了CA和CC兩個OC類,以及呼叫了UIView(Test)分類方法,以及呼叫了libFoo1函式,並且同時都採用匯入靜態庫的形式。因為這兩個工程對檔案的定義和分佈策略不同使得兩個應用程式的最終可執行程式碼的尺寸是不相同的。

FatApp中的檔案定義和分佈策略

  1. FatApp工程依賴並匯入了FatAppLib靜態庫工程。
  2. CA,CB兩個類都定義在主程式工程中。
  3. CC,CD,CE三個類,以及UIView(Test)分類,還有libFoo1,libFoo2兩個函式都定義在FatAppLib靜態庫工程中。
  4. CC,CD兩個類定義在同一個檔案中,CE類則定義在單獨的檔案中。
  5. FatApp工程的Other Linker Flags中設定了 -ObjC選項。

ThinApp中的檔案定義和分佈策略

  1. ThinApp工程依賴並匯入了ThinAppLib靜態庫工程。
  2. 主程式工程就是一個殼工程。
  3. CA,CB,CC,CD,CE5個類,以及UIView(Test)分類,還有libFoo1,libFoo2兩個函式都定義在ThinAppLib靜態庫工程中。
  4. 上述的5個類都分別定義在不同的檔案中。
  5. ThinApp工程的Other Linker Flags中沒有設定-ObjC選項。

上述兩個工程的程式被Archive出來後,FatApp可執行程式的尺寸是367KB,而ThinApp可執行程式的尺寸是334KB。通過一些工具比如Mach-O View或者 IDA可以看出:FatApp中5個OC類的程式碼以及libFoo1函式還有UIView(Test)分類的程式碼都被連結進可執行程式中;而ThinApp中則只有CA,CC兩個類以及libFoo1函式還有UIView(Test)分類的程式碼被連結進可執行程式中。在ThinApp中雖然沒有使用-Objc連結選項,但是靜態庫中的分類也被連結進可執行程式中。

應用程式工程構建規則

根據對專案中的檔案定義和引用策略以及相關的理論基礎我們可以按照如下的規則來構建您的應用程式:

  1. 儘量將所有程式碼都移植到靜態庫中,而主程式則保留為一個殼程式。具體操作方法是建立一個Workspace,然後主程式工程就只有預設建立工程時的程式碼,所有新加入的程式碼都建立並存放到靜態庫工程中去,然後通過工程依賴來引入這些靜態庫工程,或者藉助一些工程化工具比如Cocoapods來實現這種拆分和引用處理。主程式工程中只保留AppDelegate的程式碼,其他程式碼都一致到靜態庫中。然後在AppDelegate中的相關程式碼處呼叫靜態庫中定義的業務程式碼。

  2. 按業務元件對工程進行解耦每個元件是一個靜態庫工程。靜態庫中的每一個檔案中最好只有一個類的實現,並且類的分類實現最好和類實現編寫在同一個檔案中,相同功能的程式碼以及可能都會被呼叫的程式碼儘量存放在一個檔案中。

  3. 不要在主程式工程中使用-ObjC和-all_load兩個選項而改為用-force_load 來單獨指定要執行載入的靜態庫。-ObjC和-all_load選項會把主程式工程以及所依賴的所有靜態庫中的工程中的全部程式碼都連結到可執行程式中而不管程式碼是否有被呼叫過或者使用過。而force_load則只會將指定的靜態庫中的所有程式碼連結到可執行程式中,當然force_load如果沒有必要也儘量不要使用。

  4. 儘量減少在靜態庫中定義OC類的分類方法,如果一定要定義分類方法則可以將分類方法定義在和類定義相同的檔案中,或者將分類方法定義在一個一定會被呼叫和引用的實現檔案中。因為根據連結規則靜態庫中的分類是不會被連結進可執行程式中的,除非使用了上述的三個連結選項。如果將分類程式碼單獨的定義在一個檔案中的話則可以通過在分類的標頭檔案中定義一個行內函數,行內函數呼叫分類實現檔案中的一個dumy函式,這樣只要這個分類的標頭檔案被include或者import就會把整個分類的實現連結到可執行程式中去。一般情況下我們在靜態庫中建立分類那就表明一定會被某個檔案引用這個分類,從而實現整個檔案的連結處理。在分類中定義的這兩個函式則因為沒有被任何地方呼叫,因此會在連結優化中將這兩個函式給優化掉。這樣就使得即使我們不用-ObjC選項也能將靜態庫中的分類連結到可執行程式中去。最後需要注意的是在每個分類中定義的這兩個函式名最好能夠唯一這樣就不會出現符號重名衝突的問題了。

//分類檔案的標頭檔案UIView+XXX.h
@interface UIView (XXX)

//分類中定義的方法

@end

/*
  通過在分類的標頭檔案中定義一個行內函數,行內函數呼叫分類實現檔案中的一個dumy函式,這樣只要這個分類的標頭檔案被include或者import就會把
  整個分類的實現連結到可執行程式中去。一般情況下我們在靜態庫中建立分類那就表明一定會被某個檔案引用這個分類,從而實現整個檔案的連結處理。
  而在分類中定義的這兩個函式則因為沒有被任何地方呼叫,因此會在連結優化中將這兩個函式給優化掉。這樣就使得即使我們不用-ObjC選項也能
  將靜態庫中的分類連結到可執行程式中去。最後需要注意的是在每個分類中定義的這兩個函式名最好能夠唯一這樣就不會出現符號重名衝突的問題了。
 */
extern void _cat_UIView_XXX_Impl(void);
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"
//其他標頭檔案

複製程式碼
  1. 除了可以通過-force_load來載入指定靜態庫中的所有程式碼外。我們還可以在構建靜態庫時,在靜態庫的工程的Build Settings中將Perform Single-Object Prelink 中的開關選項開啟。當這個開關開啟時,系統會對生成的靜態庫的所有目標檔案執行預連結操作,預連結操作會將所有的目標檔案組合成為一個單獨的大的目標檔案。這樣根據以檔案為單位的連結規則就會將靜態庫中的所有程式碼全部都連結進可執行程式中去,但是這樣帶來的問題就是最後在dead code stripping時刪除不掉已經連結進來的那些沒有被任何地方使用過的OC類了。
  2. 對於引入的一些第三方靜態庫或者第三方的開源庫來說因為我們無法去改變其實現邏輯。如果這個靜態庫中沒有任何分類程式碼的定義則正常引用即可,如果靜態庫中有分類方法的定義則單獨對這個靜態庫採用-force_load選項。

總之一句話:為了讓你的程式瘦身,儘量將程式碼放到靜態庫中,不要使用-Objc和-all_load選項

為了驗證上述方法的有效性,筆者對專案中的應用做了一個測試:分別是有帶-ObjC選項和沒有帶-ObjC選項的情況下的應用程式包中可執行程式的大小從115M減少到95M,減少了20M的尺寸。


歡迎大家訪問歐陽大哥2013的github地址

相關文章