【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

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

评论

发布
暂无评论

FIL分币平台|FIL算力系统软件开发技术

量化系统19942438797

#区块链# fil币

看完字节大佬的算法刷题宝典,我直接手撕了500道算法算法题

Java~~~

Java 面试 算法 二叉树 架构师

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

Java~~~

Java 面试 微服务 Spring Boot 架构师

最全总结 | 聊聊 Python 数据处理全家桶(存储过程篇)

星安果

Python 数据库

【Vue2.x 源码学习】第二十七篇 - Vue 生命周期的实现

Brave

源码 vue2 8月日更

三面阿里被挂,竟获内推名额,历经5面拿下口碑offer(Java后台)

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

一个弱鸡管理者如何带领一支牛逼的队伍?

弱鸡管理者

安全 技术人 创新 技术人应知的创新思维模型 管理经验

番外1. OpenCV 图像处理之图片加载与视频加载

梦想橡皮擦

8月日更

九大核心专题,630页内容,熬夜23天吃透,我收割了3个大厂offer

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

这本“算法宝典”讲得透彻,完全掌握后,我竟拿到字节跳动offer

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

第一次凡尔赛,字节跳动3面+腾讯6面一次过,谈谈我的大厂面经

Java~~~

Java 面试 微服务 多线程 架构师

Linux内核分析学习路线总结(内核人员必看)

Linux服务器开发

操作系统 Linux内核 内核源码 内核开发 驱动开发

在阿里晋升3次,5年拿下P8岗位,这份pdf记录了我的整个成长过程

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Spark 架构剖析:一个任务是怎么运行的

程序员赤小豆

大数据 spark 架构

基于香港服务器的应用开发中测试数据管理的 3 个最佳实践

九河云安全

632页!我熬夜读完这份“高分宝典”,竟4面拿下字节跳动offer

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

面试阿里P6,过关斩将直通2面,结果3面找了个架构师来吊打我?

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

为什么拥抱能源的数字未来意味着在云上全力以赴

九河云安全

【共识专栏】Quorum机制与PBFT

趣链科技

区块链 共识机制 PBFT 共识算法

防止数据丢失和减轻勒索软件攻击的 5 种方法

九河云安全

50 亿观众的 “云上奥运”,顶级媒体背后的数智化力量

阿里云视频云

阿里云 直播技术 视频制作 视频云 奥运

百度智能云遇到三一重机,工程机械维保有了新方案

百度大脑

人工智能 三一重工

阿里首席官珍藏,SpringCloud精通日记,血汗全在这了

Java~~~

Java 面试 微服务 Spring Cloud 架构师

Ipfs未来价值怎么样?Ipfs值得投资吗?

区块链 分布式存储 IPFS fil IPFS未来价值

Serverless 全能选手,再添一“金”

Serverless Devs

Serverless 互联网 云原生

不愧为京东内部Spring Boot全解笔记,真的是把精髓全总结出来了

Java~~~

Java 面试 Spring Boot 架构师 京东

去中心化市值管理机器人开发|去中心化做市机器人

Geek_23f0c3

量化交易机器人系统开发 市值管理机器人系统开发 去中心化市值管理机器人

如何保存数据并更快地从勒索软件攻击中恢复

九河云安全

写作7堂课——【1.框架式写作】

LeifChen

框架 结构化思维 写作技巧 8月日更

云计算以及云计算周边词概念简单介绍-行云管家

行云管家

云计算 服务器 云服务

镜像是什么意思?分类有哪些?

行云管家

网络安全 镜像 堡垒机 云厂商

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