Objective-C二进制瘦身
作者 | flexih
来源 | 简书,点击“阅读原文”查看作者更多文章
先说结论:我写了个工具检测无用方法、无用类以及无用协议,只需要 Mach-O 文件,对 Build Setting 里的 Strip Style 无要求, Snake1。
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 文件格式这里不详述了,可以参考 MachOOverview2 和osx-abi-macho-file-format-reference3。
以下内容特指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_class的isa字段。这种情况是引用了外部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_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
具体实现移步Snake1和SnakeKit4。
参考
[1]https://github.com/flexih/Snake
[2]https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html
[3]https://github.com/aidansteele/osx-abi-macho-file-format-reference
[4]https://github.com/flexih/SnakeKit