通过代码获取mach-o中所有的类,方法

做包大小优化的时候,我们可以通过分析mach-o中的__objc_classlist(工程中所有类集合,包括OC和Swift类)和__objc_classrefs(用到的OC类集合)做差集得到oc的无用类。 今天就来说一下怎么样拿到mach-o中的类和方法。

准备工作:

先编译一个自己的.app包:在自己的demo工程中编译完成之后,在Products中showInFinder拿到xxx.app

方法1:通过otool命令进行过滤,拿出有效信息

首先执行

otool -arch arm64 -ov xxx.app > all.txt

拿到mach-o文件的整体描述输出到all.txt(这个文件后面会多次用到)中,打开all.txt:

发现它在__DATA_CONST中,然后再去拿__objc_classlist:

otool -arch arm64 -v -s __DATA_CONST __objc_classlist xxx.app > class.txt

class.txt中就是列的所有工程中的类,但是都是地址,可以通过复制对应的地址到all.txt查找来查看到底是哪一个类,比如00000001000925c8中对应的类是_ViewController:

可以看出,我们可以通过otool命令拿到我们想要的数据,具体可看这篇文章zhuanlan.zhihu.com/p/428084059...

方法2:通过代码分析mach-o

我们今天主要讲的是第二种方式。

在这之前,我们先来说一下mach-o的结构: 通过MachOView工具可查看mach-o文件

  • mach-header;//文件格式、文件类型、CPU构架等
  • load commands;//程序怎么加载到内存的一些描述信息(文件布局、链接信息、符号表位置等)
  • segment and section;//真正存储数据或代码的地方 //segment 中的数据都会被印射到虚拟内存中,所以 segment 是按页对齐的。即:segment 中的数据在虚拟内存 中占得大小要比在磁盘中所占大小更大。

把.app转为data,再通过偏移和size就能拿到mach-header,load commands和segment and section。 从MachOView中可以看到__objc_classlist在LC_SEGMENT_64(__DATA_CONST)中:

通过遍历data,得到__objc_classlist的偏移: 首先先拿到mach-header:

OC 复制代码
mach_header_64 mhHeader;
[self.fileData getBytes:&mhHeader range:NSMakeRange(0, sizeof(mach_header_64))];
if (mhHeader.filetype != MH_EXECUTE && mhHeader.filetype != MH_DYLIB) {
    NSLog(@"当前文件不是可执行文件");
    return;
}

mhHeader就是macho头,LoadCommonds在mhHeader的下面,所以LoadCommond的偏移就刚好是mhHeader的大小:

OC 复制代码
//当前load_command在data中的位置
unsigned long long currentLocation = sizeof(mach_header_64);  
拿到_classList和_classRefList:

mhHeader中的ncmds属性表示LoadCommonds中的个数,遍历:

OC 复制代码
section_64 _classList = {0};
section_64 _classRefList = {0};
...

for (int i = 0; i < ncmds; i ++) {
        load_command *cmd = (load_command *)malloc(sizeof(load_command));
        [fileData getBytes:cmd range:NSMakeRange(currentLocation, sizeof(load_command))];
        if (cmd->cmd == LC_SEGMENT_64) {  //只取LC_SEGMENT_64的数据
            segment_command_64 segmentCommand;
            [fileData getBytes:&segmentCommand range:NSMakeRange(currentLocation, sizeof(segment_command_64))];
            NSString *segName = [NSString stringWithFormat:@"%s",segmentCommand.segname];

            //取__TEXT、__DATA_CONST、__DATA中的值
            if ([segName isEqualToString:@"__TEXT"] ||
                [segName isEqualToString:@"__DATA_CONST"] ||
                [segName isEqualToString:@"__DATA"]) {

                unsigned long long secLocation = currentLocation + sizeof(segment_command_64);
                //segmentCommand中segment的数量
                for (int j = 0; j < segmentCommand.nsects; j ++) {
                    section_64 section;
                    [fileData getBytes:&section range:NSMakeRange(secLocation, sizeof(section_64))];
                    NSString *secName = [[NSString alloc] initWithUTF8String:section.sectname];
                    secLocation += sizeof(section_64);

                    if ([secName isEqualToString:@"__objc_classlist__DATA"] ||
                        [secName isEqualToString:@"__objc_classlist__DATA_CONST"]) {
                        _classList = section;
                    }

                    if ([secName isEqualToString:@"__objc_classrefs__DATA"] ||
                        [secName isEqualToString:@"__objc_classrefs__DATA_CONST"]) {
                        _classRefList = section;
                    }

                    if ([secName isEqualToString:@"__objc_nlclslist__DATA"] ||
                        [secName isEqualToString:@"__objc_nlclslist__DATA_CONST"]) {
                        _nlclsList = section;
                    }

                    if ([secName isEqualToString:@"__objc_nlcatlist__DATA"] ||
                        [secName isEqualToString:@"__objc_nlcatlist__DATA_CONST"]) {
                        _nlcatList = section;
                    }

                    if ([secName isEqualToString:@"__cstring"]) {
                        _cfstringList = section;
                    }

                    if ([secName isEqualToString:@"__text"]) {
                        _textList = section;
                    }

                    if ([secName isEqualToString:@"__swift5_types"]) {
                        _swift5Types = section;
                    }
                }
            }
        }
        currentLocation += cmd->cmdsize;
        free(cmd);
    }

这样就拿到了_classList和_classRefList,他们都是存的class的地址(8位),再通过遍历拿到类:

OC 复制代码
NSUInteger addressSize = 8;
NSRange range = NSMakeRange(classList.offset, 0);
for (int i = 0; i < _classList.size / addressSize; i ++) {
    //读出类的地址
    unsigned long long classAddress;
    NSData *data = [self.class readBytes:range length:addressSize fromFile:fileData];
    [data getBytes:&classAddress range:NSMakeRange(0, addressSize)];
    ...
}

拿到类的地址之后,指向了类的结构,再来看下方法1中通过otool工具得到的all.txt文件中类的结构:

而类的具体信息,如method,ivar,protocol,properties等都是在类的data中:

由此可以写出来类的结构:

OC 复制代码
struct class64 {
    unsigned long long isa;
    unsigned long long superclass;
    unsigned long long cache;
    unsigned long long vtable;
    unsigned long long data;   //指向class64_data结构地址
};

struct class64_data {
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    unsigned long long ivarLayout;
    unsigned long long name;  //类名地址
    unsigned long long baseMethods;
    unsigned long long baseProtocols;
    unsigned long long ivars;
    unsigned long long weakIvarLayout;
    unsigned long long baseProperties;
};
上一步遍历拿到类的地址之后,可以通过类的地址拿到class64和class64_data信息
OC 复制代码
- (class64)getClass64Address:(unsigned long long)address{
    unsigned long long classOffset = [self.class getOffsetFromVmAddress:address fileData:self.fileData];  //得到类的地址偏移
    class64 targetClass = {0};
    NSRange targetClassRange = NSMakeRange(classOffset, 0);
    NSData *data = [self.class readBytes:targetClassRange length:sizeof(class64) fromFile:self.fileData];
    [data getBytes:&targetClass length:sizeof(class64)];
    return targetClass;
}

- (class64_data)getClass64InfoWithAddress:(unsigned long long)address{
    class64_data targetClassInfo = {0};
    unsigned long long targetClassInfoOffset = [self.class getOffsetFromVmAddress:address fileData:self.fileData];
    targetClassInfoOffset = (targetClassInfoOffset / 8) * 8;
    NSRange targetClassInfoRange = NSMakeRange(targetClassInfoOffset, 0);
    NSData *data = [self.class readBytes:targetClassInfoRange length:sizeof(class64_data) fromFile:self.fileData];
    [data getBytes:&targetClassInfo length:sizeof(class64_data)];
    //或者直接这么取
//    [self.fileData getBytes:&targetClassInfo range:NSMakeRange(classInfoOffset, sizeof(class64_data))];
    return targetClassInfo;
}
获取类名:
OC 复制代码
//通过类的地址得到类的结构体
class64 targetClass = [self getClass64Address:classAddress];
//得到类的具体信息的结构体
class64_data classInfo = [self getClass64InfoWithAddress:targetClass.data];

//通过name的地址拿到name的偏移,再从data中拿到类名
unsigned long long stringOffset = [self.class getOffsetFromVmAddress:classInfo.name fileData:self.fileData];
NSString *className = @((char *)[self.fileData bytes]  + stringOffset);
获取方法列表:

再从all.txt中可以看出methods的结构:

前两个是方法列表的描述,entsize表示方法中的name,types,imp中存的是偏移还是地址,12:偏移,24:地址; count表示方法的个数。而后面用半圆标出来的部分代码每个方法。于是又可以拿到method的结构:

OC 复制代码
struct method64_list {
    unsigned int entsize;
    unsigned int count;
};

//方法中属性偏移
struct relative_method_t {
    int32_t nameOffset;
    int32_t typesOffset;
    int32_t impOffset;
};

//方法中属性地址
struct method64 {
    unsigned long long name;
    unsigned long long types;
    unsigned long long imp;
};
通过classInfo中baseMethods中的地址拿到method64_list:
OC 复制代码
- (NSArray *)getClass64Address:(unsigned long long)methodsAddress{
    unsigned long long address = methodsAddress;
    if (address == 0) {
        return @[];
    }
    NSData *fileData = self.fileData;
    unsigned long long max = [fileData length];
    //拿到方法列表偏移
    unsigned long long offset = [self.class getOffsetFromVmAddress:address fileData:self.fileData];
    method64_list method = {0};
    NSRange methodRange = NSMakeRange(offset, 0);
    NSData *data = [self.class readBytes:methodRange length:sizeof(method64_list) fromFile:self.fileData];
    [data getBytes:&method length:sizeof(method64_list)];

    NSMutableArray *methods = [NSMutableArray array];
    offset += sizeof(method64_list);
    for (int i = 0; i < method.count; i ++) {
        MachoMethod *methodObj = [[MachoMethod alloc] init];
        unsigned long long methodSize;
        unsigned long long methodNameOffset;
        unsigned long long methodTypeOffset;

        if ((method.entsize & 0x80000000) !=0) {
            relative_method_t methodInfo;
            [self.fileData getBytes:&methodInfo range:NSMakeRange(offset, sizeof(relative_method_t))];
            methodNameOffset = offset + methodInfo.nameOffset; //存的是方法名字地址的偏移
            methodTypeOffset = offset + methodInfo.typesOffset + sizeof(int32_t); //直接存的字符串
            unsigned long long imp = offset + methodInfo.impOffset + 8;
            methodSize = sizeof(relative_method_t);

            //方法名
            unsigned long long  methodNameAddress;  //得到方法地址
            [fileData getBytes:&methodNameAddress range:NSMakeRange(methodNameOffset, 8)];

            NSString *methodname = [self getStringWithAddress:methodNameAddress];
            methodObj.name = methodname;
            methodObj.types = @((char *)[fileData bytes] + (methodTypeOffset & ChainFixUpsRawvalueMask));
        }else {

            method64 methodInfo;
            [self.fileData getBytes:&methodInfo range:NSMakeRange(offset, sizeof(method64))];
            methodNameOffset = [self.class getOffsetFromVmAddress:methodInfo.name fileData:fileData];
            methodTypeOffset = [self.class getOffsetFromVmAddress:methodInfo.types fileData:fileData];
            methodSize = sizeof(method64);
            //方法名
            if (methodNameOffset > 0 && methodNameOffset < max) {
                const char *classmethodname = (char *)[fileData bytes] + methodNameOffset;
                methodObj.name = @(classmethodname);
            }
            //方法参数类型
            if (methodTypeOffset > 0 && methodTypeOffset < max) {
                const char *types = (char *)[fileData bytes] + (methodTypeOffset & ChainFixUpsRawvalueMask);
                methodObj.types = @(types);
            }
        }
        offset += methodSize;
        [methods addObject:methodObj];
    }

    NSLog(@"%@", methods);
    return methods;
}
类的方法包括实例方法和类方法,类方法在metaClass中:
OC 复制代码
//通过类的地址得到类的结构体
class64 targetClass = [self getClass64Address:classAddress];
//得到类的具体信息的结构体
class64_data classInfo = [self getClass64InfoWithAddress:targetClass.data];

//meta class
class64 metaClass = [self getClass64Address:targetClass.isa];
class64_data metaClassInfo = [self getClass64InfoWithAddress:metaClass.data];
obj.classMethods = [self getMethodsWithAddress:metaClassInfo.baseMethods];

再查看属性的结构同理可以获取到类的属性:

动手自己试试吧。

58同城的WBBlades工具就是通过分析Mach-o来检测无用类,欢迎使用和start

相关推荐
莹雨潇潇10 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr19 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele3 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀4 小时前
CSS——属性值计算
前端·css
DOKE4 小时前
VSCode终端:提升命令行使用体验
前端