写点什么

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

评论

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

Sentinel-Go 源码系列(三)滑动时间窗口算法的工程实现

捉虫大师

Go sentinel-go

公安合成作战指挥系统开发,情指勤舆一体化平台建设

电微13828808271

智慧公安重点人员管控系统开发建设方案

a13823115807

智慧公安

智慧警务系统建设方案,公安重点人员动态管控系统开发

电微13828808271

你可能不信,52小时能做出7款超酷产品!

LigaAI

程序员 技术 技术人生 技术分享 hackathon

在线JSON转ClickHouse数据表工具

入门小站

工具

南瓜电影CTO早晨6点接到紧急电话,被告知“系统压力大”,看其如何化解危机

大咖说

Serverless 技术 数智化

手把手教你使用 Timestream 实现物联网时序数据存储和分析!

亚马逊云科技 (Amazon Web Services)

Data

实现更高性能,一起探索Amazon Redshift高级查询加速器

亚马逊云科技 (Amazon Web Services)

Data

先进开发团队,先用 Apifox,做“API 文档先行”理念的先行者!

狐哥说技术

Apifox API swagger API文档

Linux之nl命令

入门小站

Linux

spring 通过xml文件进行依赖注入

田镇珲

【云堡垒机】市面上部分云堡垒机厂商大汇总

行云管家

云计算 网络安全 堡垒机 IT运维 IT资产

Amazon Aurora 并行查询——加速分析处理的利器

亚马逊云科技 (Amazon Web Services)

Data

质量基础设施一站式服务线上平台助力高质量发展

电微13828808271

比特币挖矿与源码解析

恒生LIGHT云社区

比特币 区块链 挖矿

超细!细说Zookeeper选举的一个案例(下)

恒生LIGHT云社区

Go golang zookeeper Go 语言

linux学习全套资料:结构化命令case-for-while

侠盗安全

Linux 运维 运维工程师 云计算架构师

问诊把脉“实景三维业务发展瓶颈在哪里”和“御医良方”

焱融科技

云计算 云原生 GIS 高性能 文件存储

【紧急】Log4j又发新版2.17.0,只有彻底搞懂漏洞原因,才能以不变应万变,小白也能看懂

Tom弹架构

Java log4j 安全漏洞

TDinsight——基于Grafana的TDengine零依赖监控解决方案

TDengine

数据库 tdengine 时序数据库 后端技术

Java 基础之详解 Java IO

编程江湖

JAVA开发

2022年一站式服务器管理软件就用行云管家!

行云管家

云计算 服务器 IT运维 服务器管理

企业如何正确使用CRM系统?

低代码小观

低代码 企业管理 CRM CRM系统 企业管理软件

Linux一学就会--Shell教程 || Shell的基础用法(详细)

学神来啦

Linux centos 运维 Shell linux云计算

中科柏诚布局信创产业,护航信创产品安全

联营汇聚

Java中List排序的3种方法!

王磊

Java

盘点 2021 征文大赛|记录你的年度闪光时刻!

InfoQ写作社区官方

盘点2021 热门活动

架构训练营 -- 模块三

LJK

架构训练营

大数据开发hadoop之yarn基础架构详解

@零度

大数据 hadoop YARN

Linux之nl命令

入门小站

Linux

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