[轉]基於clang外掛程式的一種iOS包大小瘦身方案

來源:互聯網
上載者:User

標籤:遍曆   style   lan   message   erro   個人化定製   options   ack   支援   

轉自:http://mp.weixin.qq.com/s?__biz=MzA3ODg4MDk0Ng==&mid=2651112856&idx=1&sn=b2c74c62a10b4c9a4e7538d1ad7eb739

iOS包瘦身,對於一般團隊來說並不是最佳化的首要目標,但是對於一些安裝包已經超限的團隊來說非常關鍵。和阿里Mobile Security都分享過相關的內容,後者採用的是去除無用代碼的思路,感興趣的同學可以閱讀:

  • iOS瘦身之刪除無用的mach-O檔案

而本文則將這個思路發揮到了極致,歡迎閱讀:

 

引子

包瘦身,包瘦身,包瘦身,重要的事情說三遍。

最近公司一款iOS APP(本文只討論使用Objective C開發的iOS安裝包)一直在瘦身,我們團隊的APP也愈發龐大了。而要解決這個問題,思路主要集中在兩個方向,資源和代碼。資源主要在於圖片,方法包括移除未被引用的圖片,只使用一套圖片(2x或3x),圖片伸縮等;代碼層面主要思路包括重構消除冗餘,linkmap中selector引用分析等。除此之外,有沒有別的路徑呢?

眾所周知,代碼之間存在調用關係。假設iOS APP的主入口為-[UIApplication main],則所有開發人員的原始碼(包括第三方庫)可分為兩類:存在一條調用路徑,使得代碼可以被主入口最終調用(稱此類代碼為被最終調用);不存在一條調用路徑,使得代碼最終不能被主入口調用(稱此類代碼為未被最終調用)。

假設有一個原始碼層級的分析工具(或編譯器),可以輔助分析代碼間的調用關係,這樣就使得分析最終被調用代碼成為可能,剩下的就是未被最終調用的代碼。

這種工具目前有成熟可用的嗎?答案是肯定的,就是clang外掛程式。除可用於分析未被最終調用代碼外,clang還可輔助發現重複代碼。

LLVM與clang外掛程式

LLVM工程包含了一組模組化,可複用的編輯器和工具鏈。同其名字原意(Low Level Virtual Machine)不同的是,LLVM不是一個首字母縮寫,而是工程的名字。目前LLVM包含的主要子項目包括:

  1. LLVM Core:包含一個現在的原始碼/目標裝置無關的最佳化器,一集一個針對很多主流(甚至於一些非主流)的CPU的彙編代碼產生支援。

  2. Clang:一個C/C++/Objective-C編譯器,致力於提供令人驚訝的快速編譯,極其有用的錯誤和警告資訊,提供一個可用於構建很棒的原始碼層級的工具.

  3. dragonegg: gcc外掛程式,可將GCC的最佳化和代碼產生器替換為LLVM的相應工具。

  4. LLDB:基於LLVM提供的庫和Clang構建的優秀的本地調試器。

  5. libc++、libc++ ABI: 符合標準的,高效能的C++標準庫實現,以及對C++11的完整支援。

  6. compiler-rt:針對__fixunsdfdi和其他目標機器上沒有一個核心IR(intermediate representation)對應的短原生指令序列時,提供高度調優過的底層代碼產生支援。

  7. OpenMP: Clang中對多平台並行編程的runtime支援。

  8. vmkit:基於LLVM的Java和.NET虛擬機器實

  9. polly: 支援進階別的迴圈和資料本地化最佳化支援的LLVM架構。

  10. libclc: OpenCL標準庫的實現

  11. klee: 基於LLVM編譯基礎設施的符號化虛擬機器

  12. SAFECode:記憶體安全的C/C++編譯器

  13. lld: clang/llvm內建的連結器

作為LLVM提供的編譯器前端,clang可將使用者的原始碼(C/C++/Objective-C)編譯成語言/目標裝置無關的IR(Intermediate Representation)實現。其可提供良好的外掛程式支援,容許使用者在編譯時間,運行額外的自訂動作。

我們的目標是使用clang外掛程式減少包大小。其原理是,針對目標工程,基於clang的外掛程式特性,開發人員可以編寫外掛程式以分析所有原始碼。編譯過程中,將外掛程式作為clang的參數載入並產生各種中間檔案。編譯完成後,還需編寫一個工具去分析所有包含源碼的方法(包括使用者編寫,以及引入的第三方庫原始碼),檢查這些方法中哪些最終可被程式主入口調用,剩餘即是疑似無用代碼。簡單的一個複查,移除那些確定無用的代碼,重新編譯,便可以有效去除無用的代碼從而減少包大小。

本文相關內容如下:

  1. 如何編寫一個clang外掛程式並整合到Xcode

  2. 如何?代碼層級的包瘦身

  3. 局限與個人化定製

  4. 其他

如何編寫一個clang外掛程式並整合到Xcode

Clone clang源碼並編譯安裝

cd /opt

sudo mkdir llvm

sudo chown `whoami` llvm

cd llvm

export LLVM_HOME=`pwd`

 

git clone -b release_39 [email protected]:llvm-mirror/llvm.git llvm

git clone -b release_39 [email protected]:llvm-mirror/clang.git llvm/tools/clang

git clone -b release_39 [email protected]:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra

git clone -b release_39 [email protected]:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

 

mkdir llvm_build

cd llvm_build

cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release

make -j`sysctl -n hw.logicalcpu`

編寫clang外掛程式

要實現自訂的clang外掛程式(以C++ API為例),應按照以下步驟:

  1. 自訂繼承自 
    clang::PluginASTAction(基於consumer的抽象文法樹(Abstract Syntax Tree/AST)前端Action抽象基類) 
    clang::ASTConsumer(用於客戶讀取抽象文法樹的抽象基類), 
    clang::RecursiveASTVisitor(前序或後續地深度優先搜尋整個抽象文法樹,並訪問每一個節點的基類)等基類。

  2. 根據自身需要重載 
    PluginASTAction::CreateASTConsumer 
    PluginASTAction::ParseArgs 
    ASTConsumer::HandleTranslationUnit 
    RecursiveASTVisitor::VisitDecl 
    RecursiveASTVisitor::VisitStmt 
    等方法,實現自訂的分析邏輯。

  3. 註冊外掛程式 
    static FrontendPluginRegistry::Add<MyPlugin> X("my-plugin-       name", "my-plugin-description");

更多clang外掛程式:http://clang.llvm.org/docs/ExternalClangExamples.html

編譯產生外掛程式(dylib)

假定你的clang外掛程式源檔案為your-clang-plugin-source.cpp,需產生的外掛程式名為your-clang-plugin-name.dylib,可以使用如下命令(載入了llvm,clang的include路徑,產生的相關lib等)產生:

clang -std=c++11 -stdlib=libc++ -L/opt/local/lib -L/opt/llvm/llvm_build/lib -I/opt/llvm/llvm_build/tools/clang/include -I/opt/llvm/llvm_build/include -I/opt/llvm/llvm/tools/clang/include -I/opt/llvm/llvm/include -dynamiclib -Wl,-headerpad_max_install_names -lclang -lclangFrontend -lclangAST -lclangAnalysis -lclangBasic -lclangCodeGen -lclangDriver -lclangFrontendTool -lclangLex -lclangParse -lclangSema -lclangEdit -lclangSerialization -lclangStaticAnalyzerCheckers -lclangStaticAnalyzerCore -lclangStaticAnalyzerFrontend -lLLVMX86CodeGen -lLLVMX86AsmParser -lLLVMX86Disassembler -lLLVMExecutionEngine -lLLVMAsmPrinter -lLLVMSelectionDAG -lLLVMX86AsmPrinter -lLLVMX86Info -lLLVMMCParser -lLLVMCodeGen -lLLVMX86Utils -lLLVMScalarOpts -lLLVMInstCombine -lLLVMTransformUtils -lLLVMAnalysis -lLLVMTarget -lLLVMCore -lLLVMMC -lLLVMSupport -lLLVMBitReader -lLLVMOption -lLLVMProfileData -lpthread -lcurses -lz -lstdc++ -fPIC -fno-common -Woverloaded-virtual -Wcast-qual -fno-strict-aliasing -pedantic -Wno-long-long -Wall -Wno-unused-parameter -Wwrite-strings -fno-rtti -fPIC your-clang-plugin-source.cpp -o your-clang-plugin-name.dylib

與Xcode整合

下載XcodeHacking.zip:https://raw.githubusercontent.com/kangwang1988/kangwang1988.github.io/master/others/XcodeHacking.zip

使用命令列編譯時間,可以用如下方式載入外掛程式:

clang++ *** -Xclang -load -Xclang path-of-your-plugin.dylib -Xclang -add-plugin -Xclang your-pluginName -Xclang -plugin-arg-your-pluginName -Xclang your-pluginName-param

要在Xcode中使用clang外掛程式,需要如下hack Xcode.

sudo mv HackedClang.xcplugin xcode-select -print-path/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins

 

sudo mv HackedBuildSystem.xcspec xcode-select -print-path/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

在Xcode->Target-Build Settings->Build Options->Compiler for C/C++/Objective-C選擇Clang LLVM Trunk即可使得Xcode使用上文產生的的clang來編譯。至於其他命令列參數均可通過Xcode中的編譯選項設定完成。

如何?代碼層級的包瘦身

本文所說的代碼指的是OC中的形如-/+[Class method:\*]這種形式的代碼,調用關係典型如下:

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad {

 [super viewDidLoad];

 [self.view setBackgroundColor:[UIColor redColor]];

}

@end

則稱:-[ViewController viewDidLoad]調用了: 
-[UIViewController viewDidLoad] 
-[ViewController view](文法糖) 
+[UIColor redColor] 
-[UIView setBackgroundColor:]

這種調用關係可在clang遍曆抽象文法樹的時候得到。由於編譯器訪問抽象文法樹時存在嵌套關係,如上例:編譯器在訪問類實現ViewController的時候,嵌套了訪問-[ViewController viewDidLoad]的方法實現,而在訪問-[ViewController viewDidLoad]的方法實現的時候,嵌套了訪問訊息發送-[UIViewController viewDidLoad](對應源碼[super viewDidLoad]),-[ViewController view](對應源碼self.view),+[UIColor redColor](對應源碼[UIColor redColor]),-[UIView setBackgroundColor:](對應源碼[self.view setBackgroundColor:[UIColor redColor]])等,這樣通過記錄相關資訊即可瞭解我們關注的方法間調用關係。

資料結構

為了分析調用關係,用到的中間資料結構如下:

類介面與繼承體系(clsInterfHierachy)

此資料結構記錄了所有位於抽象文法樹上的介面內容,最終的解析結果如所示:

以AppDelegate為例,interfs代表其提供的介面(注:它的property window對應的getter和setter也被認為是interf一部分);isInSrcDir代表此類是否位於使用者目錄(將workspace的根目錄作為參數傳給clang)下,protos代表其遵守的協議,superClass代表介面的父類。

這些資訊擷取入口位於VisitDecl(Decl \*decl)的重載函數裡,相關的decl有:

  • ObjCInterfaceDecl(介面聲明)

  • ObjCCategoryDecl(分類聲明)

  • ObjCPropertyDecl(屬性聲明)

  • ObjCMethodDecl(方法聲明)

介面方法調用(clsMethod)

此資料結構記錄了所有包含原始碼的OC方法,最終解析結果如下所示:

-[AppDelegate application:didFinishLaunchingWithOptions:]為例,callee代表其調用到的介面(此處為可以明確類型的,對於形如id\<XXXDelegate\>後文介紹),filename為此方法所在的檔案名稱,range為方法所在的範圍,sourceCode為方法的具體實現原始碼。

這些資訊擷取入口位於VisitDecl(Decl \*decl)VisitStmt(Stmt \*stmt)的重載函數裡,相關的decl有ObjCMethodDecl(方法聲明),stmt有ObjCMessageExpr(訊息運算式)

此處除過正常的-/+[Class method:\*]外,還有其他較多的需要考慮的情形,已知且支援的分析包括:

  • NSObject協議的performSelector方法簇 
    [obj performSelector:@selector(XXX)]不僅包含[obj performSelector:]也包含[obj XXX].(下同)

  • 手勢/按鈕的事件處理selector 
    addTarget:action:/initWithTarget:action:/addTarget:action:forControlEvents:

  • NSNotificationCener添加通知處理Selector 
    addObserver:selector:name:object:

  • UIBarButtonItem添加事件處理Selector 
    initWithImage:style:target:action:/initWithImage:landscapeImagePhone:style:target:action:/initWithTitle:style:target:action:/initWithBarButtonSystemItem:target:action:

  • Timer 
    scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:/timerWithTimeInterval:target:selector:userInfo:repeats:/initWithFireDate:interval:target:selector:userInfo:repeats:

  • NSThread 
    detachNewThreadSelector:toTarget:withObject:/initWithTarget:selector:object:

  • CADisplayLink 
    displayLinkWithTarget:selector:

  • KVO機制 
    addObserver:forKeyPath:options:context:,不同於別的都要處理方法本身調用和對應target:selector調用,這裡KVO的addObserver則暗含了observeValueForKeyPath:ofObject:change:context:

  • IBAction機制 
    如基於xib/Storyboard的ViewController中-(IBAction)onBtnPressed:(id)sender方法,認為暗含了+[ViewController的 alloc]對於+[ViewController的 onBtnPressed:]的調用關係。

  • [XXX new] 
    包含+[XXX alloc]-[XXX init]

協議的介面與繼承體系(protoInterfHierachy)

此資料結構記錄了所有位於抽象文法樹上的協議內容,最終的解析結果如所示:

其中各欄位定義同clsInterfHierachy. 
這些資訊擷取入口位於VisitDecl(Decl \*decl)的重載函數裡,相關的decl有:

  • ObjCProtocolDecl(協議聲明)

  • ObjCPropertyDecl(屬性聲明)

  • ObjCMethodDecl(方法聲明)

協議方法的調用(protoInterfCall)

此資料結構記錄了所有如:-[ViewController func1]調用了-[id\<ViewControllerDelegate\> viewController:execFunc:]的形式,最終結果如下所示:

這些資訊擷取入口位於VisitStmt(Stmt \*stmt)的重載函數裡,相關的stmt是ObjCMessageExpr.

添加通知

以第一條記錄為例,其意思是說-[AppDelegate onViewControllerDidLoadNotification:]作為通知kNotificationViewControllerDidLoad的Selector,在-[AppDelegate application:didFinishLaunchingWithOptions:]中被添加。

發送通知

第一條記錄中,作為系統層級的通知,將被認為被APP主入口調用。 
第二條記錄則說明了,-[ViewController viewDidLoad]發送了kNotificationViewControllerDidLoad。

如果-[AppDelegate application:didFinishLaunchingWithOptions:]-[UIApplication main](假定的主入口)調用,且-[ViewController viewDidLoad]被調用,則-[AppDelegate onViewControllerDidLoadNotification:]被調用。其中,如果通知是系統通知,則只需要-[AppDelegate application:didFinishLaunchingWithOptions:]被調用即可。

這些資訊擷取入口位於VisitStmt(Stmt \*stmt)的重載函數裡,相關的stmt有ObjCMessageExpr.為了簡單處理,此處只處理形如addObserver:self這種(也是最常見的情況),否則Argu作為Expr\*分析起來會很複雜。PS.系統通知和本地通知的區別使用了名稱上的匹配(系統通知常以NS,UI,AV開頭以Notification結束).

重複程式碼分析

此處的重複代碼針對的是某兩個(或兩個以上)-/+[Class method:\*]的實現是一模一樣的。參考上文提到的clsMethod中的sourceCode,可以獲得每一個方法實現的原始碼。同時為了消除諸如格式上的差異(如多了一個空格,少了一個空格之類)引起的差異,先基於clang提供的format功能,按照某種風格(google/llvm等)將所有方法實現源碼格式化,再進行分析即可。

使用LLVM風格將代碼format:

find $prjDir -type f -name "\*.m" | xargs /opt/llvm/llvm_build/bin/clang-format -i -style=LLVM

本文樣本工程得到的一個重複代碼結果如下所示:

未被最終調用程式碼分析

分析的對象在於clsMethod.json裡面所有的key,即實際擁有原始碼的所有方法。

  1. 初始化預設的調用關係usedClsMethodJson:{-[AppDelegate alloc],"-[UIApplication main]","-[UIApplication main]","-[UIApplication main]","+[NSObject alloc]","-[UIApplication main]"},其中AppDelegate由使用者傳給Analyzer.

  2. 分析所有含源碼方法是否存在一條路可以被已經調用usedClsMethodJson中的key調用。

對於某一個clsMethod,其需要檢查的路徑包括三個,類繼承體系,協議體系和通知體系。

針對類繼承體系,從當前類一直向上追溯(直到發現有被調用或者NSObject),每一個基類對應的-/+[Class method:*]是否被隱含的調用關係所調用,如-[ViewController viewDidLoad]-[ViewController alloc]隱含調用,當-[ViewController alloc]已經被調用的時候,-[ViewController viewDidLoad]也將被認為調用。這裡需要注意需要寫一個隱含調用關係表以供查詢,如下所示:

針對Protocol體系,需要參考類似Protocol引用體系向上追溯(直到發現有被調用或者NSObject協議),針對某一個特定的Protocol判斷的時候,需要區分兩種,一種是系統級的Protocol,如UIApplicationDelegate,對於-[AppDelegate application:didFinishLaunchingWithOptions:]這種,參考AppDelegate<UIApplicationDelegate>,如果-[AppDelegate alloc]被調用則認為-[AppDelegate application:didFinishLaunchingWithOptions:]被調用。針對使用者定義的Protocol,如ViewControllerDelegate,對於-[AppDelegate viewController:execFunc:]不僅需要-[AppDelegate alloc]被調用並且protoInterfCall.json中-[ViewControllerDelegate viewController:execFunc:]對應的Callers有已經存在於usedClsMethodJson的Caller.

針對通知體系,前文已經有過分析。

本例分析使用到的ClsMethod結果如下:

本例分析未被使用到的ClsMethod結果如下:

查看樣本工程:https://github.com/kangwang1988/XcodeZombieCode.git

zulip-ios的應用效果對比

鑒於樣本工程規模較小,另選取開源的zulip-ios工程,其中原始工程Archive產生的可執行檔大小為3.4MB,結合本文所述方法去除未被最終調用的代碼(包括業務代碼,第三方庫)後,可執行檔變為3MB。對於這樣一個設計良好的工程,純程式碼的瘦身效果還是比較可觀的。

zulip-ios項目:https://github.com/zulip/zulip-ios

局限與個人化定製

這種靜態分析適合可以判斷出訊息接收者類型的情況,面對運行時類型和靜態分析類型不一致,或者靜態分析不出來類型時,不可用。這種分析要求代碼書寫規範。例如一個Class實現了某個Protocol,一定要在聲明裡說明,或者Property中delegate是id<XXXDelegate>的時候也要註明。

雖然此項目已經給了一個完整的重複代碼和無用程式碼分析工具,但也有其局限性(主要是動態特性)。具體分析如下:

  1. openUrl機制 
    假設工程設定裡使用了openUrl:"XXX://XXViewController"來開啟一個VC,則Clang外掛程式裡面需要分析openUrl的參數,如果參數是XXViewController,則暗含了+[XXViewController alloc]-[XXViewController init].

  2. Model轉化 
    如如果MTLModel使用到了modelOfClass:[XXXModel class] fromJSONDictionary:error:,則暗含了+[XXXModel alloc]+[XXXModel init].

  3. Message swizzle 
    假設使用者swizzle了-[UIViewController viewDidLoad]-[UIViewController XXviewDidLoad],則需要在implicitCallStackJson中添加-[UIViewController XXviewDidLoad],-[UIViewController viewDidLoad].

  4. 第三方Framework暗含的邏輯 
    如高德地圖的AnnotationView,需要implicitCallStackJson中添加"-[MAAnnotationView prepareForReuse:]","+[MAAnnotationView alloc]"等。包括第三方Framework裡面的一些Protocol,可能也需要參考前文提到的UIApplicationDelegate按照系統層級的Protocol來處理。

  5. 一些遺漏的重載方法 
    -[XXDerivedManager sharedInstance]並無實現,而XXDerivedManager的基類XXBaseManager的sharedInstance調用了-[self alloc],但因為self靜態分析時被認定為XXBaseManager,這就導致-[XXDerivedManager sharedManager]雖然被usedclsmethod.json調用,但是-[XXDerivedManager alloc]卻不能被調用。這種情況,可以在usedClsMethodJson初始化的時候,加入 "+[XXDerivedManager alloc]","-[UIApplication main]"

  6. 類似Cell Class 
    我們常會使用動態方法去使用[[[XXX cellClassWithCellModel:] alloc] initWithStyle:reuseIdentifier:]去構造Cell,這種情況下,應該針對cellClassWithCellModel裡面會包含的各種return [XXXCell class],在implicitCallStackJson中添加[[XXXCell alloc] initWithStyle:reuseIdentifier:],-[XXX cellClassWithCellModel:]這種調用。

  7. Xib/Storyboard會暗含一些UI元素(Controller,Table,Button,Cell,View等)的alloc方法或調用關係。

  8. 其他隱含的邏輯或者動態特性導致的調用關係遺漏。

其他

對於包大小而言,可以參考以下的思路去瘦身代碼:

  1. 重複代碼的提取重構

  2. 無用代碼的移除

  3. 使用率較低的第三方庫的處理(本文不僅可以尋找到重複,無用的代碼,進一步分析clsMethod.json/unusedClsMethod.json更可以擷取到每一個framework裡面有多少個方法,各方法有多少代碼,多少個方法又被-[UIApplication main]調用到了),面對使用率很低的庫,需要考慮是不是要全部引入或者重寫。

  4. 重複引用的第三方庫的處理(曾經發現Team 專案的工程裡面引用了其他團隊的庫,但由於多個庫裡面均有一份自己的Zip的實現,面對這種情況,可以考慮將此種需求全部抽象出來一個公用的Framework去處理,其他人都引用此項目,或者乾脆使用系統本身內建的libz去處理會更好些)。

因為可在源碼層級分析,使用clang外掛程式可做的工作很多。筆者還使用了clang外掛程式去實現了代碼風格檢查,API有效性驗證,相關樣本項目如下:

代碼風格檢查:https://github.com/kangwang1988/XcodeCodingStyle.git

API有效性驗證:https://github.com/kangwang1988/XcodeValidAPI.git

[轉]基於clang外掛程式的一種iOS包大小瘦身方案

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.