App Store 官方规定 App 安装包如果超过 150MB,不可以使 OTA(over-the-air)环境下载,也就是只可以在 WiFi 环境下载。并且 App 包体积过大,对用户更新升级率也会有很大影响。
项目由于素材重复体积过大、无用的资源过多、冗余代码过多、重复造轮子等导致包体积过大,所以应用包的瘦身迫在眉睫。
瘦身方案
- 资源瘦身
- 基于编译后的瘦身
- 基于编译过程的瘦身
- 代码级别瘦身
资源瘦身
统一素材命名规范,去除重复、无用资源文件,解决名字重复问题;
获取资源文件
设置项目工程中的资源类型(jpg/gif/png/webp等)
正在匹配图片名(匹配编号规则),考虑到(image_%d类型)
集合取差集
删除无用图片(NSFileManager)
对于体积过大的资源可以将资源文件放在服务器上,按需下载。
基于编译后的瘦身
Link Map File
经过编译、链接,最终生成一个可执行文件。经过编译器编译会把每个类生成对应的 .o 文件(目标文件)。链接器会把 .o 文件和动态库链接在一起生成一个 mach-o 文件。Link Map File 就是这样一个记录 mach-o 文件格式相关信息的纯文本,里面记录了可执行文件的路径、CPU 架构、目标文件、符号等信息。
Xcode 开启编译选项 Write Link Map File
XCode -> Project -> Build Settings -> 搜 map -> 把 Write Link Map File选项设为 YES,并指定好 LinkMap 的存储位置。
LinkMap 文件格式
Path
Path路径记录的是这个 LinkMap 对应的安装包的地址。
Arch
Arch 指的是这个 LinkMap 对应的架构。
Object files
Object files 是编译后生成的文件列表,比如工程里面的 class 文件都编译成了.o文件,像我们比较熟悉的AppDelegate.o 文件等等。还有引进来的几个库,比如UIKit.tbd。第一列的序号是类的编号,通过该编号可以找到对应的类。
Sections
Section 是各种数据类型所在的内存空间,Section 主要分为两大类,__Text和__DATA。__Text指的是程序代码,__DATA指的是已经初始化的变量等。
Symbols
Symbols 这个单词肯定不陌生,什么 Crash 要有对应的符号表,连接器链接的时候经常找不到 Symbols 等。Symbols简单来说就是类名,变量名,方法名等等符号。
Dead Stripped Symbols
Mach-O
Mach-O 主要由以下三部分组成:
1、Mach-O 头部(Mach Header)。描述了 Mach-O 的 cpu 架构、文件类型以及加载命令等信息。
2、加载命令(load command)。紧跟在头部之后,这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的。在源码中有明显的注释来说明这些是动态连接器处理的。
3、Data。Data 中的每个段(segment)的数据都保存在这里,段的概念与ELF文件中段的概念类似。每个段都有一个或多个 Section,它们存放了具体的数据与代码,主要包含代码、数据,例如符号表,动态符号表等等。
Sections
每个 Section 包含了 Address、Size、Segment
1、__TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串),只读可执行。
2、__DATA 包含全局变量,静态变量等,可读写。
3、__LINKEDIT 包含了加载程序的元数据,比如函数的名称和地址,只读。
首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。
每一行的数据都紧跟在上一行后面,如第二行 __stubs 的地址 0x10192D49E 就是第一行 __text 的地址 0x1000037D0 加上 size 0x01929CCD,整个可执行文件大致数据分布就是这样。
Symbols
1、__text代码区
通过地址 0x1000037D0,可以知道它位于__TEXT段的__text区,这段区域存储着代码,通过符号表,根据地址可以对应出源代码,如 +[IMXEnterpriceViewModel creatEnterpriceViewModel]。通过第三列的类编号,可以知道该代码属于 IMXEnterpriceViewModel 类。
2、__objc_methname方法名区
通过地址 0x101B2D1D8,可以知道它位于__TEXT段的__objc_methname区,这段区域存储着方法名,通过符号表,根据地址可以对应出具体的方法名,如 localizedStringForKey:classBundle:。由上面的信息,可以看出方法名越长,最终占用的内存也越大。
3、__objc_classlist类列表区
__objc_classlist 区的 size 值都是8,区域里存储的值都是一个指针,指向了类的虚拟地址(__objc_data区中地址)。
objc_classdata 结构体中保存了类名,方法名,协议名,ivar 指针和属性对应的地址。通过地址,可以找到对应的段和分
查找未使用的类和方法
- 查找无用 selector
在 Objctive-C 中,由于它的动态性,它可以通过类名和方法名获取这个类和方法进行调用,所以编译器会把项目里所有 OC 源文件编进可执行文件里,哪怕该类和方法没有被使用到。
结合 LinkMap 文件的 __TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有 objc 类方法和实例方法(SelectorsAll)。再使用 otool 命令 otool -v -s __DATA __objc_selrefs 逆向 __DATA.__objc_selrefs 段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出 SelectorsAll 里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。
注意,系统 API 的 Protocol 可能被列入无用方法名单里,如 UITableViewDelegate 的方法,我们只需要对这些 Protocol 里的方法加入白名单过滤即可。- 查找无用 oc 类
查找无用 oc 类有两种方式,一种是类似于查找无用资源,通过搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]”等关键字在代码里是否出现。另一种是通过 otool 命令逆向 __DATA.__objc_classlist 段和 __DATA.__objc_classrefs 段来获取当前所有 oc 类和被引用的 oc 类,两个集合相减就是无用 oc 类。
分析大文件
在 Symbols 部分,可以把类编号相同的 size 加起来,算出每个类或库占用的大小。在 Object files 部分根据类的编号可以查出对应的类。
https://github.com/huanxsd/LinkMap
https://github.com/ming1016/SMCheckProject
基于编译过程的瘦身
- 编写 clang 插件,编译过程中将插件作为 clang 参数载入生成中间文件,通过编写工具分析所有的方法有哪些会被调用;
- 通过 clang 遍历语法树,获取嵌套访问关系,找出没有被调用的代码;
http://kangwang1988.github.io/tech/2016/11/01/validate-ios-api-using-clang-plugin.html
代码级瘦身
具体实施方案:
- 去除重复、无用资源文件,解决名字重复问题。
- 图片使用.xcassets管理
- 使用tinypng压缩PNG图片。视频可以通过 Final cut 等软件进行分辨率压缩。音频则降低码率即可。
- icon 使用 iconfont
- 非必须资源文件可以放到自己服务器上, 但必用资源文件**需要内置到安装包中。
- 尽可能的去除无用的代码、控制类名、方法名长度、冗余字符串
- 去掉 armv7 ,可执行文件以及库会减小,即本地 .ipa 也会减小
- Generate Debug Symbols(调试符号,debug下关掉)、Dead Code Stripping、Apple Clang - Code Generation、strip linked product等编译优化