包体积优化 1 超过 200 MB 会默认请求用户下载许可 iOS 8 之前的版本主二进制 __TEXT 段的大小不能超过 60MB 移除无用图片资源 不适配 iPhone4 以下的机型,1x 的图片都可以移除 推荐使用工具 [LSUnusedResources]: 可以根据项目实际情况定义查找文件的正则表达式。另外建议勾选 Ignore similar name ,避免扫描出图片组 压缩图片等资源文件 无损压缩工具: [ImageOptim] 有损压缩: 使用 [Webp] 格式的图片 webp 只有 jpeg 格式的 1/3, 可以使用 [cwebp]进行格式压缩转换 删除重复文件 通过校验所有资源的 MD5,筛选出项目中的重复资源,推荐使用 [fdupes]工具 进行重复文件扫描,fdupes 是 Linux 平台的一个开源工具,由 C 语言编写 ,文 件比较顺序是`大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字 节对比`。 图片资源放入.xcassets 尽量将图片资源放入 Images.xcassets 中 最终运行设备进行 2x 和 3x 分发 高频图缓存更有效 Pod 库中的资源文件 在 Pod 库里面的 Resources 目录下新建 Asset Catalog 文件,命名为 Images.xcassets,移入所有图片文件,接着手动修改该 SDK 的 `podspec` 文 件指定使用该 Images.xcassets s.resource_bundles = { 'xxsdk' => ['PAX/Assets/*.xcassets'] } Pod 资源文件的引用方式分为 `resource_bundles` 和 `resources`,这里我 们使用的是 `resource_bundles`,会为为指定的资源打一个 `.bundle`, `.bundle`包含一个 `Assets.car`,获取图片的时候要严格指定 `bundle` 的位 置,能很好的隔离各个库资源包 大资源文件url下载 通过URL下载,并显示占位图 删除未使用代码 LinkMap 结合 Mach-O: LinkMap 的 Symbols 中会列出所有方法、类、block及它们的大小,通过获取 LinkMap 即可以获得方法和类的全集;再通过 MachOView 获得使用过的方法 和类,两者的差值就是我们要找寻的未使用代码 删除未使用类 https://github.com/yan998/FindClassUnRefs 也可以通过 Runtime 提供的 `isInitialized` 方法在运行时检测一个类是否有被 使用过 精简重复代码 多人开发的项目可能存在大量复制粘贴代码,可以通过 [PMD]扫描重复的代码片 段 : pmd cpd --files 扫描文件目录 --minimum-tokens 70 --language objectivec --encoding UTF-8 --format xml > repeat.xml 编译选项-修改(生产包) Valid Architectures 不支持32位以及 iOS8 ,可去掉 armv7及之前的架构 Strip Link Product = YES Deployment Postprocessing = YES ipa会去除掉symbol符号 Generate Debug Symbols = NO 去掉断点信息和符号化的调试信息 Enable C++ Exceptions 和 Enable Objective-C Exceptions = NO 不捕获 C++ 和 OC 的异常 编译选项-保持不变 - 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 标准库中获取符 号,从而减少应用体积 App Thinning 上传 App 时,配置 App Thinning 即可开启。分为三个部分: Slicing、 Bitcode、On-Demand Resources Slicing 2x 和 3x 图会分别放入到不同的变体中,用户从 App Store 下载时只下载特定 的变体 Bitcode 开启 需要保证 App 中所有的静态库和动态库都支持,否则会编译失败 App 的编译是在 Apple 服务器中进行 Organizer 中下载对应的 dYSM文件 On-Demand Resources 可将部分资源单独下载,而不随着 App 一同下载,减少初始 App 的大小 Framework 瘦身 未开启Bitcode 时,App 内的 Framework 会包含多个指令集,我们可以手动移 除不需要的指令集 Framework 也是 Mach-O 格式文件 正常只需要 armv7s 和 arm64 指令集就足够了 移除 Framework 的未使用代码 App Extension 用动态库替代静态库 用动态库会减小二进制文件的体积,但会增加启动时间,增加了运行时载入的过 程,并且依赖外部环境,需多方面考虑。 慎重引入第三方库 不要用引入功能重复的第三库 H5本地资源优化(只保留主页面) Resources:检查无用资源文件 图片资源:Assets.car/bundle/png/jpg 等 视频 / 音频资源:mp4/mp3 等 静态网页资源:html/css/js 等 视图资源:xib/storyboard 等 国际化资源:xxx.lproj 其他:文本 / 字体 / 证书 等 使用的大资源可以在线下载 图标优化 使用 tint color 精简单色图标; 使用图标字体(IconFont)替换单色图标; 将部分相似图标进行整合 Build Setting - Excluded Architectures 项设置排除的架构 理论上只保留 arm64 架构其实就够用了,可以去除 armv6 、 armv7 、 armv7s 三种架构 链接时优化 LTO(Link-Time Optimization) LTO 会降低编译链接的速度,所以建议在打正式包时开启 参数 LLVM_LTO=YES_THIN 包体积-二 完整总结 App Store 安装包大小 路径是App Store Connect->TestFlight->交付版本->构建版本元数据->App Store文件大小 包大小 构建ipa 上传的ipa包 多个架构的产物,多套尺寸的图片资源 包含: 静态库二进制文件(arm64、armv7)、动态库(arm64、armv7)、 asset.car(@2x、@3x)、其他资源文件 下载的ipa包 上传ipa包进行裁剪和二次分发,拆分架构,包变小 包含: 静态库二进制文件(单架构)、动态库(单架构)、asset.car(单尺寸)、其他资 源文件 拆分单架构 使用lipo工具拆分单架构 lipo "originalExecutable" -thin arm64 -output "arm64Executable" 拆分asset 使用assetutil工具拆分asset xcrun --sdk iphoneos assetutil --scale 3 --output "$targetFolder/ Assets3.car" "$sourceFolder/Assets.car" ipa组成 iOS工程结构 iOS工程由壳工程和Pod模块组成 壳工程 主Target、Apple插件Target Pod模块 源代码文件(OC、C、C++的.h和.m)、nib、bunlde、xcassets、多语言文 件、各种配置文件(plist、json) ipa包 iOS上传到App Store是IPA包,IPA包解压后是一个文件夹,内部由各种类型的文 件构成 主要包括: MachO可执行文件、.framework(动态库)、Assets.car、.appex(Apple插 件)、.strings(多语言)、.bundle、nib、json、png, 音频、视频 ...。 根据这些类别,分别分析其是否有优化空间 构建ipa包 主要是: 编译和文件拷贝 1.静态库里的源代码会被编译为MachO可执行文件, 2.xcassets文件夹会被转化为Assets.car, 3.其他都可以简单理解为文件拷贝。 分析Pod模块大小 静态库 1. 先解析linkmap数据,计算出Pod模块代码大小, 2. 再解析Pods-targetName-resource.sh的资源拷贝代码,计算出拷贝到Pod 模块的资源大小 动态库 1. 先使用lipo拆分动态库的二进制文件,计算出单架构的代码大小, 2. 然后再计算动态库framework内的资源文件,得到动态库的资源文件大小。 检测 无用代码检测 原理: 计算运行时Objc类覆盖率(系统自带覆盖率检测) 删除: 验证过后,可以下线掉无用的模块或代码文件 覆盖率: APP运行时,某1个Pod模块被加载的类数量除以所有类数量,可以称为这个模块 的Objc类覆盖率 核心技术是判断一个类是否被加载过: 类第一次被使用时会调用: +initialize方法 类被加载过后: cls->isInitialized会返回True 源码: // objc-class.mm Class class_initialize(Class cls, id inst) { if (!cls->isInitialized()) { initializeNonMetaClass (_class_getNonMetaClass(cls, inst)); } return cls; } // isInitialized方法读取了metaClass的data变量里的flags,如果flags里的第 29位为1,则返回True // objc-runtime.h #define RW_INITIALIZED (1<<29) bool isInitialized() { return getMeta()->data()->flags & RW_INITIALIZED; } 未使用图片资源 技术原理: 1.先解析出所有拷贝到构建产物的资源文件, 2.再解析出代码中实际引用到的资源文件, 3.两者的差集就是无用资源 获取全量资源 在Cocoapos工程中,“Pods-targetName-resource.sh”脚本负责拷贝Pod里的 文件资源到构建产物,包括所有文件类型bundle、xcassets、json、png。 解析上面脚本可以得到每个Pod模块都拷贝了哪些图片资源: // Pods-targetName-resource.sh install_resource "${PODS_ROOT}/APodName/APodName.framework/ APodName.bundle" install_resource "${PODS_ROOT}/BPodName/BPodName.framework/ BPodName.xcassets" install_resource "${PODS_ROOT}/BPodName/BPodName.framework/ xxx.png" 实际引用资源 位置: OC代码中引用资源文件都是以【字符串】字面量的形式声明, 构建后存放在Mach-O文件"__cstring" section。 计算: 1.利用strings解析framework的二进制文件就可以得到代码中所有的字符串声 明, 2.然后基于代码的各种引用方式匹 配“xxx”,"xxx@2x.png","xxx.png","xxx.bundle/xxx.png" 导出: strings executable | grep 'xxx' > cstrings.txt 未引用类- 编译时 ️ :不能检测动态创建的类 原理: iOS编译的产物是Mach-o格式的,分析.o文件检测。 1. 文件里__DATA __objc_classlist段记录了所有类的地址 2.文件里 __DATA __objc_classrefs 段记录了所有引用过的类的地址, 3.两者Differ可以得到未被引用类的地址。 4.然后将地址符号化,就可以得到未被引用类信息。 otool命令 otool -v -s __DATA __objc_classrefs xxxMainClient #读取__DATA Segment中section为__objc_classrefs的符号 otool -v -s __DATA __objc_classlist xxxMainClient #读取__DATA Segment中section为__objc_classlist的符号 nm -nm xxxMainClient 扫描未使用类的开源工具: https://github.com/xuezhulian/classunref 瘦身模块 ROI大全 瘦身 ROI 【投资回报率】高低:由上至下,左至右 总结: 组件治理: 0代码覆盖率 无用组件 重复功能组件 资源治理: 大资源 有损压缩 无用资源 重复图标 iconfont 多语言文案 编译优化: 精简编译产物oz C++导出必要符号 删除未引用的c/c++/Swift 代码 【无OC】 Asset Catalog Compiler Symbols Hidden by Default LTO:优化跨模块调用代码 剥离符号表 Strip Linked Product 代码治理: 运行时未加载类 编译时无调用类 业务重构 运行时下载: 资源ODR 多语言文案 动态库(iOS暂不支持) 瘦身收益 优化编译逻辑 非侵入(也存在设置编译选项导致适配问题) 风险和成本比较低 收益 中等 无用模块 无用业务 确认是下线的模块功能 风险和成本比较低 收益 高 无用代码 无用方法 无用类 风险&成本 高 无用类工具检测 收益 低 无用资源 风险 低 成本 中,使用检测工具 收益 高 下面详细描述细分收益内容: 1.组件瘦身 组件瘦身的ROI:类加载率0%的组件 > 无用功能组件 > 重复功能组件 类加载率0%的组件: 完全未使用的模块,下架前需要和产品确认业务,避免因低频或者AB测原因导致 统计错误 无用功能组件 功能代码有引用,但实际上并无业务使用, 或者是AB测已经下线的逻辑 或者是已经重构了的业务,不再使用了 重复功能组件 不同业务重复导入相同文件,比如图片选择器、缓存库、UI组件 2.资源瘦身 资源瘦身ROI: 大资源>有损压缩>重复资源>iconfont>多语言文案瘦身>无用资源 大资源 音视频资源压缩,收益高 可以的话,本地资源可以使用http下载缓存 有损压缩 图片等压缩 无用资源 检测无用资源并清理 重复图标 很多图片是相同的,只是名称不一样,这个可以使用hash值统计 类似的图片可以进行合并 ODR On-Demand Resource 表示按需下载资源 分三种: Initial install tags. 此种标签的资源,会随着App从App Store下载而下载,但是会影响App的ipa大 小,也就是说此种资源会包含在ipa内。 Prefetch tag order. 此种标签会在App下载后,开始下载相应的资源,下载是存在顺序的,后面会说 明。此种资源并不会影响ipa的大小,也就是说此种资源并不包含在ipa内。 Dowloaded only on demand. 此种标签下的资源,会在必要的时候,主动触发下载,这是我们开发者自己控制 下载时机的。 iconfont iconfont支持缩放、修改颜色,它size小,适合用于箭头、占位图等图标场景 减少包大小也能提高开发视觉体验的统一性 多语言文案 如果App支持20个语种,每个语种4000多条文案,文案总大小6MB,优化空间 3.编译优化 编译优化ROI: Optimization Level > 动态库复用主二进制静态库> 链接器产物压缩 精简编译产物Oz Optimization Level多个级别,-Oz比-O3的编译产物体积小10%左右。设置- Oz以后,XCode会优化连续的汇编指令,从而减少二进制大小,但副作用是执行 速度会变慢。C++工程建议都开启 LTO(OC/C++) LTO 是在LLVM链接时,优化跨模块调用代码 编译时间可能增加几倍,甚至几十倍,建议只在Release包开启 优点: ●将一些函数內联化 ●去除了一些无用代码 ●对程序有全局的优化作用 缺点: ●降低编译链接速度,只建议在打正式包时开启 ●降低 link map 可读性(出现XX-lto.thin的类) 动态库复用 二进制静态库 C++动态库经常用到一些基础库比如openssl、libyuv、libcurl,他们一般是静 态库。如果动态库引用了静态库,它编译时默认会内嵌静态库的所有符号。 一些基础静态库 甚至被多个动态库引用,导致重复引入 最佳解决方案是共享符号表,让动态库可以调用主二进制的基础库符号,从而可 以去掉内置的静态库。只要修改XCode的Link配置,无需额外的代码开发 动态库工程设置: 1、设置当遇到未定义的函数时,动态查找APP主二进制符号表。 2、关闭bitcode 3导出动态库需要调用的外部符号,写到一个文件exported_symbols内 链接器产物压缩 iOS工程构建产物是MachO文件,MachO文件中的TEXT段存放了各种只读的数 据段,__cstring段存放了普通的C String,__objc_methtype和 __objc_methname存放了Objc的方法签名和方法名。比入Objc代码中声明的 @"Hello world",底层会产生一个CFString,构建后存放在__cstring中。这些数 据很占空间,一般工程至少会有10MB以上,压缩的收益很可观。我们上线后, App Store安装包大小从191MB优化到174MB,减少了16MB 技术原理: 链接时将TEXT段数据移到__DATA段并压缩,运行时先执行解压代码,解压 TEXT段数据存到自定义段中,将代码中对字符串的引用的地址修正为解压后的自 定义段。 剥离符号表: Strip Linked Product Debug环境不要设置YES,否则调试时看不到符号 主工程Release Deployment Postprocessing :YES Strip Linked Product :YES Strip Style :All Symbols(剥离所有符号表和重定向信息) Framework工程 Deployment Postprocessing :YES Strip Linked Product :YES Strip Style :Non-Global Symbols(剥离包括调试信息等非全局的符号,保留外 部符号) 说明: 1、静态库不能将Strip Style 设置为All Symbols,因为剥离了所有符号的静态库 是无法被正常链接的 2、去除符号不影响 dSYM 文件中的符号信息,查看崩溃日志时,可以从 dSYM 文件中找对应符号 Symbols Hidden by Default 主工程Release Symbols Hidden by Default :Yes Framework工程 静态库/动态库 Symbols Hidden by Default :NO 库设置为NO,否则会有链接错误 剔除未引用的C/C++/Swift代码: Dead Code Stripping 此设置对 对动态语言OC无效。 主工程开启设置: Dead Code Stripping :Yes Asset Catalog Compiler 主工程设置: Asset Catalog Compiler->Optimization设置为space Optimization有三个选项,空、time和Space,选择Space可以优化包大小 C++只导出必要符号: Symbol Visibility C++的静态库和动态库都只导出必须的符号,默认设置为隐藏所有符号,然后用 Visibility Attributes单独控制需要导出的符号 默认隐藏所有C++符号 Other C++ Flags->添加-fvisibility=hidden 设置需要导出的符号 __attribute__((visibility("default"))) void MyFunction1() {} __attribute__((visibility("default"))) void MyFunction2() {} .... 4.代码下线 根据Objc类覆盖率统计的结果,可以逐步下线掉未被使用的类。代码文件的量级 比较小,下线代码需要仔细确认,避免引起功能问题或crash,ROI比较低。 5.Flutter专项 Flutter是单独构建的,引入的内容包括Flutter引擎产物、Flutter业务代码产物。 APP如果引入Flutter,需要对Flutter进行专项瘦身。 精简编译产物Oz: Optimization Level -Oz选项相比Os,收益预估11%,但首屏性能1%~9%的损耗 Dart符号剔除和混淆 可以通过--dwarf-stack-trace选项去除Dart标准的调试符号。通过-- obfuscate 选项,可以将较长的符号替换为短符号,副作用是符号会被混淆 Flutter icon摇树优化 --tree-shake-icons 去除NOTICES文件 防劣化机制 review 代码需要一套机制确保包不要出现过度增长,过大对资源需要审核必要性