美文网首页iOS开发资料收集区
Objective-C二进制瘦身

Objective-C二进制瘦身

作者: flexih | 来源:发表于2020-02-04 22:55 被阅读0次

    先说结论:我写了个工具检测无用方法、无用类以及无用协议,只需要Mach-O文件,对Build Setting里的Strip Style无要求,Snake

    Objective-C是采用消息发送的方式来实现类方法的调用。消息发送使用“查表”的方式实现从方法名到方法实现的定位。因此在编译的时候编译器不能确知一个方法是否真的被调用,也就无法像C语言一样只编译使用到的方法。也因此造成了目标二进制里包含没有使用的类、没有使用的方法、以及没有使用的协议等。

    为了实现消息发送,Objective-C的编译器会在编译的时候自动生成相关的结构体变量,来存储类的信息,这些信息也被称作ObjC元信息。

    clang命令使用-rewrite-objc参数可以得到.m文件的CPP实现。比如使用 xcrun -sdk iphonesimulator clang -rewrite-objc -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Framework a.m
    截取类A的定义,ObjC代码是:

    @protocol AProtocol<NSObject>
    - (void)aMeth;
    @end
    
    @interface A: NSObject<AProtocol>
    @end
    
    @implementation A
    - (void)aMeth {
    }
    @end
    

    rewrite之后的代码是:

    extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_A __attribute__ ((used, section ("__DATA,__objc_data"))) = {
        0, // &OBJC_METACLASS_$_A,
        0, // &OBJC_CLASS_$_NSObject,
        0, // (void *)&_objc_empty_cache,
        0, // unused, was (void *)&_objc_empty_vtable,
        &_OBJC_CLASS_RO_$_A,
    };
    static struct _class_ro_t _OBJC_CLASS_RO_$_A __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        0, sizeof(struct A_IMPL), sizeof(struct A_IMPL), 
        (unsigned int)0, 
        0, 
        "A",
        (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_A,
        (const struct _objc_protocol_list *)&_OBJC_CLASS_PROTOCOLS_$_A,
        0, 
        0, 
        0, 
    };
    static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
        &OBJC_CLASS_$_A,
    };
    

    CPP代码变量的声明处都出现了__attribute__((used, section("xx")))。这里section的意思是在Mach-O里对应名字的section,并把变量的内容放到该section下。

    Mach-O是iOS/macOS平台下可执行二进制文件格式。Mach-O文件格式这里不详述了,可以参考MachOOverviewosx-abi-macho-file-format-reference

    以下内容特指64位Arch。

    __objc_classlist

    该section下存储的是指针,指针指向struct objc_class的内存,位于__objc_constsection下。这里存储了代码里所有的ObjC类。

    __objc_classrefs, __objc_superrefs

    该section下存储的是指针,指针指向struct objc_class的内存。这里存储了代码里使用到的ObjC类,也即是使用过消息发送方式调用过alloc或者new方法生成对象的类。不包含NSClassFromString()方式生成的对象的类

    __objc_selrefs

    该section下存储的是指针,指针指向以\0结尾的字符串,字符串的内容是方法名。这里存储了被调用过的方法名。不包含NSSelectorFromString()返回的SEL

    __objc_protolist

    该section下存储的是指针,指针指向struct protocol_t的内存。这里存储了代码里所有的protocol。

    __objc_catlist

    该section下存储的是指针,指针指向struct category_t的内存。这里存储了代码里所有的分类。

    Binding Info

    对于某些非零字段的值却是0,比如struct objc_classisa字段。这种情况是引用了外部lib里的符号。外部符号记录在dyld_info_command下的binding info里。此部分的格式可以参考MachOView的处理。从Binding Info里得到地址到符号的对应关系。遇到非零字段为0的时候,去Bind Info里查找该内存地址对于的符号即可。

    实现

    一般的无用方法获取方式,是利用otool、nm等命令获取。这里直接读取Mach-O文件,解析出ObjC信息。
    无用的方法 = __objc_classlist的 (instanceMethods - __objc_selrefs) + clasMethods - __objc_selrefs
    无用的类 = __objc_classlist - ((__objc_classrefs+__objc_superrefs)+(__objc_classrefs+__objc_superrefs)的superclass)
    无用的协议 = __objc_protolist - (__objc_classrefs+__objc_superrefs) 的protocol_list

    配合使用Mach-O对应的Linkmap,可以获得方法的大小和方法、类、协议所属的library。可以生成json格式数据,以供进一步处理。
    代码使用C++编写,文件读取使用mmap,处理一个460.6M大小的Mach-O文件和134.3M的linkmap文件只需要1.62秒。

    Usage:
      snake [-scp] [-l path] mach-o ...
    
      -s, --selector     Unused selectors
      -c, --class        Unused classes
      -p, --protocol     Unused protocoles
      -l, --linkmap arg  Linkmap file, which has selector size, library name
      -j, --json         Output json format
          --help         Print help
    

    具体实现移步SnakeSnakKit

    相关文章

      网友评论

        本文标题:Objective-C二进制瘦身

        本文链接:https://www.haomeiwen.com/subject/jbirxhtx.html