背景:
日常的項目經(jīng)過長時間的迭代,優(yōu)化,重構(gòu)之后,可能會積累一些用不到了的類,長久下去,會影響我們的包大小。
定期的檢測,可以在一定程度上控制ipa的增大<話說不是砍需求才是減少代碼的最佳方式嘛!哈哈,如果產(chǎn)品同意!>
腳本使用方式
FindClassUnRefs.py & FindAllClassIvars.py 腳本地址
python FindClassUnRefs.py -p /Users/a58/Library/Developer/Xcode/DerivedData/XXX-bqqoxganvkvgwuefbskxsbvnxlnn/Build/Products/Debug-iphonesimulator/XXX.app -w JD,BD,AL
參數(shù)說明:
-p Xcode運行之后的,項目Product路徑
-w 結(jié)果白名單處理,檢測結(jié)果,只想要以什么開頭的類,多個用逗號隔開,比如JD,BD,AL
-b 結(jié)果黑名單處理,檢測結(jié)果,不想要以什么開頭的類,多個用逗號隔開,比如Pod,AF,SD
-w 和 -b 不能共存,共存會報錯
-p 對應(yīng)的路徑

運行結(jié)果
獲取項目中所有的類...
獲取項目中所有被引用的類...
獲取項目中所有使用load方法的類...
通過符號表中的符號,獲取類名...
獲取項目中所有類的屬性...
查找結(jié)果:
只作為其他類的成員變量,不確定有沒有真正被使用,請在項目中查看 --------
1 : WBQuotaReduceRateDto
2 : WBAdjustQuota
。。。。
7 : WBActionSheetConfiguration
8 : WBQuotaCardButton
未使用的類 --------
1 : AHKPageResponesRouteModel
2 : WBNavigationBar
3 : AHKPageRouteModel
4 : WBIndexRange
。。。。。
19 : WBStoreService
20 : WBWarningView
21 : AHKFileTool
未使用到的類查詢完畢,結(jié)果已保存在了find_class_unRefs.txt中,【請在項目中二次確認(rèn)無誤后再進行相關(guān)操作】
流程結(jié)果圖

最終干掉1,2,3,4,5,6這幾個集合,剩下藍色的底兒就是最終的結(jié)果
當(dāng)然了6會被作為存疑被打印出來。接下來讓我們看看具體流程吧
詳細工作流程:
流程圖

實現(xiàn)分析
通過使用otool工具對編譯產(chǎn)生的mach-o文件進行分析,產(chǎn)生結(jié)果。
第一步:
分別找到所有類和引用類的集合,然后取差集,初步得到未使用類集合
第二步:
因為一些原因,有些類被引用了沒有出現(xiàn)在引用類集合中,而變成未引用類,我們要做的就是找到這些特殊情況,然后排除掉,減少誤傷。
原理篇
Mach-O簡介

關(guān)于Mach-O更詳細的每一部分的介紹可以參考
https://www.cnblogs.com/dengzhuli/p/9952202.html
我們也是主要分析__DATA、__TEXT,然后通過Symbol解析,再分析
otool簡介
otool可以提取并顯示iOS下Mach-O的相關(guān)信息,包括頭部,加載命令,各個段,共享庫,動態(tài)庫等等。它擁有大量的命令選項,是一個功能強大的分析工具,當(dāng)然還可以做反匯編的工具使用。
比如:
otool -v -s __TEXT __cstring ClassUnRefDemo001

接下來按照上面流程圖的順序進行回顧
1、獲取項目所有的類符號集合
otool -v -s __DATA __objc_classlist mach-o Path
獲取完畢之后進行倒敘編碼
a8 45 00 00 01 00 00 00 ==> 00000001000045a8

2、獲取項目所有被引用的類符號集合
otool -v -s __DATA __objc_classrefs mach-o Path
獲取完畢之后進行倒敘編碼
60 47 00 00 01 00 00 00 ==> 0000000100004760

3、獲取項目所有調(diào)用load類符號集合
otool -v -s __DATA __objc_nlclslist mach-o Path
獲取完畢之后進行倒敘編碼
28 48 00 00 01 00 00 00 ==> 0000000100004828

4、取合集,調(diào)用load方法的類也認(rèn)為是有用的類
將上面的第2步結(jié)果和第3步結(jié)果求合集,變成所有引用類。
因為調(diào)用load方法的類不一定出現(xiàn)在所有引用類中,但是某個類實現(xiàn)了load方法我們認(rèn)為他一定會使用。
5、取差集,獲取未使用到的類符號集合
這一步為了獲取項目中所有沒有被使用的類
6、通過類的符號,找到類名
nm -nm mach-o Path
通過符號表,找到相關(guān)的類名,光看符號我們可不認(rèn)識這個是啥

7、查找當(dāng)前所有類的父類和子類
如果父類沒有被使用,子類被使用了,父類是不會出現(xiàn)在第1步的引用類集合的,所以這里要特殊處理。
如果父類在未使用類集合,子類不在未使用類集合,認(rèn)為父類有被用到。
從未使用類集合將父類刪掉
otool -oV 是獲取所有的類結(jié)構(gòu)及其定義的方法
otool -oV mach-o Path

8、檢測項目中的靜態(tài)字符串
如果某個靜態(tài)字符串在未使用集合,刪除當(dāng)前類<認(rèn)為使用了runtime的形式調(diào)用,事后可以再次確認(rèn)>
otool -v -s __TEXT __cstring 是獲取項目中所有的靜態(tài)字符串
otool -v -s __TEXT __cstring mach-o Path
為什么要加這層過濾呢,因為項目中的類可能是通過runtime的方式調(diào)用的,類名轉(zhuǎn)類,然后使用。這種類是不會出現(xiàn)第1步的。
比如下面的 CViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UIViewController *vc = [[NSClassFromString(@"CViewController") alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}

9、檢測當(dāng)前未使用類的分類是否使用了load方法
分類使用了load方法,認(rèn)為當(dāng)前類被引用
otool -oV mach-o Path
如果一個分類使用了load方法,那么認(rèn)為這個類也是被使用的,分類也不能自己裸奔吧。
通過正則找到所有的分類中有l(wèi)oad方法的類,進行過濾

10、黑白名單過濾
黑名單過濾:
我們在查找完畢之后,可能會有很多的類,有些類我們沒必要修改也修改不了,比如Pods中的類,或者MJRefresh中檢測出一個無用類,我們也不能刪除,作為工具類多余的類在以后不一定沒用。
我們可以選擇不看,比如,輸入?yún)?shù)
-b MJ,AF
python FindClassUnRefs.py -p /Users/a58/Library/XXX.app -b MJ,AF
那么我們的結(jié)果中會把MJ和AF開頭的干掉,方便查看
白名單:
我們只想看我們自己的類,一般項目都會有固定的開頭,方便管理
比如,我們的類名都是以WB開頭的,輸入?yún)?shù) -w WB,那么結(jié)果只會顯示WB開頭的未使用類
11、查看項目所有的屬性
如果未使用類是已使用類的屬性,那么在檢測結(jié)果刪除
otool -oV mach-o Path
如果一個類是另外一個類的屬性,除此之外別的地方?jīng)]有使用過。那么他不會出現(xiàn)在第1步。可能會被認(rèn)為是未使用類
這個時候可以通過正則找到所有類的屬性,如果當(dāng)前類沒有在未使用類集合,并且他的屬性中有在未使用類中,那么他應(yīng)該屬于已經(jīng)用類

12、檢測結(jié)果寫入日志文件
結(jié)果可能過多,終端顯示不開,將結(jié)果寫入文件,方便閱讀
13、根據(jù)檢測結(jié)果,在項目中二次確認(rèn)后處理
檢測結(jié)果是有偏差的,有些已使用的類仍然不太好檢測。
已知的情況:
1、類里面都是C語言的函數(shù),即使方法被調(diào)用了,也認(rèn)為是未使用類
2、storyBoard中的類,都是nib,檢測不到
3、作為數(shù)組之類的集合類型被引用,并且使用的時候沒有體現(xiàn)該類
@property (nonatomic, strong) NSArray<Person *> *perArray;
4、一些類沒有進行實例化,而是作為父類進行匹配
比如:
WBBaseHeaderFooterView
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
if (self.sectionsData.count == 0) {
return nil;
}
else {
WBBaseHeaderFooterView *view = [tableView dequeueReusableHeaderFooterViewWithIdentifier:self.footerSectionIdentifier];
WBBaseCellSectionModel *model = self.sectionsData[section];
if (model.footerModel) {
if (!view) {
view = [NSClassFromString(self.footerSectionIdentifier) new];
}
[view setFooterModel:model.footerModel];
return view;
}
else {
return nil;
}
}
}
5、A類引用B類,只是#import "B.h",沒有調(diào)用B的任何方法,會認(rèn)為B沒被用到,這個是正常的。
6、A類引用B類,并且在A類中的方法調(diào)用了B類的方法,會認(rèn)為B類被使用了,不用擔(dān)心,A類認(rèn)為未被使用。
7、A類引用B類,B類引用A類,并且相互調(diào)用了相互的方法,被認(rèn)為都被使用過,這個目前還沒啥好辦法。
如果在檢測過程中出現(xiàn)一些誤差,還請留言,然后對該腳本進行優(yōu)化,謝謝?。?/p>