前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >mach-o文件分析多余的类和方法

mach-o文件分析多余的类和方法

原创
作者头像
莫空9081
修改2021-06-16 10:27:03
3.6K0
修改2021-06-16 10:27:03
举报
文章被收录于专栏:iOS 备忘录iOS 备忘录

x^2 + y^2 = r^2# mach-o文件分析多余的类和方法.md

背景

最近做包大小优化,在做项目代码优化时,其中有一个过程是分析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符号格式化,输出项目的类结构及定义的方法
代码语言:txt
复制
// 直接在命令行查看
otool -arch arm64 -ov TestClass

// 或者输出对应信息到指定文件,比如导出到otool.txt
otool -arch arm64 -ov TestClass > otool.txt
    1. 查看链接了哪些库
代码语言:txt
复制
otool -L TestClass
    1. 筛选是否链接了某个指定的库,比如CoreFoundation
代码语言:txt
复制
otool -L TestClass | grep CoreFoundation
    1. 查看Mach-O所有类集合
代码语言:txt
复制
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classlist TestClass

// 或者输出对应信息到指定文件,比如导出到classlist.txt
otool -arch arm64 -v -s __DATA __objc_classlist TestClass > classlist.txt
    1. 查看Mach-O所有使用类的集合
代码语言:txt
复制
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass

// 或者输出对应信息到指定文件,比如导出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_classrefs TestClass > classrefs.txt
    1. 查看Mach-O所有使用方法的集合
代码语言:txt
复制
// 直接在命令行查看
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass

// 或者输出对应信息到指定文件,比如导出到classrefs.txt
otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt
    1. 查看c语言字符串
代码语言:txt
复制
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 镜像信息

分析无用类

获取__objc_classlist

来看__objc_classlist所在的section

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

WX20210511-182718.png
WX20210511-182718.png

参考LinkMap项目的symbolMapFromContent方法实现,笔者发现,它的匹配是读取文件,然后单行单行,匹配文案,设置标记位,从而解析对应信息。代码如下

代码语言:txt
复制
- (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。代码如下:

代码语言:txt
复制
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看有多少个。
    1. 运行LinkMap项目,选择otool.txt,然后断点看classListFromContent:方法的输出
    1. 两个结果个数一致,笔者认为代码运行正确。

获取__objc_classrefs

来看__objc_classrefs所在的section

代码语言:txt
复制
Contents of (__DATA,__objc_classrefs) section
000000010000d410 0x0 _OBJC_CLASS_$_UIColor
000000010000d418 0x10000d450
000000010000d420 0x0 _OBJC_CLASS_$_UISceneConfiguration
000000010000d428 0x10000d568

同样,先来分析上述代码,可以看到单行信息中,后面的部分要不是系统信息,要不是类地址。如下:

WX20210511-194536.png
WX20210511-194536.png

所以,笔者采取同样的处理逻辑,读取Contents of (__DATA,__objc_classrefs) section的内容,单行单行读取,判断如果包含0x100,说明是类地址,存储到数组里。实现如下

代码语言:txt
复制
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:方法返回的个数对比,相同则说明方法无错误。

取差值,获取无用类

在LinkMap中的analyze:方法中,调用classListFromContent:classRefsFromContent:,获取到了所有类和已引用类后,所有类存储是{ classAddress: className },已引用类存储的是[classAddress],去重后,遍历去重后的已引用类,然后把所有在已引用的地址从所有类中移除。最后所有类中剩下的就是无用的类。代码如下

代码语言:txt
复制
    // 所有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文件中配置的,但是都被识别为无使用类。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的类。

WX20210512-084919.png
WX20210512-084919.png
分析无用方法

无用方法的分析与类稍有不同,因为没有直接获取所有方法的地方,__objc_selrefs是所有引用到的方法,因此笔者想到的是,用__objc_classlist中的BaseMethods、InstanceMethods以及ClassMethods中的数据,作为所有方法的集合,然后和引用的方法做差值,最终得到无用方法。

获取__objc_selrefs

来看__objc_selrefs所在的section

代码语言:txt
复制
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 }的方式存起来。代码如下:

代码语言:txt
复制
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;
}

获取所有方法列表

这部分稍有麻烦,笔者想的是用__objc_classlist中的BaseMethods、InstanceMethods以及ClassMethods中的数据,作为所有方法的集合,所以先来看文件结构,总结出来规律

代码语言:txt
复制
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行的数据,而且笔者还希望能把这个方法跟类关联起来,这样最后输出查找的时候也比较方便。

WX20210512-103154.png
WX20210512-103154.png

笔者总结出来的规律如下

    1. 按照一行行的读取逻辑来,读到了data,然后读到了name,这时候name是类名字。
    1. 再接着往下读,读到了baseMethods或者InstanceMethods或者Class Methods,再然后读到了name,这时候name中是方法名字和方法地址。
    1. 再接着往下读,读到了data,重复步骤1

用代码逻辑实现就是,设置两个标志位,一个标记是类名,一个标记是方法;读到了data之后,把第一个标记置为YES,然后判断第一个标记位YES时,读到了name就更新类名;读到了包含Methods之后,把第一个标记置为NO,第二个标记置为YES,然后判断是第二个标记位YES时,就存储方法名和方法地址。最终数据以{ className:{ address: methodName } }存储。代码如下

代码语言:txt
复制
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;
}

取差值,获取无用方法

在LinkMap中的analyze:方法中,调用allSelRefsFromContent:selRefsFromContent:,获取到了所有方法和已引用方法后,所有方法存储是{ className:{ address: methodName } },已引用方法存储的是{ methodAddress: methodName },遍历去重后的已引用方法,然后把所有在已引用的地址从所有方法中移除。最后所有方法中剩下的就是无用的方法。代码如下

代码语言:txt
复制
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的代理方法被识别为了多余方法。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的代理方法。

WX20210512-101907.png
WX20210512-101907.png

最后

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

参考

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 原理
  • 实现
    • otool命令简单使用
      • OtoolAnalyse的实现原理
        • 分析无用类
        • 分析无用方法
    • 最后
    • 参考
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档