1. 程式人生 > >CLANG技術分享系列四:IOS APP無用程式碼/重複程式碼分析

CLANG技術分享系列四:IOS APP無用程式碼/重複程式碼分析


01 NOV 2016 . CATEGORY: TECH COMMENTS 
#CLANG

問題背景

包瘦身,包瘦身,包瘦身,重要的事情說三遍。
最近公司一款APP一直在瘦身,我們團隊的APP也愈發龐大了。就想看看除過資源外,還有那些路徑可以縮小包大小,直觀來看,業務程式碼畢竟有限,各種庫嫌疑很大,但是如果沒有一個定量的分析,一切說辭都顯得有些蒼白。
當然了所有的APP套路都一樣,開始執行就跑一個迴圈,不斷地從訊息佇列裡去獲取訊息,獲取到使用者操作,系統通知等訊息的時候就處理此訊息,獲取不到就休息。一直迴圈往復到被殺死,說到底所有的有用API都應該有一條最終被main函式呼叫的路徑,否則就認為是無用的程式碼。

繼續閱讀之前,請先閱讀我的另一篇博文Clang技術分享系列三:API有效性檢查

Clang技術分享系列三:API有效性檢查

分析無用程式碼資料結構

除過系列三提到的clsInterfHierachyclsMethodprotoInterfHierachyprotoInterfCall外還用到的資料結構有:
1.clsMethod新增通知
以第一條記錄為例,其意思是說-[AppDelegate onViewControllerDidLoadNotification:]作為通知kNotificationViewControllerDidLoadSelector
,在-[AppDelegate application:didFinishLaunchingWithOptions:]中被新增。

clang-validate-ios-api-clsMethodAddNotifs

2.clsMethod傳送通知
以第二條記錄為例,其代表了-[ViewController viewDidLoad]傳送了kNotificationViewControllerDidLoad

clang-validate-ios-api-notifPostedCallers

這樣對於通知,如果-[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結束).

最終分析

如API有效性檢查中提到的現有機制:書寫Plugin->書寫分析檔案->使用Plugin去編譯工程並生成中間檔案->Build結束的時候,使用shell呼叫分析工具分析。分析工具現在著重兩個方面重複程式碼和無用程式碼:
1.重複程式碼的比對
如API有效性檢查中介面方法呼叫中提到的clsMethod的資料結構,可以通過clang-format掉所有clsMethod的原始碼,然後hash求值,然後hash值一樣的clsMethod將具有相同的原始碼。
本文使用的例子產生的結果如下所示:

clang-validate-ios-api-repeatCode​ 2.無用程式碼的分析

分析的物件在於clsMethod.json裡面所有的key,即實際擁有原始碼的-/+[cls method]呼叫。
a.初始化預設的呼叫關係usedClsMethodJson:-[AppDelegate alloc],"-[UIApplication main]","-[UIApplication main]","-[UIApplication main]","+[NSObject alloc]","-[UIApplication main]",其中AppDelegate由使用者傳給Analyzer.
b.分析-/+[cls method]是否存在一條路可以被已經呼叫usedClsMethodJson中的key呼叫。
對於某一個clsMethod,其需要檢查的路徑包括三個,繼承體系,Protocol體系和Notification體系。
針對Notification體系,前文已經有過分析。
針對類繼承體系,從當前類一直向上追溯(直到發現有被呼叫或者NSObject),每一個基類對應的-/+[cls method]是否被隱含的呼叫關係所呼叫,如-[ViewController viewDidLoad]被-[ViewController alloc]隱含呼叫,當-[ViewController alloc]已經被呼叫的時候,-[ViewController viewDidLoad]也將被認為呼叫。這裡需要注意需要寫一個隱含呼叫關係表以供查詢,如下所示:

clang-find-duplicate-unused-code-implicitCallStackJson

針對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有已經存在於usedClsMethodJsonCaller.

clang-find-duplicate-unused-code-protointerfcall​ 一個簡單的分析結果如下圖: 使用到的ClsMethod

clang-find-duplicate-unused-code-usedclsmethod

未使用到的ClsMethod

clang-find-duplicate-unused-code-unusedclsmethod

檢出示例工程

鑑於示例工程規模較小,另選取開源的zulip-ios工程,結合本文所述方法去除未被最終呼叫的程式碼(包括業務程式碼,第三方庫),效果如下:clang-find-duplicate-unused-code-zulip-original-binaryclang-find-duplicate-unused-code-zulip-trimmed-binary

其中原始工程Archieve生成的可執行檔案大小為3.4MB,去除最終未被呼叫的程式碼後,可執行檔案變為3MB。對於這樣一個設計良好的工程,純程式碼的瘦身效果還是比較可觀的。

針對不同工程的定製

雖然此專案已經給了一個完整的重複程式碼和無用程式碼分析工具,但也有其侷限性(主要是動態特性)。具體分析如下:
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.其他隱含的邏輯或者動態特性導致的呼叫關係遺漏。 ## 其他問題

正如API有效性檢查一文提到的,分析工具要求程式碼書寫要規範。並且對於很多隻有執行時才能知道型別的問題無能為力。
對於包大小而言,主要可以參考以下的思路去瘦身程式碼:
1.重複程式碼的提取重構
2.無用程式碼的移除
3.使用率較低的第三方庫的處理(本例不僅可以查詢到重複,無用的程式碼,進一步分析clsMethod.json,unusedClsMethod.json更可以獲取到每一個framework裡面有多少個方法,多少個又被-[UIApplication main]呼叫到了),面對使用率很低的庫,需要考慮是不是要全部引入或者重寫。
4.重複引用的第三方庫的處理(曾經發現團隊專案的工程裡面引用了其他團隊的庫,但由於多個庫裡面均有一份自己的Zip的實現,面對這種情況,可以考慮將此種需求全部抽象出來一個公共的Framework去處理,其他人都引用此專案,或者乾脆使用系統本身自帶的libz去處理會更好些)。