查看原文
其他

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

王徳亮 搜狐技术产品 2021-11-19


  

本文字数: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-OMach Object文件格式的缩写,是一种记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。

Mach-O文件主要由3部分组成:

  • Mach Header: 描述 Mach-O 的CPU架构、文件类型、加载命令等信息
  • Load Command: 描述文件中数据等具体组织结构,不同数据类型使用不同等加载命令表示
  • Data: Data中每一个段(Segment)的数据保存在此,段用来存放数据和代码

列举Data常见的Section,来自Mach-O 文件格式探索

表头表头
Section用途
__TEXT.__text主程序代码
__TEXT.__cstringC 语言字符串
__TEXT.__constconst 关键字修饰的常量
__TEXT.__stubs用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methnameObjective-C 方法名称
__TEXT.__objc_methtypeObjective-C 方法类型
__TEXT.__objc_classnameObjective-C 类名称
__DATA.__data初始化过的可变数据
__DATA.__la_symbol_ptrlazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const没有初始化过的常量
__DATA.__cfstring程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bssBSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common没有初始化过的符号声明
__DATA.__objc_classlistObjective-C 类列表
__DATA.__objc_protolistObjective-C 原型
__DATA.__objc_imginfoObjective-C 镜像信息
__DATA.__objc_selrefsObjective-C 方法引用
__DATA.__objc_protorefsObjective-C 原型引用
__DATA.__objc_superrefsObjective-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


      1. 查看链接了哪些库

      otool -L TestClass


        1. 筛选是否链接了某个指定的库,比如CoreFoundation

        otool -L TestClass | grep CoreFoundation


          1. 查看Mach-O所有类集合

          // 直接在命令行查看
          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所有使用类的集合

            // 直接在命令行查看
            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所有使用方法的集合

              // 直接在命令行查看
              otool -arch arm64 -v -s __DATA __objc_selrefs TestClass

              // 或者输出对应信息到指定文件,比如导出到classrefs.txt
              otool -arch arm64 -v -s __DATA __objc_selrefs TestClass > selrefs.txt


                1. 查看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_classlistObjective-C 类列表
                __DATA.__objc_classrefsObjective-C 类引用
                __DATA.__objc_superrefsObjective-C 超类引用
                __DATA.__objc_catlistObjective-C category列表
                __DATA.__objc_protolistObjective-C 原型
                __DATA.__objc_selrefsObjective-C 方法引用
                __DATA.__objc_imginfoObjective-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], nil16);
                                    
                                    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,笔者用这样方法,分析出来了项目中无用的类、无用的方法,删除前要注意先确认。项目还有待完善的地方,比如系统方法的过滤,基类的判断逻辑,等等,留待后续补充。但整体分析的逻辑如上,笔者趟过了河,先分享为敬,😄。

                    引用

                    • otool 工具分析可能没有使用的Objective-C类
                    • LinkMap
                    • iOS调优 | 深入理解Link Map File
                    • iOS堆栈信息解析(Mach-O)
                    • 包体积大小:瘦身
                    • Mach-O学习
                    • Mach-O 文件格式探索
                    • 二进制文件分析之常用命令
                    • iOS代码瘦身实践:删除无用的类

                    本期赠书

                    《Swift4 从零到精通iOS开发》

                    张益珲 著

                    本书由资深iOS开发工程师精心编撰,兼备核心语法、编程技巧、应用实践3部分。第1部分从Xcode开发工具及Swift学习环境的搭建开始,重点介绍Swift的语言特性和应用场景、Swift 4的新增特性。第2部分介绍Swift开发iOS应用的基本技能,包括独立UI控件的应用、视图界面逻辑的开发、动画与布局技术、网络与数据处理技术等。第3部分为应用部分,这部分安排3个实战项目(简易计算器、生活记事本、中国象棋游戏),旨在全面锻炼读者的实际开发能力。
                    本书各章还安排了练习题与模拟面试题。通过本书的学习,读者可以掌握使用Swift语言开发一款iOS软件从理论到实践的全部技术细节。

                    活动参与方式:留言点赞数前五名的同学各获赠书一本

                    获奖公布时间及位置:7月15日头条推送文末

                    特别提醒:兑奖截止至7月22日,请参与读者及时兑奖~




                    也许你还想看

                    (▼点击文章标题或封面查看)

                    正经分析iOS包大小优化

                    2021-05-27

                    【文末有惊喜!】iOS日历攻略:提醒调休并过滤法定节假日

                    2021-06-24

                    【文末有惊喜!】如何让iOS推送播放语音?

                    2021-05-13

                    包教包会:设计一套完整日志系统

                    2021-05-20

                    从YYModel源码分析JSON解析原理

                    2021-06-03


                    : . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

                    您可能也对以下帖子感兴趣

                    文章有问题?点此查看未经处理的缓存