背景
早些年的iPhone在非WiFi模式下下载APP限制为几十MB,超过这个大小就强制必须使用WiFi下载,虽然苹果官方这些年一直在提高这个大小限制,如今的iOS 13下载超过200MB的安装包时会默认弹框请求用户是否继续下载,如果APP的安装包体积更小,则可以提高整体更新率,减少用户等待时间,更快的触达用户,因此安装包瘦身是APP优化中的重要一环。
资源瘦身
大资源文件通过运行下载
对于一些非必要的大资源文件,例如字体库
、换肤资源
、静态的H5样式
等等,可以在 APP 启动后通过异步下载到本地,而不用直接放在 ipa 包内。
图片资源放入xcassets
尽量将图片资源放入Images.xcassets
中,包括 pod
库的图片。 Images.xcassets
中的图片加载后会有缓存,提升加载速度,并且在最终打包时会自动进行压缩(Compress PNG Files),再根据最终运行设备进行 2x 和 3x
分发。
对于内部 Pod
库中的资源文件,我们可以在 Pod
库里面的 Resources
目录下新建 Asset Catalog
文件,命名为 Images.xcassets
,移入所有图片文件,接着手动修改该 SDK 的 podspec 文件指定使用该 Images.xcassets
s.resource_bundles = {
'xxsdk' => ['PAX/Assets/*.xcassets']
}
NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/PAX.bundle"];
NSBundle *resource_bundle = [NSBundle bundleWithPath:bundlePath];
UIImage *image = [UIImage imageNamed:@"xxxx" inBundle:resource_bundle compatibleWithTraitCollection:nil];
删除重复文件
通过校验所有资源的 MD5
,筛选出项目中的重复资源,推荐使用 fdupes
工具进行重复文件扫描,fdupes
是 Linux 平台的一个开源工具,由 C 语言编写 ,文件比较顺序是大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字节对比
通过 Homebrew 安装 fdupes:
brew install fdupes
查看目标文件夹下的重复文件:
fdupes -Sr 文件夹 // 查看文件夹下所有子目录中的重复文件及大小
fdupes -Sr 文件夹 > 输出地址.txt // 将信息输出到txt文件中
4474 bytes each:
Test/Images.xcassets/TabBarImage/tabBar_2.imageset/tabBar_2@2x.png
Test/Resource/TabBarImage/tabBar_2@2x.png
3912 bytes each:
Test/Images.xcassets/TabBarImage/tabBar_3.imageset/tabBar_3@2x.png
Test/Resource/TabBarImage/tabBar_3@2x.png
资源文件压缩
图片等资源建议使用无损压缩,建议和公司的设计沟通已确保图片保持在一个合理的大小区间
还可以使用WebP
格式的图片,Webp 是由 Google 推出的图片格式,有损压缩模式下图片体积只有 jpeg 格式的 1/3,无损压缩也能减小 1/4,目前SDWebImage
的扩展都已支持对该格式图片的加载
移除无用资源
通过资源关键字
进行全局匹配,筛选出未使用的资源。有些资源使用是通过拼接或者后台下发名称,需对筛选出的资源进行确认,以防止误删
代码瘦身
删除未使用的代码
删除未使用的类、方法可以有效的减少代码段的大小,从而减少包体积。但人为的去筛选未使用的方法太过于耗时耗力,可以通过LinkMap
和Mach-O
相互结合去排查未使用的代码。
运行时Objc类覆盖率
如果能知道App运行时有哪些类被使用过,就可以下线掉无用的模块或代码文件,Objc类覆盖率指标可以帮到我们。
APP运行时,某个Pod模块被加载的类数量除以所有类数量,可以称为这个模块的Objc类覆盖率。核心技术是判断一个类是否被加载过,下面介绍一个经过线上验证的轻量级方案。ObjC的类第一次被使用时会调用+initialize方法,类被加载过后cls->isInitialized会返回True。isInitialized方法读取了metaClass的data变量里的flags,如果flags里的第29位为1,则返回True。
// objc-class.mm
Class class_initialize(Class cls, id inst) {
if (!cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
return cls;
}
// objc-runtime.h
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
/*
这个方法我们是无法直接调用的,它是 OC 的方法。但是,要知道类的元数据结构是不会变的,所以我们可以通过自己模拟构建类的元数据结构来获取 RW_INITIALIZED 标记位数据,从而来确定某个类是否已经初始化,代码如下:
*/
- (BOOL)isUsedClass:(NSString *)cls {
Class metaCls = objc_getMetaClass(cls.UTF8String);
if (metaCls) {
uint64_t *bits = (__bridge void *)metaCls + 32;
uint32_t *data = (uint32_t *)(*bits & FAST_DATA_MASK);
if ((*data & RW_INITIALIZED) > 0) {
return YES;
}
}
return NO;
}
删除版本遗留代码
随着业务的迭代,有些时候可能会产生一些兼容性等遗留下来的代码,这些代码到了某个版本、时间点不再继续运行的时候,可以删除掉,从而节省大小
精简重复代码
多人开发协作时,可能会存在大量的功能性相同、或者是直接复制粘贴的代码,对于这种代码,可以从架构层次去解决,尽可能的封装为一个公共方法。
其他
慎用引入第三方库
- 不要引用重复的三方库,如JSONModel和MJExtension
- 要考虑引入三方库的必要性,不能够仅仅是只有一个地方用到了某个功能,从而引入一个三方库
编译选项
-
Valid Architectures 设置编译生成的 ipa 包所支持的架构,不支持32位以及 iOS8 ,可去掉 armv7及之前的架构
-
Strip Link Product 和 Deployment Postprocessing Strip Linked Product 默认为 Yes,Deployment Postprocessing 默认为 No,Strip Linked Product 在 Deployment Postprocessing 设置为 YES 的时候才生效。当Strip Linked Product设为YES的时候,ipa会去除掉symbol符号,运行 App 断点不会中断,在程序中打印[NSThread callStackSymbols]也无法看到类名和方法名。而在程序崩溃时,终端的函数调用栈中也无法看到类名和方法名。但是不会影响正常的崩溃日志生成和解析,依然可以通过符号表来解析崩溃日志,适合线上使用,建议在 release 下都设置为 Yes
-
Generate Debug Symbols 默认为 Yes,当设置为 Yes 时,编译生成的 .o 文件会更大,包含了断点信息和符号化的调试信息,方便开发阶段调试,建议在 release 下设置为 No,线上需要获取崩溃信息时搭配编译生成的 dSYM 文件解析符号。
-
Enable C++ Exceptions 和 Enable Objective-C Exceptions 默认都为 Yes,用于捕获 C++ 和 OC 的异常,如果项目中使用了 try catch, 可考虑去掉并在 release 下设置为 No,配合在 Other C Flags 添加 -fno-exceptions 和 -fno-rtt ,会有比较明显的体积减小
-
Generate Debug Symbols 默认为 Yes,用生成dSYM文件,有助于解析崩溃信息。
-
Make Strings Read-Only 默认为 Yes,复用字符串字面量。
-
Dead Code Stripping 默认为 Yes,去除冗余代码。
-
Optimization Level Release 下默认为 Fastest, Smalllest[-Os],自动优化代码。
-
Symbols Hidden by Default Release 下默认为 Yes,会移除符号信息,把所有符号都定义成 private extern。
-
Strip Swift Symbols 默认为 Yes,移除 Swift 相关的符号表,运行时再从 SWIFT 标准库中获取符号,从而减少应用体积。
本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.