背景:
日常的项目经过长时间的迭代,优化,重构之后,可能会积累一些用不到了的类,长久下去,会影响我们的包大小。
定期的检测,可以在一定程度上控制ipa的增大<话说不是砍需求才是减少代码的最佳方式嘛!哈哈,如果产品同意!>
脚本使用方式
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
参数说明:
-p Xcode运行之后的,项目Product路径
-w 结果白名单处理,检测结果,只想要以什么开头的类,多个用逗号隔开,比如JD,BD,AL
-b 结果黑名单处理,检测结果,不想要以什么开头的类,多个用逗号隔开,比如Pod,AF,SD
-w 和 -b 不能共存,共存会报错
-p 对应的路径
运行结果
获取项目中所有的类...
获取项目中所有被引用的类...
获取项目中所有使用load方法的类...
通过符号表中的符号,获取类名...
获取项目中所有类的属性...
查找结果:
只作为其他类的成员变量,不确定有没有真正被使用,请在项目中查看 --------
1 : WBQuotaReduceRateDto
2 : WBAdjustQuota
。。。。
7 : WBActionSheetConfiguration
8 : WBQuotaCardButton
未使用的类 --------
1 : AHKPageResponesRouteModel
2 : WBNavigationBar
3 : AHKPageRouteModel
4 : WBIndexRange
。。。。。
19 : WBStoreService
20 : WBWarningView
21 : AHKFileTool
未使用到的类查询完毕,结果已保存在了find_class_unRefs.txt中,【请在项目中二次确认无误后再进行相关操作】
流程结果图
最终干掉1,2,3,4,5,6这几个集合,剩下蓝色的底儿就是最终的结果
当然了6会被作为存疑被打印出来。接下来让我们看看具体流程吧
详细工作流程:
流程图
实现分析
通过使用otool工具对编译产生的mach-o文件进行分析,产生结果。
第一步:
分别找到所有类和引用类的集合,然后取差集,初步得到未使用类集合
第二步:
因为一些原因,有些类被引用了没有出现在引用类集合中,而变成未引用类,我们要做的就是找到这些特殊情况,然后排除掉,减少误伤。
原理篇
Mach-O简介
关于Mach-O更详细的每一部分的介绍可以参考
https://www.cnblogs.com/dengzhuli/p/9952202.html
我们也是主要分析__DATA、__TEXT,然后通过Symbol解析,再分析
otool简介
otool可以提取并显示iOS下Mach-O的相关信息,包括头部,加载命令,各个段,共享库,动态库等等。它拥有大量的命令选项,是一个功能强大的分析工具,当然还可以做反汇编的工具使用。
比如:
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、获取项目所有调用load类符号集合
otool -v -s __DATA __objc_nlclslist mach-o Path
获取完毕之后进行倒叙编码
28 48 00 00 01 00 00 00 ==> 0000000100004828
4、取合集,调用load方法的类也认为是有用的类
将上面的第2步结果和第3步结果求合集,变成所有引用类。
因为调用load方法的类不一定出现在所有引用类中,但是某个类实现了load方法我们认为他一定会使用。
5、取差集,获取未使用到的类符号集合
这一步为了获取项目中所有没有被使用的类
6、通过类的符号,找到类名
nm -nm mach-o Path
通过符号表,找到相关的类名,光看符号我们可不认识这个是啥
7、查找当前所有类的父类和子类
如果父类没有被使用,子类被使用了,父类是不会出现在第1步的引用类集合的,所以这里要特殊处理。
如果父类在未使用类集合,子类不在未使用类集合,认为父类有被用到。
从未使用类集合将父类删掉
otool -oV 是获取所有的类结构及其定义的方法
otool -oV mach-o Path
8、检测项目中的静态字符串
如果某个静态字符串在未使用集合,删除当前类<认为使用了runtime的形式调用,事后可以再次确认>
otool -v -s __TEXT __cstring 是获取项目中所有的静态字符串
otool -v -s __TEXT __cstring mach-o Path
为什么要加这层过滤呢,因为项目中的类可能是通过runtime的方式调用的,类名转类,然后使用。这种类是不会出现第1步的。
比如下面的 CViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UIViewController *vc = [[NSClassFromString(@"CViewController") alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
9、检测当前未使用类的分类是否使用了load方法
分类使用了load方法,认为当前类被引用
otool -oV mach-o Path
如果一个分类使用了load方法,那么认为这个类也是被使用的,分类也不能自己裸奔吧。
通过正则找到所有的分类中有load方法的类,进行过滤
10、黑白名单过滤
黑名单过滤:
我们在查找完毕之后,可能会有很多的类,有些类我们没必要修改也修改不了,比如Pods中的类,或者MJRefresh中检测出一个无用类,我们也不能删除,作为工具类多余的类在以后不一定没用。
我们可以选择不看,比如,输入参数
-b MJ,AF
python FindClassUnRefs.py -p /Users/a58/Library/XXX.app -b MJ,AF
那么我们的结果中会把MJ和AF开头的干掉,方便查看
白名单:
我们只想看我们自己的类,一般项目都会有固定的开头,方便管理
比如,我们的类名都是以WB开头的,输入参数 -w WB
,那么结果只会显示WB
开头的未使用类
11、查看项目所有的属性
如果未使用类是已使用类的属性,那么在检测结果删除
otool -oV mach-o Path
如果一个类是另外一个类的属性,除此之外别的地方没有使用过。那么他不会出现在第1步。可能会被认为是未使用类
这个时候可以通过正则找到所有类的属性,如果当前类没有在未使用类集合,并且他的属性中有在未使用类中,那么他应该属于已经用类
12、检测结果写入日志文件
结果可能过多,终端显示不开,将结果写入文件,方便阅读
13、根据检测结果,在项目中二次确认后处理
检测结果是有偏差的,有些已使用的类仍然不太好检测。
已知的情况:
1、类里面都是C语言的函数,即使方法被调用了,也认为是未使用类
2、storyBoard中的类,都是nib,检测不到
3、作为数组之类的集合类型被引用,并且使用的时候没有体现该类
@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",没有调用B的任何方法,会认为B没被用到,这个是正常的。
6、A类引用B类,并且在A类中的方法调用了B类的方法,会认为B类被使用了,不用担心,A类认为未被使用。
7、A类引用B类,B类引用A类,并且相互调用了相互的方法,被认为都被使用过,这个目前还没啥好办法。
如果在检测过程中出现一些误差,还请留言,然后对该脚本进行优化,谢谢!!