最近做包大小优化,在做项目代码优化时,其中有一个过程是分析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部分组成:
列举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文件。
比如项目名字为TestClass,进入TestClass exec所在的文件夹
// 直接在命令行查看
otool -arch arm64 -ov TestClass
// 或者输出对应信息到指定文件,比如导出到otool.txt
otool -arch arm64 -ov TestClass > otool.txt
2 . 查看链接了哪些库
otool -L TestClass
3 . 筛选是否链接了某个指定的库,比如CoreFoundation
otool -L TestClass | grep CoreFoundation
4 . 查看Mach-O所有类集合
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classlist TestClass
// 或者输出对应信息到指定文件,比如导出到classlist.txt
otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt
5 . 查看Mach-O所有使用类的集合
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass
// 或者输出对应信息到指定文件,比如导出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt
6 . 查看Mach-O所有使用方法的集合
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass
// 或者输出对应信息到指定文件,比如导出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt
7 . 查看c语言字符串
otool -v -s __TEXT __cstring TestClass
到这里为止,otool是什么?__DATA.__objc_classlist是什么?哪里来的?怎么跟otool命令结合起来使用?这几个问题解决了。但是接下来的,怎么获取差值?怎么结合使用正则表达式?要怎么解决呢?
《iOS代码瘦身实践:删除无用的类》这篇文章里使用python代码有实现的过程。但是笔者走了另一条路,这里分享一下,希望大家多多指点。
首先,参考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格式,所以这里难住了,没办法取对应的信息。笔者对比多个类结构,希望能总结出来固定的规律。
pwuqhvd2bl6chmo.png
参考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
看有多少个。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
同样,先来分析上述代码,可以看到单行信息中,后面的部分要不是系统信息,要不是类地址。如下:
jvdjs41eg2koqhy.png所以,笔者采取同样的处理逻辑,读取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文件中配置的,但是都被识别为无使用类。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的类。
r3ejgqmrutzpinw.png
无用方法的分析与类稍有不同,因为没有直接获取所有方法的地方,__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行的数据,而且笔者还希望能把这个方法跟类关联起来,这样最后输出查找的时候也比较方便。
546lptym28fsnyq.png
笔者总结出来的规律如下
用代码逻辑实现就是,设置两个标志位,一个标记是类名,一个标记是方法;读到了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的代理方法被识别为了多余方法。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的代理方法。
xsdk3uinmgbohre.png
完整的项目地址OtoolAnalyse,笔者用这样方法,分析出来了项目中无用的类、无用的方法,删除前要注意先确认。项目还有待完善的地方,比如系统方法的过滤,基类的判断逻辑,等等,留待后续补充。但整体分析的逻辑如上,笔者趟过了河,先分享为敬,。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/z0eK4cOfvYWFhHl16rGnEg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。