这也是App体积优化的最后一个优化方向,可执行文件的优化,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。
那么我们如何能直观的看到可执行文件大小的变化呢?那么你就需要了解一下LinkMap的使用,因为这不是本文的重点,不多做赘述。
可执行文件的优化
1. 动态库的优化
由于iOS8对可执行文件__Text字段60M限制,大型的App逼近60M大关,为了不影响发版,有不少模块以动态库的形式集成到工程中。那么怎么给动态库瘦身呢?
我们先来讨论两种去除动态库多余符号(符号表等)的方式:
1)在链接时去除,即在动态库工程中Other Linker Flags中添加-s参数,经过测试:不管是在启动时加载,还是手动方
式加载动态库都没问题。于是准备使用这个方案。然而,在执行的时候发现了一个严重的问题:加了此参数后,不能生成完整
的dsym文件,这会影响崩溃后符号的解析。于是此方案作罢。
2)使用strip -x命令处理动态库。因为是对动态库产物进行处理,所以不会对dsym产生影响,经过测试,strip后的动态
库,也可以使用dsym文件找到符号。于是我们尝试在工程中添加脚本统一处理工程中的动态库。在添加脚本的时候遇到个问
题:动态库被拷贝到沙盒的时候会签名,而我们的strip操作发生在这个后面。在debug环境下,加载动态库的时候会提示签
名后动态库被修改的错误。而在release导出包的时候会重新对动态库进行签名。所以在release下不会有问题。最终,我
们修改了脚本,只在release环境下,执行strip操作:
if [ $CONFIGURATION == Release ]; then
strip -x dylib路径
fi
2. 无用类/方法
无用类
通过 otool 逆向Mach-O文件 __DATA.__objc_classlist段和__DATA.__objc_classrefs 段获取所有 OC 类和
被引用的类,两个集合差值为无用类集合,结合 nm -nm 得到地址和对应类名符号化无用类类名;
参考的过滤规则如下:
otool 逆向 __DATA.__objc_nlclslist 获取实现 load 方法的类过滤(RN与原生的桥接类、Swizzle Method 类);
通过 otool 逆向 __TEXT.__cstring 获取所有字符串常量,过滤通过 NSClassFromString 调用的;
子类实例化,父类没有实例化,父类不会出现在中 __objc_classrefs,通过 otool -oV 逆向出类的继承关系,过滤
出子类被实例化(NSClassFromString 调用),父类没有实例化(NSClassFromString 调用)的类;
过滤使用 Plist 文件引用的类;
无用方法
通过 otool 逆向 __DATA.__objc_selrefs 段获取使用到的方法,通过 otool -oV 获取实现的所有方法取差值。然
后过滤掉 setter、getter、系统方法和协议、自定义的协议、sel 调用。
3. 插件
在ipa包中我们也注意到了PlugIns目录,这里主要存放一些插件,比如today extension,share extension等,虽然这些插件在整个ipa包中的大小占比不大,但是我们还是决定梳理下有没有优化点。梳理后发现这些插件对于一些基础类库(网络框架,图片加载框架等)的使用都是以拷贝代码的方式加到工程中。我们知道这些类库完全可以和主app共享,因为主app中这些库是以动态库的形式使用的。经过优化后,成功的将today extension的大小减少了0.9M(嗯~蚊子虽小...)。
4. 冗余字符串
代码上定义的所有静态字符串都会记录在在可执行文件的__cstring段,如果项目里Log非常多,这个空间占用也是可观的,也有几百K的大小,可以考虑清理所有冗余的字符串。另外如果有特别长的字符串,建议抽离保存成静态文件,因为AppStore对可执行文件加密导致压缩率低,特别长的字符串抽离成静态资源文件后压缩率会比在可执行文件里高很多。
5. 类/方法名长度
观察linkmap可以发现每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的,原因还是Objective-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,Objective-c对象模型会把类名,方法名列表都保存下来。
可以考虑在编译前把所有类和方法名进行混淆,把长名字替换成短名字,这样做的好处除了缩小体积外,还对安全性有很大提升,别人拿到可执行文件对它class-dump出来的结果都是混淆后的类和方法名,就无法从类和方法名中猜出某个方法是做什么的,就难以挂钩子进行hack。不过这样有个缺点就是crash堆栈反解出来的堆栈方法名会是混淆后的,需要再加一层混淆->原名的转换,实现和使用成本有点高。
实际上这部分占用的长度比较小,中型项目也就几百K,对安全性要求高的情况可以试试。
参考文档: