包体积
包体积的影响
包体积优化的必要性
- 下载转化率下降:每增加6M,应用下载转化率下降1%。
- App Store限制:超过200MB的包,iOS 13以下用户无法通过蜂窝数据下载,iOS 13及以上用户需手动设置。
- 磁盘占用:大包体积占用更多存储空间,影响低存储用户。
- 用户下载意愿:大包体积减少用户下载意愿,尤其在蜂窝数据低数据模式下。
- 性能影响:包体积大增加启动时间和SIGKILL风险,降低基础体验。
技术方案
- 资源优化:优化大块资源、无用配置文件和重复资源。
- 工程架构优化:建立体积检测流水线,控制体积增长。
- 图片优化:无用图片优化、Asset Catalog优化、HEIC和WebP压缩优化、TinyPng压缩。
- 编译器优化:使用LLVM编译选项,进行OC、C++、Swift等语言的编译优化。
- 代码优化:无用类、方法、模块瘦身,精简重复代码,AB实验固化。
包体积-系统优化
- App Thinning:利用Apple提供的App Thinning功能,根据用户的设备自动下载适合该设备的资源包,有助于减少初装包的大小。
- 按需下载资源:使用On-Demand Resources来按需下载资源,只下载用户实际需要的部分,从而减小初始安装包的大小。
包的分析
安装包生成过程与指标
- 生成过程:ipa包上传AppStore后,App Thinning针对不同设备生成不同编译产物。
- 安装包与下载包大小:安装包大小是用户安装后占用的磁盘空间,下载包大小是用户下载时的压缩包大小。
- 衡量标准:iOS端通常将安装包体积作为衡量标准。
iOS端安装包组成部分
- Mach-O文件:iOS系统上的可执行文件。
- Watch APP:带有小组件功能的WatchApp。
- 自定义动态库:动态库推迟到运行时加载,节省代码段空间。
- Swift系统库:高版本iOS系统自带,低版本需iPA包中自带。
- Assets资源:Assets.car文件,包含图片资源。
- 根目录下图片资源:直接添加进工程的图片文件。
- bundle资源:管理图片和其他配置文件。
- 其他配置文件:如plist、js、css、json等。
Mach-O文件
Mach-O是Mach Object文件格式的缩写,用于记录Mac及iOS系统上的可执行文件、目标代码、动态库和内存转储。使用MachOView和otool命令查看Mach-O文件信息,以及通过file和lipo命令查看文件格式和架构。Mach-O文件有Header、LoadCommands和Data部分,特别是LoadCommands中的关键cmd类型如LCSEGMENT64,及其段(__PAGEZERO、__TEXT、__DATA、__LINKEDIT)。
开源工具
5GUIs
5GUIs 是一款基于 SwiftUI 开发的 macOS 应用程序,它具备检测其他 macOS 应用程序中所使用的 GUI(图形用户界面)技术的能力。通过分析应用捆绑包并利用 LLVM 的 objdump 工具,5GUIs 能够识别出各种界面技术,如 AppKit、Apple 的 Catalyst(原 Marzipan 项目)、iOS 式的 SwiftUI,以及基于 Electron 和 UIKit 的应用。5GUIs 不仅能够识别技术栈,还能展示不同 GUI 风格的应用窗口,为用户提供一个直观的界面设计展示平台。
APPAnalyze
APPAnalyze 是一款用于分析iOS ipa包的脚本工具,能够自动扫描并发现可修复的包体积问题,同时生成包体积数据用于查看。
数据指标量化:
- 包体积问题:提供数据化平台查看每个组件的包体积待修复问题。
- 包体积大小:展示总大小、单个文件二进制大小和每个资源大小,支持组件化粒度的包体积数据对比。
扫描规则与修复方式:
- 未使用的类:定义但未使用的ObjC和Swift类,建议移除。
- 未使用的ObjC协议:定义但未使用的协议,建议移除。
- Bundle内多Scale图片:移除Scale更低的图片。
- 大资源:文件大小超过20KB即为大资源,建议移除或动态下发。
- 重复的资源文件:移除多余的文件。
- 未使用的属性、方法、资源文件等均有详细的扫描规则和修复建议。
安全与性能:
- 安全:动态反射调用、属性内存申明错误、冲突的分类方法等问题及其修复方式。
- 性能:使用动态库会增加启动耗时,建议使用静态库;减少+load方法的使用以降低启动耗时。
包体积-资源优化
方案
- 图片资源压缩:使用无损或有损压缩工具(如ImageOptim、tinypng、pngquant等)对图片资源进行压缩,尤其是PNG和JPEG格式的图片。考虑将图片转换为WebP格式,因为WebP格式具有较高的压缩率且肉眼难以察觉质量损失。
- 资源清理:使用工具如LSUnusedResources来查找并删除项目中未被引用的图片、音频、视频等资源文件。手动检查项目中是否存在已经不再使用但未被删除的资源,如旧版本的启动图、图标等。
- 动态加载资源:对于大型资源文件(如视频、音频文件),可以考虑在运行时从服务器下载,而不是直接包含在APP安装包中。
- 使用Assets.xcassets:使用Assets.xcassets来管理图片资源,因为Xcode会对Assets.xcassets中的图片进行自动压缩,生成更小的Assets.car文件。
无用图片优化
通过工具获取所有图片资源及代码中可能引用图片的静态字符串,对比找出未引用的图片并删除。具体步骤:
- 获取所有图片:使用脚本递归遍历工程目录,收集所有图片资源及其所属关系。
- 获取可能引用图片的静态字符串:针对不同文件格式(如Objective-C、Swift、HTML等),使用正则表达式匹配可能引用图片的字符串。
- 获取未引用图片:对比图片资源和引用字符串集合,找出未引用的图片。
- 二次过滤:针对字符串拼接的常见case(如暗黑模式图片、图片序列等)进行二次过滤,提高准确度。
Asset Catalog图片优化
Asset Catalog是Xcode提供的资源管理工具,用于集中管理项目中的图片等资源。通过Xcode自带工具actool生成Assets.car文件,可使用assetutil工具分析文件内容。开发者在图片放入Asset Catalog前不要做无损压缩,因为actool会重新进行压缩处理。
Asset Catalog 的优点有:
- 包体积瘦身:根据不同设备下载匹配的图片资源,减少下载包大小。
- 统一的图片无损压缩:采用Apple Deep Pixel Image Compression技术,提高压缩比。
- 便利的资源管理:将图片资源统一压缩成Assets.car文件,便于管理。
- 高效的I/O操作:图片加载耗时减少两个数量级,提升应用性能。
图片压缩工具
TinyPng和pngquant,pngquant支持批量压缩和自定义压缩品质。
HEIC图片编码优化
HEIC图片编码具有高压缩率、节省内存和解码效率高的特点。自iOS 11起,苹果将HEIC设置为图片存储的默认格式,适用于对图片质量和体积要求较高的场景。
大资源优化
- 获取大资源:通过递归遍历ipa包获取体积大于40K的资源文件。
- 优化方法:包括异步下载和资源压缩,以降低首次启动加载压力。
无用配置文件
- 获取配置文件:使用排除法从ipa包中获取除特定文件(如dylib、asset.car、图片、JS&CSS)外的配置文件。
- 获取静态字符串常量:通过otool命令从mach-o文件的TEXT字段静态字符串常量中获取引用的配置文件。
- 无用文件排查:对比获取的配置文件和引用的配置文件,确认并删除无用文件。
- JS&CSS文件排查:针对JS&CSS文件的特殊性,采用类似无用图片检测的方法进行优化。
重复资源优化
- 获取资源文件:从ipa包中获取所有资源文件。
- 判断重复资源:通过MD5判断资源是否重复,并删除重复资源以减小包体积。
包体积-代码优化
方案
- 移除未使用的代码:查找并删除未使用的类、方法、变量等。审查业务逻辑,删除不再使用或已被废弃的代码模块。
- 重构代码:对重复的代码进行重构,使用函数、类等方法来减少代码冗余。优化数据结构,减少内存占用和CPU消耗。
- 编译策略调整:修改编译策略,如启用LTO(链接时优化)来优化跨模块调用代码。剥离符号表(Strip Linked Product),删除未引用的C/C++/Swift代码。精简编译产物,只保留必要的符号和导出信息。
- 代码组件化:将常用代码文件打包成静态库,切断不同业务代码之间的依赖,减少每次编译的代码量。
- 减少文件引用:能使用@class就使用@class,尽量减少文件之间的直接引用关系。
- 减少Storyboard和XIB文件的使用:尽量使用代码布局,减少Storyboard和XIB文件的使用,这些文件在编译时会增加包体积。
- 清理未使用的资源:清理项目中未使用的图片、音频等资源文件,以及未使用的类和合并重复功能的类。
- 模块化设计:将App拆分成多个模块,每个模块独立编译和打包,可以根据需要动态加载或更新模块,减少主包的体积。
- 依赖管理:合理使用CocoaPods、Carthage等依赖管理工具,管理项目的第三方库依赖,避免不必要的库被包含进最终的包中。
Link Map文件解析
- 定义与功能:Link Map是Mach-O二进制文件的辅助文件,描述了可执行文件的全貌,包括编译后的目标文件信息及其代码段、数据段存储详情。
- 生成方法:通过Xcode的Build Settings设置Write Link Map File为yes,并指定存储位置。
- 文件结构:包括基础信息(如可执行文件路径、CPU架构)、Object文件列表、Section段表、Symbols模块等。
无用类
静态检测,通过分析Mach-O文件中的__DATA __objc_classlist和__DATA __objc_classrefs段,获取未使用的类信息。但存在无法检测反射调用类及方法的缺点。
动态检测的方法。在Objective-C(OC)中,每个类结构体内部都含有一个名为isa的指针,这个指针非常关键,因为它指向了该类对应的元类(meta-class)。元类本身也是一个类,用于存储类方法的实现等信息。
通过对元类(meta-class)的结构体进行深入分析,我们可以找到classrwt这样一个结构体,它是元类内部结构的一部分。在classrwt中,存在一个flag标志位,这个标志位用于记录类的各种状态信息。
通过检查这个flag标志位,我们可以进行一系列的计算或判断,从而得知当前类在运行时(runtime)环境中是否已经被初始化过。这种机制是Objective-C运行时系统的一个重要特性,它允许开发者在运行时动态地获取类的信息,包括类的初始化状态等。
也就是通过isa指针找到元类,再分析元类中的classrwt结构体中的flag标志位,我们可以得知OC中某个类是否已被初始化。
1 | // class is initialized |
在Objective-C的运行时(runtime)机制中,类的内部结构和状态通常是由Objective-C运行时库管理的,而不是直接暴露给开发者在应用程序代码中调用的。不过,你可以通过Objective-C的runtime API来间接地获取这些信息。
关于类是否已被初始化的问题,通常不是直接通过objc_class结构体中的某个函数来判断的,因为objc_class结构体(及其元类)的细节和具体实现是私有的,并且不推荐开发者直接操作。然而,Objective-C运行时确实提供了一些工具和API来检查类的状态和行为。
为了检查一个类是否在当前应用程序的生命周期中被使用过(即“被初始化过”),开发者可能会采用一些间接的方法,而不是直接操作类结构体的内部函数。以下是一个简化的说明:
由于不能直接访问类的内部结构,开发者可能会通过其他方式来跟踪类的使用情况。例如,可以在类的初始化方法中设置一个静态标志位或计数器,以记录类是否已被初始化或实例化的次数。虽然不能直接调用objc_class结构体中的函数,但开发者可以使用Objective-C的runtime API(如objc_getClass、class_getInstanceSize等)来获取类的元信息和执行其他操作。然而,对于直接检查类是否“被初始化过”的需求,这些API可能并不直接提供所需的功能。在实际应用中,可能并不需要直接检查类是否“被初始化过”,而是可以通过检查该类的实例是否存在、类的某个特定方法是否被调用过等间接方式来判断。自定义与系统类相同的结构体并实现isInitialized()函数可能是一种模拟或抽象的方式。然而,在实际Objective-C开发中,这样的做法是不必要的,因为直接操作类的内部结构是违反封装原则且容易出错的。相反,开发者应该利用Objective-C提供的runtime API和其他设计模式来达成目标。提到通过赋值转换获取meta-class中的数据,这通常指的是利用Objective-C的runtime机制来查询类的元类信息。然而,直接“判断指定类是否在当前生命周期中是否被初始化过”并不是通过简单地查询元类数据就能实现的,因为这需要跟踪类的实例化过程,而不是仅仅查看元类的结构。
获取类结构体里面的数据
1 | struct mock_objc_class : lazyFake_objc_object { |
所有 OC 自定义类
1 | Dl_info info; |
是否初始化
1 | struct mock_objc_class *objectClass = (__bridge struct mock_objc_class *)cls; |
最后通过无用类占比指标(无用类数量/总类数量*100%)快速识别不再被使用的模块。对于无用类占比高的模块,进行下线或迁移处理,减少组件数量。
无用方法
- 业内常用方法:结合Mach-O和LinkMap文件分析获取无用方法,但准确率低。
- 代码分析:用 Swift 编写的工程代码静态分析命令行工具 smck、使用Swift3开发了个macOS的程序可以检测出objc项目中无用方法,然后一键全部清理
- 代码覆盖率:LLVM插桩获得所有方法及其调用关系。通过分析调用关系,找出未被调用的方法。详见使用 LLVM
精简重复代码
- 工具使用:采用开源工具扫描重复代码。
- 检测与重构:通过静态分析检测重复代码,并结合实际情况进行逻辑重构。
包体积-编译优化
Xcode
Xcode 14的编译器可能通过更智能的分析,识别并消除不必要的Retain和Release调用。这些调用在内存管理中是必要的,但在某些情况下,它们可能是多余的,因为对象的生命周期管理可以通过其他方式更有效地实现。在Objective-C的运行时层面,Xcode 14可能引入了更高效的内存管理策略。这些策略可能包括更快的对象引用计数更新、更智能的对象生命周期预测等,从而减少了Retain和Release操作的执行次数和开销。剥离了未使用的代码和库,包括那些与Retain和Release操作相关的部分。这种优化可以减少最终生成的二进制文件的大小。
编译优化
- Generate Debug Symbols:在Levels选项内,将Generate Debug Symbols设置为NO,这可以减小安装包体积,但需要注意,这样设置后无法在断点处停下。
- 舍弃老旧架构:舍弃不再支持的架构,如armv7,以减小安装包体积。
- 编译优化选项:在Build Settings中,将Optimization Level设置为Fastest, Smallest [-Os],这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。同时,将Strip Debug Symbols During Copy和Symbols Hidden by Default在release版本设为yes,可以去除不必要的调试符号。
- 预编译头文件:将Precompile Prefix Header设置为YES,预编译头文件可以加快编译速度,但需要注意,一旦PCH文件和引用的头文件内容发生变化,所有引用到PCH的源文件都需要重新编译。
- 仅编译当前架构:在Debug模式下,将Build Active Architecture Only设置为YES,这样只编译当前架构的版本,可以加快编译速度。但在Release模式下,需要设置为NO以确保兼容性。
- Debug Information Format:设置为DWARF,减少dSYM文件的生成,从而减少包体积。
- Enable Index-While-Building Functionality:设置为NO,关闭Xcode在编译时建立代码索引的功能,以加快编译速度。
编译器和运行时优化
- Swift 和 Objective-C 的优化:Apple在Swift和Objective-C的编译器和运行时上进行了许多优化,比如Swift协议一致性检查的优化,这些优化可以在不升级工程最低部署版本的情况下,通过App运行在iOS 16、tvOS 16或watchOS 9上享受到性能提升。
- 并发读取缓存:Swift的协议一致性检查引入了ConcurrentReadableHashMap结构,该结构并行读取而无需加锁,相比之前的缓存方案更快。
包体积-链接器优化
使用 -whyload 链接器标志来减少 iOS 应用程序的二进制文件大小, -whyload 标志的作用:它可以帮助开发者识别最终二进制文件中包含的不必要符号。
在 iOS 开发中,链接器负责将代码、库和资源结合成一个最终的可执行文件。在此过程中,可能会有一些不必要的代码被包含进去,例如未使用的库、重复的符号或模块。这些多余的代码会导致应用程序的二进制文件增大,进而影响应用的下载速度、安装时间以及设备的存储空间。
-ObjC 标志,它通常用于强制链接所有 Objective-C 代码到最终的二进制文件中。这在某些情况下是必要的,例如使用了某些需要反射的 Objective-C 代码时,但是它也会导致未使用的代码被包含进去。通过 -why_load,开发者可以识别出哪些代码是多余的,并通过删除 -ObjC 标志来减少文件大小。
包体积-三方库优化
- 组件二进制化:将第三方pod库或自己项目中的业务库由代码格式打包成framework格式,提高编译速度并减小安装包体积。可以使用cocoapods-packager、cocoapods-binary等工具来实现。
- 定期更新第三方库:定期检查项目中使用的第三方库是否有更新版本,新版本可能包含性能改进和体积优化。