【文末有惊喜!】详解:mach-o文件如何分析多余的类和方法

发表于 3年以前  | 总阅读数:265 次

背景

最近做包大小优化,在做项目代码优化时,其中有一个过程是分析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-OMach 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所在的文件夹

  1. otool符号格式化,输出项目的类结构及定义的方法

// 直接在命令行查看
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代码有实现的过程。但是笔者走了另一条路,这里分享一下,希望大家多多指点。

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格式,所以这里难住了,没办法取对应的信息。笔者对比多个类结构,希望能总结出来固定的规律。

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的个数一致,就说明没有问题。具体如下:

  1. 笔者把otool.txt文件中除去Contents of (__DATA_CONST,__objc_classlist) section部分删掉,然后搜索000000010看有多少个。
  2. 运行LinkMap项目,选择otool.txt,然后断点看classListFromContent:方法的输出
  3. 两个结果个数一致,笔者认为代码运行正确。

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

笔者总结出来的规律如下

  1. 按照一行行的读取逻辑来,读到了data,然后读到了name,这时候name是类名字。
  2. 再接着往下读,读到了baseMethods或者InstanceMethods或者Class Methods,再然后读到了name,这时候name中是方法名字和方法地址。
  3. 再接着往下读,读到了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的代理方法被识别为了多余方法。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的代理方法。

xsdk3uinmgbohre.png

最后

完整的项目地址OtoolAnalyse,笔者用这样方法,分析出来了项目中无用的类、无用的方法,删除前要注意先确认。项目还有待完善的地方,比如系统方法的过滤,基类的判断逻辑,等等,留待后续补充。但整体分析的逻辑如上,笔者趟过了河,先分享为敬,。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/z0eK4cOfvYWFhHl16rGnEg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录