目录
一:摘要
二:安装包组成
三:系统优化
四:资源优化
五:可执行文件优化
六:编译器优化
七:拓展
一,摘要
众所周知苹果对iOS App大小有所限制,如果大于200MB使用蜂窝网络下载需要请求许可,对于流量和手机储存敏感的用户很不友好,包的大小甚至影响新用户的转化,所以对于安装包的瘦身是App发展和优化过程中不可避的问题。
二,安装包的组成
1,介绍
iOS安装包就是ipa
,ipa
是一个压缩包,用户下载的就是这个压缩包,下载完成就会自动解压,解压的过程也就是通常所说的安装过程,把ipa
文件后缀改为zip
,然后解压出来再payload中的.app
显示包内容就可以查看里面的资源文件。
2,.app里面的主要内容
-
_CodeSignature
:存放文件的hash
列表。里面有一个文件CodeResources
,这个文件是一个属性列表,包含bundle
中所有其他文件的列表。这个属性列表只有一项files
,这是一个字典,键是文件名,值通常是Base64
格式的散列值。如果键表示的文件是可选的,那么值本身也是一个字典,这个字典有一个hash
键和一个optional
键(布尔值true
)。它的作用是用来判断一个应用程序是否完好无损,能够防止不小心修改或损坏资源文件。
一些
bundle
文件:bundle
是一种标准化的层次结构,保存了可执行代码以及代码所需要的资源。bundle
文件可以理解为一个资源包,用于储存图片,音频,文本,nib
文件等,方便在其他项目中引用包内资源。bundle
包是静态的,不参与编译,也就意味着bundle
包中不能包含可执行文件,它仅仅作为资源被解析成为特定的二进制数据。从.app
包可以了解到,bundle
文件基本都是一些SDK
制作的。Assets.car
: 把放在Assets.xcassets
中的图片(Applcon
和LaunchImage
这两种图片是直接放在包中的)打包后压缩成一个Assets.car
的文件,减小包大小。plist
: 一些属性列表文件png
、jpg
、mp4
、json
等资源文件
三,系统优化(App Thinning)
1,简介
App Thinning
是App Store和操作系统在安装iOS或者watchOS的app的时候通过一系列优化似的app以最小的合适的大小被安装到你的设备上。
2,App Thinning有三种方式
-
App Slicing
:会在你向iTunes Connect 上传App后,对App做切割,创建不同的变体,这样就可以适用到不同的设备。 -
Bitcode
:针对特定设备进行包大小优化(优化不是很明显)。 -
On-Demand Resources
,主要是为游戏多关卡场景服务的,它会根据用户的关卡进度下载随后几个关卡的资源,并且已经过关的资源会被删除,这样就可以减少初装App的包大小。
3,使用
- 这里大部分工作其实都是由Xcode和App Store来帮你完成的,我们只需要通过Xcode添加
xcassets
目录,然后将图片添加进来即可,新建一个文件选择 Asset Catalog模板,如图所示:
-
On-Demand Resources
就不多说了,提一点, 在iOS9以后Xcode默认开启了,可以在下图位置进行设置。
四,资源优化
1,简介
资源优化主要分两部分,一是资源做无损的压缩,二是删除无用的资源,比如图片、音频、视频、配置文件等。
2,图片压缩
- 如果图片资源小于
100k
,使用网页工具 Tinyjpg 或者GUI工具 ImageOptim; - 如果图片资源大于
100k
,转换图片格式WebP
,转换WebP
图片格式工具推荐 iSparta,也可以使用谷歌自己提供的图片压缩工具cwebp
; - 需要注意的是WebP在cpu消耗和解码时间上会比png高两倍,所以有些需要自己综合考虑权衡利弊,不要为了优化而优化。
3,无用资源删除
- 推荐使用 LSUnusedResources 工具,很方便(需要二次确认,比如如果你把资源名放入
plist
文件中,它也会认为没有使用)。
五,可执行文件优化
1,简介
开头已经讲过App安装包主要由资源文件和可执行文件(Mach-O
)组成,可执行文件的大小由代码量决定,所以要想对可执行文件瘦身,首先应该做的就是找到并删除无用的代码。
2,Link Map文件分析
Xcode build产生的Link Map
文件能比较直观的反映出程序各部分的文件大小情况,对于减少包体积很有帮助。
- 获取 LinkMap :将 Build Setting 里的
Write Link Map File
设置为Yes
,然后指定 Path to Link Map File 的路径就可以得到每次编译后的LinkMap
文件了。我们只修改一下生成的 Link Map文件的路径就可以了,后缀名不要修改。
- linkMap 分为三部分
-
Object files
:代码工程中所有文件编译后的目标文件.o;
-
Section
: 描述代码段在生成的Mach-O
里面的偏移位置和大小,包括代码段(__TEXT
)和数据段(__DATA
)的分布情况。
-
Symbols
:符号相关信息,列出了每个方法、类、block以及它们的大小,第一列Address
是在文件中偏移的位置,第二列size是大小,第三列File是对应上面Object files 中的文件编号,第四列Name
是文件名。如下图与上图Object files
是对应的。
通过对LinkMap的分析不但可以统计出所有的方法和类,还能清晰的看到代码所占包大小的具体分布,从而有针对性的对代码进行优化。(对Symbols分析还可以通过方法的二进制重排来提成App冷启动速度
)。
3,Mach-O文件分析
Mach-O
是Mach Object
的缩写,是Mac/iOS
上用于储存程序、库的标准格式。一个简单的方法获得该文件->Xcode编译结束会生成一个可执行程序,查找方法如下图:
在这个文件夹下找到对应的项目文件名/Build/Products/Debug进入目录就可找到可执行文件了。
iOS的方法都是通过objc_msgSend来调用的,而objc——msgSend在Mach-O文件里是通过__objc_selrefs
这个section来获取selector这个参数的,所以__objc_selrefs
里是被调用的方法,__objc_classrefs
里是被调用过的类,__objc_superrefs
是被调用过的super的类,这样就能找出使用过的类和子类,然后对比Link Map文件就可以找出没有用到的类和方法了(OC是动态语言,方法可以在运行时调用,所以该方法找出的无用代码需要我们二次确认才可删除
)。
4,使用AppCode
以上可以看到,工作量还是不小的,推荐一个神器 AppCode ,AppCode通过静态分析可以快速的帮我们找到没用的类以及方法等,使用起来也特别简单,在菜单栏选择 code ->Inspect Code就可以了。
分析结果包括:
- Not implemented methods:没有实现的方法;
- Key value coding:KVC相关,比如使用KVC访问了@private修饰的成员变量;
- Unused import statement:无用类引入声明;
- Unused instance variable :无用的实例变量;
- Unused method:没有用到的方法;
- Unused class:没有用到的类;
- Unused property :没有用到的属性;
- Unused parameter :无用参数;
- Unused local variable :无用的局部变量;
- Unused value :无用的值;
- Unused macro :无用的宏;
- Unused global declaration :无用全局声明。
看似AppCode
已经把所有工作都做完了,其实不然。下面我们再来列举下AppCode静态检测的问题:
- 引入的第三方库,比如
JsonModel
里面定义了未使用的协议会被认为是无用的协议; - 如果子类使用了父类的方法,父类的这个方法不会被认为使用了;
- 通过点的方式使用属性,该属性会被认为没有使用;
- 使用
performSelector
方式调用的方法也检测不出来,会被判断为为使用; - 运行时声明类的情况检查不出来。比如使用
NSClassFromString
方式调用的类会被检查出来没有使用; -
[ [self class] loadData]
这样不指定类名的方式使用的类也会被检测出来是未使用的类,以及UITableView的自定义Cell使用registerClass
,这样的情况也会被认为这个Cell为使用;
所以:AppCode检查完成之后,还是需要我们二次确认才能够删除。
6,编译器优化
Xcode 支持编译器层面的一些优化选项,这些优化选项可以在更快的编译速度、更小的二进制和更快的执行速度之间根据实际需要选择合适的方案。
- Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES(
新版Xcode默认打开
); - 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO, Other C Flags 添加 -fno-exceptions
(Other C Flags 添加 -fno-exceptions 一般只有对程序运行效率及资源占用比较看重的场合才会使用, 如果要做的话最好连libstdc++和其他所有的的c++库都用这两个参数重新编译一遍, 否则只是你自己的程序禁用了这两个特性, 而别的库依然开着, 效果就大打折扣了);
- Strip Debug Symbols During Copy 设置为YES。这个是将那些拷贝进项目包的第三方库、资源或者Extension的Debug Symbol去除掉
(只需在Release模式开启即可,因为开启后不能对第三方库进行断点调试以及符号化);
- Build Settings -> Optimization Level有几个编译优化选项,release版应该选择Fastest, Smalllest,这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小
(新版Xocde默认已经打开);
- Enable BitCode设置为YES;
7,拓展
- 上面这些工作做完相信App已经很
"洁净"
了,"瘦身"
工作基本完成了,特别对于迭代很久的老项目会有很大的成效。 - 对项目进行Archive后会生成
.xcarchive
文件,该文件中包含了App
、dsYMS
以及其他信息,将.xcarchive
文件上传到 App Store Connect后,苹果会对App中的可执行文件进行DRM加密,然后将App压缩成ipa文件并发布到App Store。加密对可执行文件的大小影响不大,但是它会严重影响可执行文件的压缩效率,导致压缩后的ipa大小增加,也就是我们的安装包增大。 - 这种加密使用脱壳工具很容易进行解密。Mach-O文件代码的解密发生在Mach-O文件被加载的时候,由Mach Loader进行。Mach Loader 会读取 Mach-O 中的
LC_ENCRYPTION_INFO
这条Load Command
来判断可执行文件是否加密。 - 所以可以通过
otool -l
的命令来查看 Mach-O 是否被加密过。
其中 cryptoff
表示加密字段位于文件中偏移 16384
个字节;cryptsize
表示加密内容长度 16515072
字节;cryptid
表示加密方法为 1
,如果为 0
表示不加密(平时打包出来的验证可以发现都为0
,那时因为加密是在上传App Store时才做的,怎么获取线上ipa
呢,可以参考我另外一篇文章->ipa包获)。
查看 LC_SEGMENT_64
中 __TEXT
段的范围:
根据上面的结果可以算出加密内容实际上都位于 __TEXT
中,也就是说苹果只会对Mach-O文件中的 __TEXT
段加密,而不会对其他段加密,也就是说,只要能把 __TEXT
段中的节移到其他段,就能减少加密范围,从而使压缩效率提升,从而减小下载包大小。
一般来讲,在App中可执行文件占80%的大小,而加密部分可占执行文件的 70%左右,加密会影响 60%左右的压缩率,因此移走该加密部分会提升 34%左右的下载大小(需要注意的是,iOS13已经对下载大小做了优化,所以该方案无法再对iOS13及以上的设备下载大小再做进一步优化
)。
需要注意的是,__TEXT
段中所有节都移走对于小型App没有任何问题,但在较大类型的App,还需要注意 Crash
和链接失效
问题。
文章到这里暂告一段落,如果有什么问题或者不足欢迎指正,如果对你有帮助,记得点赞收藏,谢谢!