写点什么

将 iOS 应用体积缩小一半的秘籍:妥善运用动态框架

  • 2024-04-22
    北京
  • 本文字数:5361 字

    阅读完需:约 18 分钟

将 iOS 应用体积缩小一半的秘籍:妥善运用动态框架

每个开发新手,在编写软件前都听说过这样一条原则:“别自我重复”。但 App Store 上不少体量最大的 iOS 应用却仍在犯下同样的致命错误:不必要地照搬整个模块。


以现代汽车发布的 MyHundai 应用为例,这款软件可供车主轻松访问车辆的服务历史记录并申请道路救援。



看看我们分析后得出的大块红色片段——这些就是资产目录中重复的部分,而且在应用程序包中整整被照搬了三回。


这当然不纯是因为现代汽车的开发者特别喜欢.car 文件,而是 iOS 扩展中的部件(MyHyundaiWidget)和共享扩展(MyHundaiSharePoi)都以沙箱化的形式与应用本体各自保持独立。


所以除非大家小心谨慎地规划应用架构,否则就很容易犯下我们在 MyHyundai 软件中看到的错误:将共享 UI 库同各个目标静态链接。


静态库虽然表面上是在共享代码,但实际上却被单独打包在每个目标的编译二进制文件当中(在本示例中就是 1 个应用加 2 个扩展),而这很可能会导致不必要的重复。


教科书式的解决方案并不复杂:对于在各目标之间共享的模块,应该将其链接为动态框架,而非静态库。


不同于将模块副本嵌入到各个目标当中,动态框架会将各模块独立存放在.app 捆绑包的 Frameworks/ 文件夹内,再由 dyId 在启动时将其链接至您的应用(或者扩展)。


在实践当中,特别是当大家的应用软件用到 Swift Packge Manager 提供的现代多模块架构时,对模块的动态链接往往会被隐藏起来。


所以这里我们需要做一点调整。


本文将以简单的开源教程项目 EmergeMotors 为例,带大家从存在问题的 Before/ 文件夹入手,以结对编程的形式不断改进架构,直至与 After/ 完全吻合。期间我们会随时分析调整对于应用程序大小的影响。


初见 EmergeMotors


EmergeMotors 其实是受到 MyHyundai 应用的启发,假设这是一款人气颇高的新应用,主要功能是……查看汽车照片。其中配有共享扩展和部件扩展,均可用于显示汽车图像。



与各类现代应用一样,EmergeMotors 拥有一个专用的 UI 库 EmergeUI,其中包含常用组件及资产。这一切都将被导入至全部三个目标当中:应用本体、共享扩展和部件扩展。


于是乎,EmergeMotors 自然也就与 MyHyundai 应用面临相同的架构问题:二进制文件中的 UI 包被照搬了三次。



除了资产之外,EmergeUI 视图代码和 Lottie 子依赖项也被单独与各二进制文件捆绑在了一起。


如前所述,解决这个问题的标准方案就是将静态链接的 EmergeUI 库转换为动态框架。


使用 SwiftPM 创建动态框架


默认情况下,Xcode 允许开发者选择以静态还是动态方式链接 Swift 包。而在实际操作中,它总是会直接将包捆绑为静态库。


大家可以将包的库类型指定为.dynamic 来要求 Xcode 动态接入 Swift 包:


// EmergeUI/Package.swift
let package = Package( name: "EmergeUI", platforms: [.iOS(.v16)], products: [ .library( name: "EmergeUI", type: .dynamic, targets: ["EmergeUI"]), ], dependencies: [ .package(url: "https://github.com/airbnb/lottie-ios.git", .upToNextMajor(from: "4.4.1")), ], targets: [ .target( name: "EmergeUI", dependencies: [.product(name: "Lottie", package: "lottie-ios")] ) ])
复制代码


好了,现在咱们的库“动”起来了!


大家可以查看 Xcode 中的主项目来检查是否设置成功。


对于静态库,框架、库和嵌入内容中的“Embed”下不会存在与模块相关联的选项。而将库类型设置为动态后,则会出现一个下拉菜单,我们可以在其中指定如何嵌入框架(如果仍无显示,请通过文件、包、重置包缓存的方式强制刷新)。



确保您的主应用目标将框架设置为“Embed & Sign”,这样即可确保框架被复制到应用程序包内并使用您的配置文件与证书对代码进行签名。


我们的扩展目标应使用“Do Not Embed”不嵌入选项,以避免在应用程序包中制作额外的副本。



伞形框架


现在,我们的 Swift 包已经成为动态框架。


除了包内定义的代码之外,各子依赖项(包括第三方库)现在也已成为动态链接框架的一部分,即使子依赖项本身仍为静态。


通过这种方式,我们甚至可以将多个库打包进同一伞形框架之内,并向用户开放统一的公共接口,就如同只导入单一模块一样。


苹果一直使用伞形框架(导入 Foundation、导入 UIKit、导入 AVKit……),但除非大家明确知道自己在做什么,否则常规方案一般不建议使用这种粗暴的方法。


初步结果


现在我们已经在 Package.swift 中定义了动态框架,并告知 Xcode 如何将其链接至各个目标(包括框架、库和嵌入内容),现在我们可以保存 EmergeMotors 并尝试分析。



好吧,看来我们还有很长的路要走。


虽然我们共享的 EmergeUI 库代码和第三方 Lottie 依赖项都被顺利打包成了框架,但占比最大的组件 EmergeUI.bundle 仍然被捆绑到了各目标当中。


直接检查我们的 xcarchive 文件,我们即可查看.app 包内部(右键单击 + 显示包内容)并观察 EmergeUI.bundle 本体。



资产目录与 Lottie JSOn 被统一打包起来并静态链接至各个目标。对于资产密集型模块来说,这已经抵消了使用框架带来的大部分好处。


现在,如果大家的共享模块主要是代码——比如第三方依赖项的打包器、内部 SDK 或者某些子模块的伞形框架——那么优化工作已经完成了。用默认 SwiftPM 方法创建动态框架已经可以带来很好的效果。


但如果您的应用不幸跟我们的示例类似,即共享代码中包含大量资源,那么 Swift Package Manager 就会严重限制优化效果。


重复资产删除


这个问题当然也可以解决,甚至仍旧可以通过 SwiftPM 来实现。但这样肯定会破坏我们精美的包架构。


如果各位已经是经验丰富的 SwiftUI 老手,而且习惯了用 UIKit 来访问更复杂的功能,那么接下来我要展示的方法在本质上是相同的,只是操作起来更加友好。



免责声明:整个设置过程确实有点烦人,而且每次更新共享资源时都会带来沉重的运行开销。所以在让架构复杂化之前,请确保各个目标是否确有必要共享资产。或者,大家也可以考虑为每个目标单独创建最小资产模块,以最大程度减少重复。


我的这门资产标准化秘方包含四个步骤:


  1. 创建一个新的 Xcode Framework 并将共享资源转移过去。

  2. 使用二进制目标创建一个新的 Swift 包。

  3. 为每个架构建立框架,并将 build 输出打包在 xcframework 当中,由上述二进制目标进行引用。

  4. 将新包导入至现有动态库中。


创建框架


这里我创建了一个名叫 EmergeAssets 的新 Xcode 项目,并把资产目录和 JSON 资源全部转移过去(记得检查目标的成员身份!)。



为了便于量化,我还创建了下面这条重要的辅助函数。


// EmergeAssets/EmergeAssets/BundleGetter.swift
public final class BundleGetter { public static func get() -> Bundle { Bundle(for: BundleGetter.self) }}
复制代码


这样我们就能从其他模块处引用 EmergeAssets 包内的资产:


// EmergeUI/Sources/EmergeUI/Car/Car.swift
import EmergeAssets
public struct Car { // ... public var image: Image { Image("(id)", bundle: EmergeAssets.BundleGetter.get()) }}
复制代码


导入二进制目标


接下来,我创建了一个新的 Swift 包,然后毫不意外将其命名为 EmergeAssetsSPM。


作为一个打包器包,它的架构非常简单:


// EmergeAssetsSPM/Package.swift
let package = Package( name: "EmergeAssetsSPM", products: [ .library( name: "EmergeAssetsSPM", targets: ["EmergeAssetsSPM"]), ], targets: [ .binaryTarget( name: "EmergeAssetsSPM", path: "EmergeAssets.xcframework" ) ])
复制代码


这里的 binaryTarget 正是关键。


二进制目标经过预编译,以确保我们的资产包已被整齐打包在框架之内。也就是说编译器不会对其进行构建,也不会将其重新捆绑至各个目标当中。


起初,除了 Package.swift 和这个神秘的 shell 脚本: generate_xcframework.sh,EmergeAssetsSPM 包中再无其他文件。


构建 XCFramework


我们可以使用 xcodebuild 命令行工具来创建二进制框架。


我编写了一个 shell 脚本,用于构建本地 EmergeAssets 框架,并将我需要的架构变体(iOS+ 模拟器)打包进 xcframework 当中。该 xcframework 可以作为 EmergeAssetsSPM 的二进制目标进行导入。


// EmergeAssetsSPM/generate_xcframework.sh
# /bin/bash!
# Build framework for iOSxcodebuild -project ../EmergeAssets/EmergeAssets.xcodeproj -scheme EmergeAssets -configuration Release -sdk iphoneos BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO
# Build framework for Simulatorxcodebuild -project ../EmergeAssets/EmergeAssets.xcodeproj -scheme EmergeAssets -configuration Release -sdk iphonesimulator BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO
# To find the Build Products directory, you can either: # 1. Manually build the framework and look in Derived Data # 2. run `xcodebuild -project EmergeAssets.xcodeproj -scheme EmergeAssets -showBuildSettings` and search for BUILT_PRODUCTS_DIRPRODUCTS_DIR=~/Library/Developer/Xcode/DerivedData/EmergeAssets-fuszllvjudzokhdzeyiixzajigdl/Build/Products
# Delete the old framework if it existsrm -r EmergeAssets.xcframework
# Generate xcframework from build productsxcodebuild -create-xcframework -framework $PRODUCTS_DIR/Release-iphoneos/EmergeAssets.framework -framework $PRODUCTS_DIR/Release-iphonesimulator/EmergeAssets.framework -output EmergeAssets.xcframework
复制代码


要亲自尝试,大家需要注意包含适用于所有目标平台的 SDK——要正常支持,请确保包含 macosx、appletvos、watchos 以及相应的模拟器。


虽然我只构建了发布配置,但在试验过程中调试构建仍然顺利通过,大家的实操结果可能会有所不同。


导入我们的资产框架


最后,我们的 EmergeUI 模块可以导入 SwiftPM 打包的框架以作为常规本地包依赖项。


// EmergeUI/Package.swift
let package = Package( name: "EmergeUI", platforms: [.iOS(.v16)], products: [ .library( name: "EmergeUI", type: .dynamic, targets: ["EmergeUI"]), ], dependencies: [ .package(url: "https://github.com/airbnb/lottie-ios.git", .upToNextMajor(from: "4.4.1")), .package(path: "../EmergeAssetsSPM") ], targets: [ .target( name: "EmergeUI", dependencies: ["EmergeAssetsSPM", .product(name: "Lottie", package: "lottie-ios")] ), .testTarget( name: "EmergeUITests", dependencies: ["EmergeUI"]), ])
复制代码


最终结果


在解决这个重大架构难题之后,我们的项目终于构建完成了。我们的全部三个目标(应用程序、共享扩展与部件扩展)均能按预期正常工作。


经过 归档和分析,我们看到了以下结果——终于舒服了。



资产目录(及 Lottie JSON)在 EmergeAssets.framework 中彼此独立地和谐共存。EmergeUI 框架保持单独链接,两个扩展插件几乎微不可见——只要不照搬非必要资源,它们本可以如此小巧!


安装包大小也从 32.3 MB 急剧缩小至 13.7 MB。



启动速度


我可不是要盲目宣传动态框架,它也有自己的缺点,而且最直接的影响就是大大拖慢应用程序的启动速度。


在应用程序启动的预主阶段,dyId 会将必要的框架链接至目标,确保所有可执行代码及资产均可访问。


我在各 builds 之间进行了快速性能分析,想要评估具体有何影响,最终得出了漂亮的焰形统计图。


这里的阶段也就是 dyId 在启动时链接动态框架的过程。除了链接我们自己的 EmergeUI 框架之外,dyId 还链接了 SwiftUI、Foundation 以及 Swift 本身!


以下就是 Before/ 中我们初始应用的启动性能统计。



优化之前,EmergeMotors 应用程序的启动性能统计。


以下是 After/ 瘦身优化之后的应用程序启动性能。



优化之后,EmergeMotors 应用程序的启动性能统计。


在本示例中,二者几乎没有统计学意义上的显著变化,意味着额外的动态链接对于启动时间的影响可以忽略不计。但我强烈建议大家分析自己的应用程序,在明确性能影响之后再做权衡。


总结


苹果就是不愿意让我们简简单单、舒舒服服地搞开发。


他们在 Swift Package Manager 中提供了出色的第一方包生态系统,但却不愿认真解释要如何充分加以使用。


打包一个动态框架并不困难,但我们得经历很多莫名其妙的环节才能正确删除重复资产,并让应用程序保持“纤细苗条”。


但在一切尘埃落定之后,我们最终获得了令人惊叹的结果,比如应用程序的二进制文件大小缩减了 58%。欢迎大家亲自上手示例项目,体验这些秘密技术,并以类似的方式对自己的应用程序进行瘦身!


原文链接:


https://www.emergetools.com/blog/posts/make-your-ios-app-smaller-with-dynamic-frameworks


今日好文推荐


AI 手机来了,App 将消亡,前端开发范式变了!


Vue 的响应式机制就是个“坑”


李彦宏“程序员将不再存在”言论被周鸿祎驳斥,网友怒怼:先把百度程序员都开除了!


生成式 AI,前端开发的终结者?无障碍组件告诉你:NO!


2024-04-22 19:274200

评论

发布
暂无评论
发现更多内容

开源软件供应链攻击激增430%,供应链安全不容小觑丨行业报告解读

SEAL安全

开源 DevOps 行业报告 软件供应链安全

vue面试之Composition-API响应式包装对象原理

bb_xiaxia1998

Vue

长安链源码分析之交易过程分析(6)

微信小程序wx.getLocation审核不通过的解决方法

源字节1号

前端开发 小程序开发

web前端开发培训女生学习怎么样

小谷哥

从输入URL到渲染的过程中到底发生了什么?

loveX001

JavaScript

请求投放个性化广告时,如何征得用户同意?

HarmonyOS SDK

广告

java培训哪家比较靠谱

小谷哥

1024,我们干了点儿大事 | StarRocks 2.4 新版本特性介绍

StarRocks

数据库

一天梳理完React所有面试考察知识点

beifeng1996

React

长安链源码分析之交易过程分析(5)

关于JavaScript的本地存储方案

CoderBin

JavaScript 前端 LocalStorage 本地存储 10月月更

杨帆:拆解研发流程,做好探索型项目的过程管理丨声网开发者创业讲堂 • 第 5 期

声网

技术管理 人工智能’

腾讯前端常考vue面试题整理

bb_xiaxia1998

Vue

前端培训机构包就业靠谱吗?

小谷哥

一次 Redis 事务使用不当引发的生产事故

悟空聊架构

redis 事务 悟空聊架构 10月月更 @Transactional

长安链源码分析之交易过程分析(7)

百度搜索业务交付无人值守实践与探索

百度Geek说

Pytho 企业号十月 PK 榜 智能测试

java开发培训机构要怎么谨慎选择

小谷哥

SpringCloud-05 Hystrix学习笔记

游坦之

10月月更

React源码分析5-commit

goClient1992

React

倒计时第1天!2022 XDR网络安全运营新理念峰会即将开幕

未来智安XDR SEC

网络安全

SpringCloud-04 Feign学习笔记

游坦之

10月月更

盘它!基于CANN的辅助驾驶AI实战案例,轻松搞定车辆检测和车距计算!

华为云开发者联盟

人工智能 华为云 辅助驾驶 企业号十月 PK 榜

一道React面试题把我整懵了

beifeng1996

React

web技术分享| 虚拟 tree

anyRTC开发者

Vue 前端 Web tree antDesign vue

React源码分析6-hooks源码

goClient1992

React

最短的桥

掘金安东尼

算法 10月月更

使用注解 @requires 给 SAP CAP CDS 模型添加权限控制

汪子熙

云原生 CAP Cloud SAP 10月月更

大数据培训学习就业难吗

小谷哥

百度前端高频react面试题总结

beifeng1996

React

将 iOS 应用体积缩小一半的秘籍:妥善运用动态框架_工程化_Jacob Bartlett_InfoQ精选文章