前言
关于项目瘦身的相关知识,总体分为三个大类:
- 官方 App Thinning
- 图片资源优化
- 代码瘦身
一般我们接触的项目规模,只要按照规范的开发流程,定期review代码,是不太会涉及到项目瘦身的相关工作的。但是有些知识是我们必须要了解和掌握的。比如:
1 App Store 规定了安装包大小超过 150M 就不能使用蜂窝网络下载,一定要Wi-Fi环境才能下载。(之前这个规定是100M以内,所以微信的大小很长一段时间控制在99M,就是为了能够让用户可以使用蜂窝网络更新)
3 一个项目打包成 IPA ,其大小主要是由资源文件及代码组成,其中 Xcode 在打包和上传过程中本身就会对 IPA 做大小优化处理(官方 App Thinning),但是对于图片资源的压缩幅度很小。
下面展开讲讲图片资源和无用代码的优化方法,由于我没有在项目中切实执行过,所以仅当了解。
正文
一、官方 App Thinning
App Thinning 是苹果官方推出的一项改善 App 下载进程的技术,你可能第一次听到这个名字,但是在项目中我们基本都有使用。因为在操作上,它的大部分工作都是由 Xcode 和 App Store 默认完成的。
那它具体做了什么呢?
我们都知道,为了适配不同的设备和屏幕尺寸,我们的 App 需要包含多种芯片架构版本(32位、64位)和不同的分辨率图片资源(2x、3x)。这么多资源都集中在一个包中,势必会增大体积。
App Thinning 的作用就是将众多版本的文件及资源进行切分,用户下载时只会下载一个适合自己设备的版本。
除此之外,如果是游戏类的 App,它可以通过游戏关卡的进度控制资源的下载和删除,这样也可以控制初装 App 的包大小。
当然,它也会通过 Bitcode 针对特定的设备进行包大小优化,只是优化不明显。
我们只需要通过 Xcode 添加和管理 Assets.xcassets
文件即可,其他都不需要我们操作。
二、图片资源优化
之前也提到过,Xcode 打包的过程对图片资源的压缩是很有限的。(例如:我们在项目中使用了总大小为10M的图片资源,最终打包成 IPA 文件时,包含的图片资源总大小依然是 10M 左右)。所以对于图片资源优化空间的效果是立竿见影的,也是比较容易的。
对于图片资源的优化主要体现在 无用图片删除 和 图片资源压缩 这两方面:
1 无用图片删除
原文中介绍了两种方式,一种是获取项目中所有资源文件,通过正则筛选图片资源,过滤使用到的资源文件,取差集。另一种是使用开源工具 LSUnusedResources 。
1.1 自己写工具
具体步骤如下图所示,我认为难点在于如何取到代码中使用到的资源文件。这个过程我没有想到应该如何实现,如果大家有什么好办法可以私信给我^ ^。
1.2 使用开源工具
LSUnusedResources
2 图片资源压缩
处理完无用的图片资源后,我们还可以对正在使用的图片资源进行压缩来节省空间。
推荐的方式是将图片资源转成WebP。
2.1 图片转 WebP
转换需要借助工具,谷歌开源的 cwebp 或者 腾讯开发的 iSqarta。
工具1: cwebp
工具2: iSqarta
支持从png转webp,其他格式的图片需要先转成png再通过上述工具转成webp
2.2 使用 WebP
WebP-iOS-example
2.3 注意
WebP 在 CPU 性能和解码时间上比 PNG 高两倍,性能和体积的取舍。
2.4 建议
如果图片大小超过100kb时可以考虑使用webp。小雨100kb时,直接使用网页工具TinyPng或者下载工具ImageOptim进行图片压缩。这两个工具的压缩率没有 WebP 那么高,但不会改变图片的压缩方式,所以解析时对性能损耗也不会增加。
三、代码瘦身
可执行文件(Mach-O)的瘦身,就是找到并删除无用代码的过程
1 LinkMap 结合 Mach-O 找无用代码
1 找出方法和类的全集
2 找到使用过的方法和类
3 取二者的差集,就是无用代码
4 无用代码确认后进行删除
1.1 通过分析 LinkMap 来获得所有的代码类和方法的信息
获取 LinkMap 可以通过将 Build Setting 里的 Write Link Map File 设置为 YES,然后指定 Path to Link Map File 的路径即可。(如下图)LinkMap 文件分为三部分: Object File、Section 和 Symbols。
Object File 包含了代码工程的所有文件
Section 描述了代码段在生成 Mach-O 里的偏移位置和大小
Symbols 会列出每个方法、类、block,以及它们的大小
1.2 通过 Mach-O 获取使用过的方法和类
我们都知道 iOS 中方法的调用都会通过 objc_msgSend 来调用。而 objc_msgSend 在 Mach-O 文件中是通过 __objc_selrefs
这个 section 来获取 selector 参数的。所以 __objc_selrefs
中的 sel 是一定被调用过的方法。__objc_classrefs
中的 class 和 __objc_superrefs
中的 super 是一定被使用过的类。
Mach-O 文件的 __objc_selrefs、_objc_classrefs 和 _objc_superrefs 可以使用 MachOView 这个软件查看。
注意:这个方法的问题在于,Objective-C 是门动态语言,方法调用可以写成运行时动态调用,这样就无法收集全所有调用的方法和类。所以,这种方法找出的无用方法和类只能作为参考。
2 通过 AppCode 找出无用代码
如果工程量不是很大的话,可以直接使用 AppCode 来做分析。
方法: Code -> Inspect Code 静态分析
分析完成后,Unused code 里看到所有无用代码:
当然,使用 AppCode 静态分析出的无用代码也需要审查和处理,需要注意的问题很多,需要我们多次确认后才能删除。
3: 运行时检查类是否真正被使用过
通过 ObjC 的 runtime 源码,我们可以找到怎么判断类是否被初始化:
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 的 1<< 29 位 信息里。
// 类的方法列表已修复
#define RW_METHODIZED (1<<30)
// 类已经初始化了
#define RW_INITIALIZED (1<<29)
// 类在初始化过程中
#define RW_INITIALIZING (1<<28)
// class_rw_t->ro 是 class_ro_t 的堆副本
#define RW_COPIED_RO (1<<27)
// 类分配了内存,但没有注册
#define RW_CONSTRUCTING (1<<26)
// 类分配了内存也注册了
#define RW_CONSTRUCTED (1<<25)
// GC:class 有不安全的 finalize 方法
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)
// 类的 +load 被调用了
#define RW_LOADED (1<<23)
不得不说,flags 采用这种位方式记录信息的方式非常巧妙。这么多布尔类型统一管理,方便扩展和检索。值得我们多学习。
既然能够在运行中看到类是否被初始化,那么就能够找出那些类是没有初始化的,进而找到在真实环境中没有用到的类,并清理掉。
最后
对于戴铭老师留的问题:为什么苹果公司要设计元类?
这个问题真的很开放,开放到不知道从哪个角度去回答,千丝万缕又不知道从何讲起。感觉说来说去也只是把元类的作用贯穿一下。
如果大家有什么好的回答思路,可以一起讨论一下。