写点什么

将 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:274091

评论

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

一个渐进式微前端框架 - Fronts

RingCentral铃盛

架构 大前端 测试 经验分享

【混合云小知识】混合云应用场景包含哪些?

行云管家

云计算 混合云

EasyRecovery如何恢复ps的psd文件

淋雨

数据恢复 EasyRecovery

Vue.js 的九个性能优化技巧

编程江湖

Vue 大前端

学习react源码 征服面试官

buchila11

React

大数据开发之Hadoop家族都有谁

@零度

大数据 hadoop

🍃【Spring专题】「实战系列」spring注解@ConditionalOnExpression详细使用说明

洛神灬殇

spring Spring Framework Condition 12月日更 ConditionOnExpression

Linux一学就会之重定向和文件的查找(Linux下一切皆文件)

学神来啦

Linux 运维 linux云计算 linux一学就会

羊肉泡馍我们来了,尚硅谷西安分校设立首期特惠

编程江湖

编程开发

伴鱼基于 Flink 构建数据集成平台的设计与实现

Apache Flink

大数据 flink 编程 后端 实时计算

怎么排查是哪里出现了数据倾斜

编程江湖

大数据 数据倾斜

零代码训练营第七期本月启动,现正开放报名!

明道云

热门盘点:企业该如何对待低代码?应不应该选择低代码?

优秀

低代码

万字详解什么是生成对抗网络GAN

华为云开发者联盟

算法 推荐算法 GAN 强化学习 生成对抗网络

Redis分布式锁的正确使用

编程江湖

redis java编程

uni-app技术分享| uniapp实现直播旁路推流

anyRTC开发者

uni-app 音视频 视频直播 视频通话 旁路推流

华为云应用构建技术实践精选集

华为云开发者联盟

云计算 华为云 内容合集 技术专题合集 应用构建

【喜讯】尚硅谷西安分校成立啦

@零度

尚硅谷 西安分校成立

埃文科技上榜CCSIP 2021中国网络安全产业全景图3大安全模块

郑州埃文科技

网络安全 ip技术 全景图

「MySQL」数据库备份和还原

恒生LIGHT云社区

MySQL 数据库 MySQL 数据库

如何用GoldWave将音频添加生成机械化音效

懒得勤快

结算中心全国集中化支撑解决之道

鲸品堂

API标准化对Dapr的重要性

行云创新

搞定react源码 惊艳面试官

buchila11

React

产品经理进阶(一)Web APP UI一致性设计

No Silver Bullet

产品经理 12月日更

The Data Way Vol.7|从故事里寻找开源的『内核』

SphereEx

Apache 开源 播客 Meetup SphereEx

解析云原生2.0架构设计的8大关键趋势

华为云开发者联盟

云原生 架构设计 数据治理 存算分离 分布式云

保险行业办理过等保选择哪家好?有成功案例吗?

行云管家

网络安全 等保 等级保护 等保2.0

CSS之选择器

Augus

CSS 12月日更

再添神器!Paddle.js 发布 OCR SDK

百度开发者中心

OCR paddle.js

30个类手写Spring核心原理之环境准备(1)

Tom弹架构

Java spring 源码

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