【文末有惊喜!】详解:mach-o文件如何分析多余的类和方法
本文字数:5014字
预计阅读时间:34分钟
背景
最近做包大小优化,在做项目代码优化时,其中有一个过程是分析Mach-O文件,看网上很多文章都说通过otool分析Mach-O,获取__objc_classrefs、__objc_classlist等,然后找出无用类和无用方法。
比如:无用类通过 otool 逆向Mach-O文件 __DATA.__objc_classlist段和__DATA.__objc_classrefs 段获取所有 OC 类和被引用的类,两个集合差值为无用类集合,结合 nm -nm 得到地址和对应类名符号化无用类类名
来自干货!京东商城iOS App瘦身实践
又或者结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)
来自iOS微信安装包瘦身
上面那些话看起来简简单单的,但是笔者操作起来确遇到了很多困难,首先otool是什么?然后__DATA.__objc_classlist是什么?哪里来的?怎么跟otool命令结合起来使用?怎么获取差值?怎么结合使用正则表达式,等等?笔者在没有大佬带领的情况下,只能是一步步趟过来。
于是笔者这两天就自己小马过河,实践了一下,做成了一个类似LinkMap分析的工具——OtoolAnalyse,分享一下具体的实现过程和原理。
主要涉及到otool命令的简单使用、OtoolAnalyse的实现原理两部分。
原理
首先来看Mach-O
是什么,Mach-O
是Mach Object
文件格式的缩写,是一种记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。
Mach-O文件主要由3部分组成:
Mach Header: 描述 Mach-O 的CPU架构、文件类型、加载命令等信息 Load Command: 描述文件中数据等具体组织结构,不同数据类型使用不同等加载命令表示 Data: Data中每一个段(Segment)的数据保存在此,段用来存放数据和代码
列举Data常见的Section,来自Mach-O 文件格式探索
表头 | 表头 |
---|---|
Section | 用途 |
__TEXT.__text | 主程序代码 |
__TEXT.__cstring | C 语言字符串 |
__TEXT.__const | const 关键字修饰的常量 |
__TEXT.__stubs | 用于 Stub 的占位代码,很多地方称之为桩代码。 |
__TEXT.__stubs_helper | 当 Stub 无法找到真正的符号地址后的最终指向 |
__TEXT.__objc_methname | Objective-C 方法名称 |
__TEXT.__objc_methtype | Objective-C 方法类型 |
__TEXT.__objc_classname | Objective-C 类名称 |
__DATA.__data | 初始化过的可变数据 |
__DATA.__la_symbol_ptr | lazy binding 的指针表,表中的指针一开始都指向 __stub_helper |
__DATA.nl_symbol_ptr | 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
__DATA.__const | 没有初始化过的常量 |
__DATA.__cfstring | 程序中使用的 Core Foundation 字符串(CFStringRefs) |
__DATA.__bss | BSS,存放为初始化的全局变量,即常说的静态内存分配 |
__DATA.__common | 没有初始化过的符号声明 |
__DATA.__objc_classlist | Objective-C 类列表 |
__DATA.__objc_protolist | Objective-C 原型 |
__DATA.__objc_imginfo | Objective-C 镜像信息 |
__DATA.__objc_selrefs | Objective-C 方法引用 |
__DATA.__objc_protorefs | Objective-C 原型引用 |
__DATA.__objc_superrefs | Objective-C 超类引用 |
实现
Mach-O文件获取:Xcode打包好的iPA,改后缀名为.zip,然后解压缩得到payload文件夹,其中有xxx.app,右键显示包内容,其中有xxx的exec文件,即是Mach-O文件。
otool命令简单使用
比如项目名字为TestClass,进入TestClass exec所在的文件夹
otool符号格式化,输出项目的类结构及定义的方法
// 直接在命令行查看
otool -arch arm64 -ov TestClass
// 或者输出对应信息到指定文件,比如导出到otool.txt
otool -arch arm64 -ov TestClass > otool.txt
查看链接了哪些库
otool -L TestClass
筛选是否链接了某个指定的库,比如CoreFoundation
otool -L TestClass | grep CoreFoundation
查看Mach-O所有类集合
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classlist TestClass
// 或者输出对应信息到指定文件,比如导出到classlist.txt
otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt
查看Mach-O所有使用类的集合
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass
// 或者输出对应信息到指定文件,比如导出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt
查看Mach-O所有使用方法的集合
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass
// 或者输出对应信息到指定文件,比如导出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt
查看c语言字符串
otool -v -s __TEXT __cstring TestClass
到这里为止,otool是什么?__DATA.__objc_classlist是什么?哪里来的?怎么跟otool命令结合起来使用?这几个问题解决了。但是接下来的,怎么获取差值?怎么结合使用正则表达式?要怎么解决呢?
《iOS代码瘦身实践:删除无用的类》这篇文章里使用python代码有实现的过程。但是笔者走了另一条路,这里分享一下,希望大家多多指点。
OtoolAnalyse的实现原理
首先,参考otool的命令otool -arch arm64 -ov TestClass > otool.txt
,生成otool.txt
打开otool.txt,搜索Contents of (__DATA
,会发现
Contents of (__DATA_CONST,__objc_classlist) section
或者Contents of (__DATA,__objc_classlist) section
Contents of (__DATA,__objc_classrefs) section
Contents of (__DATA,__objc_superrefs) section
Contents of (__DATA,__objc_catlist) section
Contents of (__DATA_CONST,__objc_protolist) section
或者Contents of (__DATA,__objc_protolist) section
Contents of (__DATA,__objc_selrefs) section
Contents of (__DATA_CONST,__objc_imageinfo) section
结合下面的表格来看,就能知道每个section代表的含义是什么了。
表头 | 表头 |
---|---|
Section | 用途 |
__DATA.__objc_classlist | Objective-C 类列表 |
__DATA.__objc_classrefs | Objective-C 类引用 |
__DATA.__objc_superrefs | Objective-C 超类引用 |
__DATA.__objc_catlist | Objective-C category列表 |
__DATA.__objc_protolist | Objective-C 原型 |
__DATA.__objc_selrefs | Objective-C 方法引用 |
__DATA.__objc_imginfo | Objective-C 镜像信息 |
分析无用类
1. 获取__objc_classlist
来看__objc_classlist
所在的section
0000000100008028 0x10000d450 // 后面的地址0x10000d450,是class的唯一地址
isa 0x10000d478
superclass 0x0 _OBJC_CLASS_$_UIViewController // 父类
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x10000c0b8
flags 0x90
instanceStart 8
instanceSize 8
reserved 0x0
ivarLayout 0x0
name 0x1000073cd SecondViewController // 类名
baseMethods 0x1000064f0
entsize 12 (relative)
count 1
name 0x6ed8 (0x10000d3d0 extends past end of file)
types 0xf6a (0x100007466 extends past end of file)
imp 0xfffffbb8 (0x1000060b8 extends past end of file)
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
这里我们通过单个类的信息结构,可以看出其中包含类的地址、类的名字、父类的地址,而笔者想做的是通过固定的代码获取类的信息,然后放到字典中,直到__objc_classlis
这个section结束,然后就获取了所有类名字和地址。
那要怎么做呢?由于文件不是固定的json格式,所以这里难住了,没办法取对应的信息。笔者对比多个类结构,希望能总结出来固定的规律。
参考LinkMap项目的symbolMapFromContent方法实现,笔者发现,它的匹配是读取文件,然后单行匹配,匹配文案,设置标记位,从而解析对应信息。代码如下
- (NSMutableDictionary *)symbolMapFromContent:(NSString *)content {
NSMutableDictionary <NSString *,SymbolModel *>*symbolMap = [NSMutableDictionary new];
// 符号文件列表
NSArray *lines = [content componentsSeparatedByString:@"\n"];
BOOL reachFiles = NO;
BOOL reachSymbols = NO;
BOOL reachSections = NO;
for(NSString *line in lines) {
if([line hasPrefix:@"#"]) {
if([line hasPrefix:@"# Object files:"])
reachFiles = YES;
else if ([line hasPrefix:@"# Sections:"])
reachSections = YES;
else if ([line hasPrefix:@"# Symbols:"])
reachSymbols = YES;
} else {
if(reachFiles == YES && reachSections == NO && reachSymbols == NO) {
NSRange range = [line rangeOfString:@"]"];
if(range.location != NSNotFound) {
SymbolModel *symbol = [SymbolModel new];
symbol.file = [line substringFromIndex:range.location+1];
NSString *key = [line substringToIndex:range.location+1];
symbolMap[key] = symbol;
}
} else if (reachFiles == YES && reachSections == YES && reachSymbols == YES) {
NSArray <NSString *>*symbolsArray = [line componentsSeparatedByString:@"\t"];
if(symbolsArray.count == 3) {
NSString *fileKeyAndName = symbolsArray[2];
NSUInteger size = strtoul([symbolsArray[1] UTF8String], nil, 16);
NSRange range = [fileKeyAndName rangeOfString:@"]"];
if(range.location != NSNotFound) {
NSString *key = [fileKeyAndName substringToIndex:range.location+1];
SymbolModel *symbol = symbolMap[key];
if(symbol) {
symbol.size += size;
}
}
}
}
}
}
return symbolMap;
}
所以,笔者发现,如果按照同样的逻辑,单行读取+标记位时,同样的逻辑也可以使用,即每次000000010
开头时,说明是一个新类的开始,存储对应的地址,设置可以存储名字标记位,然后读取到name时,就用{ classAddress: className }
的格式存储下来,并把标识位清除,直到下一行包含000000010
时,再重置标识位为YES。代码如下:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";
// 获取classList的类
- (NSMutableDictionary *)classListFromContent:(NSString *)content {
// 符号文件列表
NSArray *lines = [content componentsSeparatedByString:@"\n"];
BOOL canAddName = NO;
NSMutableDictionary *classListResults = [NSMutableDictionary dictionary];
NSString *addressStr = @"";
BOOL classListBegin = NO;
for(NSString *line in lines) {
if([line containsString:kConstPrefix] && [line containsString:kQueryClassList]) {
classListBegin = YES;
continue;
}
else if ([line containsString:kConstPrefix]) {
classListBegin = NO;
break;;
}
if (classListBegin) {
if([line containsString:@"000000010"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
NSString *address = [components lastObject];
addressStr = address;
canAddName = YES;
}
else {
if (canAddName && [line containsString:@"name"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
NSString *className = [components lastObject];
[classListResults setValue:className forKey:addressStr];
addressStr = @"";
canAddName = NO;
}
}
}
}
NSLog(@"__objc_classlist总结如下,共有%ld个\n%@:", classListResults.count, classListResults);
return classListResults;
}
然后怎么调试这个代码的正确与否?
笔者这时候想到了借助LinkMap的UI,因为同样都是需要选择文件,读取文件,而且笔者也想做分析之后结果显示,外加最后输出结果到文件,一整套的逻辑。所以,笔者就想到了把LinkMap的内部实现改掉。
首先第一步,注释掉checkContent:
的判断,然后analyze:
方法中把调用symbolMapFromContent:
的地方改为调用classListFromContent:
,断点调试看classListFromContent:
方法是否正确?那如何判断这个方法是否正确呢?最简单的方法根据个数来,经过classListFromContent:
得到的NSMutableDiction的数据的个数,和直接搜索otool.txt
文件中Contents of (__DATA_CONST,__objc_classlist) section
部分000000010
的个数一致,就说明没有问题。具体如下:
笔者把 otool.txt
文件中除去Contents of (__DATA_CONST,__objc_classlist) section
部分删掉,然后搜索000000010
看有多少个。运行LinkMap项目,选择otool.txt,然后断点看 classListFromContent:
方法的输出两个结果个数一致,笔者认为代码运行正确。
2. 获取__objc_classrefs
来看__objc_classrefs
所在的section
Contents of (__DATA,__objc_classrefs) section
000000010000d410 0x0 _OBJC_CLASS_$_UIColor
000000010000d418 0x10000d450
000000010000d420 0x0 _OBJC_CLASS_$_UISceneConfiguration
000000010000d428 0x10000d568
同样,先来分析上述代码,可以看到单行信息中,后面的部分要不是系统信息,要不是类地址。如下:
所以,笔者采取同样的处理逻辑,读取Contents of (__DATA,__objc_classrefs) section
的内容,单行读取,判断如果包含0x100
,说明是类地址,存储到数组里。实现如下
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassRefs = @"__objc_classrefs";
// 获取classrefs
- (NSArray *)classRefsFromContent:(NSString *)content {
// 符号文件列表
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableArray *classRefsResults = [NSMutableArray array];
BOOL classRefsBegin = NO;
for(NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQueryClassRefs]) {
classRefsBegin = YES;
continue;
}
else if (classRefsBegin && [line containsString:kConstPrefix]) {
classRefsBegin = NO;
break;
}
if(classRefsBegin && [line containsString:@"000000010"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
NSString *address = [components lastObject];
if ([address hasPrefix:@"0x100"]) {
[classRefsResults addObject:address]; }
}
}
NSLog(@"\n\n__objc_refs总结如下,共有%ld个\n%@:", classRefsResults.count, classRefsResults);
return classRefsResults;
}
然后校验上面方法的正确与否,去除除了Contents of (__DATA,__objc_classrefs) section
的之外的内容,然后搜索0x100
的个数,与classRefsFromContent:
方法返回的个数对比,相同则说明方法无错误。
3. 取差值,获取无用类
在LinkMap中的analyze:
方法中,调用classListFromContent:
和classRefsFromContent:
,获取到了所有类和已引用类后,所有类存储是{ classAddress: className }
,已引用类存储的是[classAddress]
,去重后,遍历去重后的已引用类,然后把所有在已引用的地址从所有类中移除。最后所有类中剩下的就是无用的类。代码如下
// 所有classList类和类名字
NSDictionary *classListDic = [self classListFromContent:content];
// 所有引用的类
NSArray *classRefs = [self classRefsFromContent:content];
// // 所有引用的父类
// NSArray *superRefs = [self superRefsFromContent:content];
// 先把类和父类数组做去重
NSMutableSet *refsSet = [NSMutableSet setWithArray:classRefs];
// [refsSet addObjectsFromArray:superRefs];
// 所有在refsSet中的都是已使用的,遍历classList,移除refsSet中涉及的类
// 余下的就是多余的类
for (NSString *address in refsSet.allObjects) {
[classListDic setValue:nil forKey:address];
}
// 移除系统类,比如SceneDelegate,或者Storyboard中的类
NSLog(@"多余的类如下:%@", classListDic);
最后测试输出结果如下,可以看到输出结果的结构,但是其中ViewController是Storyboard引用的,SceneDelegate是Info.plist文件中配置的,但是都被识别为无使用类。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的类。
分析无用方法
无用方法的分析与类稍有不同,因为没有直接获取所有方法的地方,__objc_selrefs
是所有引用到的方法,因此笔者想到的是,用__objc_classlist
中的BaseMethods、InstanceMethods以及ClassMethods中的数据,作为所有方法的集合,然后和引用的方法做差值,最终得到无用方法。
4. 获取__objc_selrefs
来看__objc_selrefs
所在的section
Contents of (__DATA,__objc_selrefs) section
0x100006647 Tapped:
0x1000067e5 application:didFinishLaunchingWithOptions:
0x1000070f9 application:configurationForConnectingSceneSession:options:
0x100007135 application:didDiscardSceneSessions:
0x10000717d scene:willConnectToSession:options:
0x1000071a1 sceneDidDisconnect:
0x1000071b5 sceneDidBecomeActive:
0x1000071cb sceneWillResignActive:
0x1000071e2 sceneWillEnterForeground:
0x1000071fc sceneDidEnterBackground:
0x10000715a window
0x100007161 setWindow:
0x10000739d .cxx_destruct
0x1000065e4 viewDidLoad
0x1000065f0 purpleColor
0x1000065fc view
0x100006601 setBackgroundColor:
0x100006615 navigationController
0x10000662a pushViewController:animated:
0x10000664f role
0x100006654 initWithName:sessionRole:
可以看到,这部分的数据比较简单,前面是地址,后面是方法名字,这里遍历每一行数据,然后直接以{ methodAddress: methodName }
的方式存起来。代码如下:
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQuerySelRefs = @"__objc_selrefs";
// 获取已使用的方法集合
- (NSMutableDictionary *)selRefsFromContent:(NSString *)content {
// 符号文件列表
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableDictionary *selRefsResults = [NSMutableDictionary dictionary];
BOOL selRefsBegin = NO;
for(NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQuerySelRefs]) {
selRefsBegin = YES;
continue;;
}
else if (selRefsBegin && [line containsString:kConstPrefix]) {
selRefsBegin = NO;
break;
}
if(selRefsBegin) {
NSArray *components = [line componentsSeparatedByString:@" "];
if (components.count > 2) {
NSString *methodName = [components lastObject];
NSString *methodAddress = components[components.count - 2];
[selRefsResults setValue:methodName forKey:methodAddress];
}
}
}
NSLog(@"\n\n__objc_selrefs总结如下,共有%ld个\n%@:", selRefsResults.count, selRefsResults);
return selRefsResults;
}
5. 获取所有方法列表
这部分稍有麻烦,笔者想的是用__objc_classlist
中的BaseMethods、InstanceMethods以及ClassMethods中的数据,作为所有方法的集合,所以先来看文件结构,总结出来规律
00000001007c1c20 0x100935c98
isa 0x100935c70
superclass 0x0 _OBJC_CLASS_$_NSObject
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x1007c4fc8
flags 0x90
instanceStart 8
instanceSize 8
reserved 0x0
ivarLayout 0x0
name 0x1006fb54a ColorManager
baseMethods 0x0
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
Meta Class
isa 0x0 _OBJC_METACLASS_$_NSObject
superclass 0x0 _OBJC_METACLASS_$_NSObject
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x1007c4f80
flags 0x91 RO_META
instanceStart 40
instanceSize 40
reserved 0x0
ivarLayout 0x0
name 0x1006fb54a ColorManager
baseMethods 0x1007c4f18
entsize 24
count 4
name 0x100689e19 primaryTextColor
types 0x1007038cd @16@0:8
imp 0x100004810
name 0x100689e2a secondaryTextColor
types 0x1007038cd @16@0:8
imp 0x10000482c
name 0x100689e3d primaryTintColor
types 0x1007038cd @16@0:8
imp 0x100004848
name 0x100689e4e backgroundColor
types 0x1007038cd @16@0:8
imp 0x100004878
baseProtocols 0x0
ivars 0x0
weakIvarLayout 0x0
baseProperties 0x0
00000001007c1c28 0x100935ce8
isa 0x100935cc0
superclass 0x0 _OBJC_CLASS_$_NSObject
cache 0x0 __objc_empty_cache
vtable 0x0
data 0x1007c5648
flags 0x194 RO_HAS_CXX_STRUCTORS
instanceStart 8
instanceSize 152
reserved 0x0
ivarLayout 0x1006fb56a
layout map 0x15 0x21 0x12
name 0x1006fb55a SectionModel
baseMethods 0x1007c5078
entsize 24
count 31
name 0x100689eac groupName
types 0x1007038cd @16@0:8
imp 0x100004948
name 0x100689eb6 setGroupName:
types 0x1007038d5 v24@0:8@16
imp 0x100004954
name 0x100689ec4 name
types 0x1007038cd @16@0:8
imp 0x10000495c
name 0x100689ec9 setName:
types 0x1007038d5 v24@0:8@16
imp 0x100004968
name 0x100689ed2 menuId
types 0x1007038cd @16@0:8
imp 0x100004970
name 0x100689ed9 setMenuId:
types 0x1007038d5 v24@0:8@16
...
上面的文件能看出来什么规律?脑壳疼,笔者想获取的是BaseMethods后面的name行的数据,而且笔者还希望能把这个方法跟类关联起来,这样最后输出查找的时候也比较方便。
笔者总结出来的规律如下
按照一行行的读取逻辑来,读到了data,然后读到了name,这时候name是类名字。 再接着往下读,读到了baseMethods或者InstanceMethods或者Class Methods,再然后读到了name,这时候name中是方法名字和方法地址。 再接着往下读,读到了data,重复步骤1
用代码逻辑实现就是,设置两个标志位,一个标记是类名,一个标记是方法;读到了data之后,把第一个标记置为YES,然后判断第一个标记位YES时,读到了name就更新类名;读到了包含Methods之后,把第一个标记置为NO,第二个标记置为YES,然后判断是第二个标记位YES时,就存储方法名和方法地址。最终数据以{ className:{ address: methodName } }
存储。代码如下
static NSString *kConstPrefix = @"Contents of (__DATA";
static NSString *kQueryClassList = @"__objc_classlist";
// 获取所有方法集合 { className:{ address: methodName } }
- (NSMutableDictionary *)allSelRefsFromContent:(NSString *)content {
// 符号文件列表
NSArray *lines = [content componentsSeparatedByString:@"\n"];
NSMutableDictionary *allSelResults = [NSMutableDictionary dictionary];
BOOL allSelResultsBegin = NO;
BOOL canAddName = NO;
BOOL canAddMethods = NO;
NSString *className = @"";
NSMutableDictionary *methodDic = [NSMutableDictionary dictionary];
for (NSString *line in lines) {
if ([line containsString:kConstPrefix] && [line containsString:kQueryClassList]) {
allSelResultsBegin = YES;
continue;
}
else if (allSelResultsBegin && [line containsString:kConstPrefix]) {
allSelResultsBegin = NO;
break;
}
if (allSelResultsBegin) {
if ([line containsString:@"data"]) {
if (methodDic.count > 0) {
[allSelResults setValue:methodDic forKey:className];
methodDic = [NSMutableDictionary dictionary];
}
// data之后第一个的name,是类名
canAddName = YES;
canAddMethods = NO;
continue;
}
if (canAddName && [line containsString:@"name"]) {
// 更新类名,用作标记{ className:{ address: methodName } }
NSArray *components = [line componentsSeparatedByString:@" "];
className = [components lastObject];
continue;
}
if ([line containsString:@"methods"] || [line containsString:@"Methods"]) {
// method之后的name是方法名,和方法地址
canAddName = NO;
canAddMethods = YES;
continue;
}
if (canAddMethods && [line containsString:@"name"]) {
NSArray *components = [line componentsSeparatedByString:@" "];
if (components.count > 2) {
NSString *methodAddress = components[components.count-2];
NSString *methodName = [components lastObject];
[methodDic setValue:methodName forKey:methodAddress];
}
continue;
}
}
}
return allSelResults;
}
6. 取差值,获取无用方法
在LinkMap中的analyze:
方法中,调用allSelRefsFromContent:
和selRefsFromContent:
,获取到了所有方法和已引用方法后,所有方法存储是{ className:{ address: methodName } }
,已引用方法存储的是{ methodAddress: methodName }
,遍历去重后的已引用方法,然后把所有在已引用的地址从所有方法中移除。最后所有方法中剩下的就是无用的方法。代码如下
NSMutableDictionary *methodsListDic = [self allSelRefsFromContent:content];
NSMutableDictionary *selRefsDic = [self selRefsFromContent:content];
// 遍历selRefs移除methodsListDic,剩下的就是未使用的
for (NSString *methodAddress in selRefsDic.allKeys) {
for (NSDictionary *methodDic in methodsListDic.allValues) {
[methodDic setValue:nil forKey:methodAddress];
}
}
// 遍历移除空的元素
NSMutableDictionary *resultDic = [NSMutableDictionary dictionary];
for (NSString *classNameStr in methodsListDic.allKeys) {
NSDictionary *methodDic = [methodsListDic valueForKey:classNameStr];
if (methodDic.count > 0) {
[resultDic setValue:methodDic forKey:classNameStr];
}
}
NSLog(@"多余的方法如下%@", resultDic);
最后测试输出结果如下,可以看到输出结果的结构,其中AppDelegate和SceneDelegate的代理方法被识别为了多余方法。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的代理方法。
最后
完整的项目地址OtoolAnalyse,笔者用这样方法,分析出来了项目中无用的类、无用的方法,删除前要注意先确认。项目还有待完善的地方,比如系统方法的过滤,基类的判断逻辑,等等,留待后续补充。但整体分析的逻辑如上,笔者趟过了河,先分享为敬,😄。
引用
otool 工具分析可能没有使用的Objective-C类 LinkMap iOS调优 | 深入理解Link Map File iOS堆栈信息解析(Mach-O) 包体积大小:瘦身 Mach-O学习 Mach-O 文件格式探索 二进制文件分析之常用命令 iOS代码瘦身实践:删除无用的类
本期赠书
《Swift4 从零到精通iOS开发》
张益珲 著
活动参与方式:留言点赞数前五名的同学各获赠书一本
获奖公布时间及位置:7月15日头条推送文末
也许你还想看
(▼点击文章标题或封面查看)
正经分析iOS包大小优化
2021-05-27
【文末有惊喜!】iOS日历攻略:提醒调休并过滤法定节假日
2021-06-24
【文末有惊喜!】如何让iOS推送播放语音?
2021-05-13
包教包会:设计一套完整日志系统
2021-05-20