背景
随着需求爆发,代码和图片资源越来越多,包体积越来越大,用户下载成本越来越高,瘦包迫在眉睫,要想瘦包,就需要知道包由何组成,每个组成部分又是怎么来的,这就必须了解编译过程,当然有人会说,不就瘦包嘛,网上有教程呀,巴拉巴拉照着做就行了!
嗯~那证明app的包还不够复杂,当你需要引用上百个自研或是开源库的时候自会明白,我们来看下文件组成
iOS应用的文件目录结构
一个app通常有如下几个模块组成:源码编译后的二进制(静态库包含在其中)、动态库、bundle等资源文件、plist等配置文件、代码签名文件CodeResources
源码编译后的二进制
静态库是单独编译的,在源码文件编译完成后会由静态链接器一起打包生成最终的Mach-O格式的二进制文件,这就意味着多份相同的静态库是没有办法链接通过的,有一种办法可以解决这个问题,就是同一份静态库包装到不同的动态库中以形成隔离,因为动态库在app编译完成后不会一起进行静态链接并且符号表也是与app分开的,在集成三方库的时候可能会遇到这个问题动态库
动态库也是单独编译的,我们程序中用到的所有系统级别的framework都是动态库,它们集成在操作系统中,不会占用app的存储空间,自研的动态库是集成在app目录下的Frameworks中,编译时,动态库的物理路径需要配置到app的编译设置的搜索路径,在App编译的时候就会被拷贝到app的压缩包里的Frameworks文件夹下,符号表单独保存并且需要上传到苹果的后台bundle
程序的图片资源文件plist配置文件
程序的标识符以及系统服务的访问权限等都配置在里面代码签名
就是存储通过证书以及加密算法生成签名密钥的文件
可执行文件的物理结构
可执行文件包含了机器指令代码、数据、符号表、调试信息等,目标文件以段的形式存储以上信息,目标文件通常包括:文件头、text段、data段、bss段
文件头
描述整个文件的属性(包含目标机器架构、是动态链接还是静态链接以及是否是可执行文件)和一个段表
段表就是存储段的数组,描述了各个段在文件中的偏移量以及段本身的属性text段
源代码编译后的机器指令被存放在text段,又叫做代码段data段
初始化的全局变量和静态变量被放在data段,又叫做数据段bss段
未初始化的全局变量和静态变量被放在bss段,和data段一起被称为数据段
符号表
符号通常指函数和变量(还有其他符号比如段名、行号信息等),编译的最后一个阶段静态链接,就是符号的处理过程,每个目标文件都有一个符号表,记录了目标文件里面所有的符号以及对应的符号值,对于函数和变量来讲符号值就是他们的地址,符号表在程序编译之初就创建,在整个编译过程中都起着至关重要的作用,在静态链接完成之后会生成最终的符号表,动态库由于不参与主可执行文件的静态链接过程,所以符号表是抽离的
调试信息
编译器支持源代码级别的调试,debug环境下可以在程序中打断点调试,release环境下编译器会过滤掉调试信息,即编译后的二进制里面没有调试代码,因此断点是无效的
可执行程序二进制(包含了静态库)、动态库都是编译器的产物,那么为什么需要编译呢,源码不可以直接执行吗
Object-C为什么需要编译
- 解释型语言
解释型语言运行时实时被解释器解析为机器码并且执行一次就需要解析一次,脚本语言如JavaScript等都是解释型语言,优点是省去了编译过程,但是运行效率低,虽然当代浏览器解释器经过深度优化,但是相比直接运行可执行二进制来讲也会慢很多 - 编译型语言
有一个复杂又耗时的编译过程,最终生成的二进制机器码,可以被处理器直接识别并执行,相比解释性语言来说运行时效率高了很多
Object-C是一门编译型语言,底层通过c、c++、汇编实现,上层架设了一层语法糖,配合强大的运行时库使用
编译原理
一句话概括:从源码生成二进制机器码的过程就是编译过程,整个过程分为四个阶段:预编译、编译、汇编、静态链接
- 预编译:
进行宏替换、头文件包含、条件编译识别等工作 - 词法分析:
将源代码的字符序列分割成一些列的记号,包含关键字、标识符、字面量、符号,同时将标识符存放到符号表,常量存放到文字表以备后续使用 - 语法分析:
把上一步产生的记号解析成一个以表达式为节点的抽象语法树(AST),这个阶段会进行表达式合法性校验,比如缺少操作符、括弧不匹配等编译器会直接给出错误提示,当然xcode在没有bulid之前也会进行语法检测,这也是衡量一款开发工具是否合格的基本要求 - 语义分析:
编译器只能分析静态语义,即编译期间就可以确定的语义,通常包括类型匹配、转换,这个阶段编译器会给出类型不匹配等警告,不会报错,因为运行时基本类型不匹配实际会进行精度取舍,指针类型不匹配会以指针所指向的真实对象去调用方法,这些都是动态语义,不在编译器的管理范畴,当然也没有这个能力 - 中间代码生成:
由于直接在语法树上做优化比较困难,编译器会将整个语法树转换成中间代码,它是语法树的顺序表示,至此已经生成接近目标代码的汇编代码,只是还与目标机器的运行时环境无关,比如机器字长、变量地址、寄存器名称等,因此中间代码是编译前后端的分割线,前端负责产生中间代码,后端负责将其转换为目标代码,这也使得编译器跨平台成为可能 - 目标代码生成与优化:
目标代码生成器会根据目标机器的运行环境将中间代码转换为目标代码,目标代码优化器会对汇编码进行优化,比如循环优化、多余指令删除、寻址优化、用位移操作代替乘法运算等 - 汇编:
汇编器是编译后端的后端,负责将汇编码转化为机器码 - 静态链接:
编译器会把源代码编译成一个一个的目标文件.o文件,定义在A.o文件中的变量及函数在B.o文件中是无法得到地址的,编译器会用0填充,待到链接的时候由链接器进行修正,这个过程叫做重定位,最终静态链接器把编译产生的所有.o文件和静态库一起打包生成一个Mach-O格式的二进制文件
减小包体积
我们以目标为导向,瘦包需要瘦哪些模块:可执行二进制文件、动态库、Bundle资源
- 第一招:✔️
前面优化图片资源会带来很多惊喜,扫描无用资源文件直接删除和压缩Bundle中的图片,可能分分钟降下来几十兆,一期目标直接达成,接下来就要对代码部分动刀了 - 第二招:❌
经过上面的编译原理我们知道不同架构的目标机器编译出来的目标代码是不一样的,iOS的app是一个胖架构包,可以包含很多架构在里面,打包送上应用商店后苹果会对包进行拆分,不同架构的手机下载对应架构的包,因此减少app兼容的架构是不能够减小包体积的 - 第三招:✔️❌参半
扫描删减无用代码,合并类似功能的冗余代码,这招对于源文件、动态库都会奏效,对于静态库来讲未必奏效,因为静态库在链接的时候只有用到的部分才会打包到可执行二进制 - 第四招:✔️❌需要根据包本身的体积衡量
多个动态库引用了相同一份静态库会分别在动态库中链接到使用的部分,换句话说静态库被动态库使用的部分会打包到动态库中,因此多个动态库就打包了多份静态库(当然只有使用到的部分),这里可以做个优化把静态库变成动态库,这样就是所有动态库引用一份动态库,不会出现多份冗余的情况,当然要考虑到库包本身的大小,如果太大做成动态库反而会增大包体积,因为动态库是全量拷贝到app包内的