HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

评论

发布
暂无评论

轻松复现一张AI图片

程序那些事

人工智能 程序那些事 4月月更 openai

Giants Planet 宣布推出符文,建立在坚实价值的基础上

加密眼界

EasyMR6.2 全面解读:四大功能深度优化,解锁全新大数据处理和计算体验

袋鼠云数栈

hadoop 数据处理 计算引擎 数据计算 国产化替代

ChatGPT助力测试领域!探索人工智能编写测试用例的新前景

测试人

软件测试

国密数据加密在堡垒机上的应用有哪些?

行云管家

信息安全 数据加密 堡垒机 国密

手把手教你实现 OceanBase 数据到 Apache Doris 的便捷迁移|实用指南

SelectDB

数据库 大数据 数据分析 数据同步 数据迁移

Monibuca v5 实现零拷贝 BufReader

不卡科技

Go 性能优化 流媒体

通义灵码牵手阿里云函数计算 FC ,打造智能编码新体验

阿里云云效

阿里云 云原生 通义灵码

漫画项目管理 | 项目目标不合理,该如何修订?

禅道项目管理

项目管理 职场 pmp 能力提升 项目管理PMP

Giants Planet 宣布推出符文,建立在坚实价值的基础上

大瞿科技

最新版Spring Security 中的路径匹配方案!

江南一点雨

Java spring security

文心大模型“你说我画”:PaddleHub与PaddleSpeech的协同实践

百度开发者中心

人工智能 NLP 大模型 大模型

通义灵码牵手阿里云函数计算 FC ,打造智能编码新体验

阿里巴巴云原生

阿里云 云原生 函数计算 通义灵码

短视频评论提取工具软件介绍|评论采集下载爬取

Geek_16d138

Serverless 成本再优化:Knative 支持抢占式实例

阿里巴巴云原生

阿里云 Serverless 云原生

捷途山海T2:通勤低成本,日常出行更经济

Geek_2d6073

利用飞桨与文心大模型重塑大宗商品数字供应链

百度开发者中心

深度学习 大模型

短视频评论抓取拓客软件|评论采集爬取下载工具

Geek_16d138

爬虫工具 爬虫技术 好用的软件分享

ChatGPT助力测试领域!探索人工智能编写测试用例的新前景

测吧(北京)科技有限公司

测试

根据Nginx Ingress指标对指定后端进行HPA

华为云开发者联盟

nginx 开发 华为云 华为云开发者联盟 企业号2024年4月PK榜

拥抱AI技术:6月22-23日全球金牌CSM认证课程 · Jim老师引导讨论AI & Agility话题

ShineScrum捷行

ScrumMaster 敏捷认证 Scrum官方认证

京东广告研发——AIGC在京东广告创意的技术应用

京东科技开发者

DBAIOps生态概述

DBAIops社区

数据库 运维

润和软件成功举办2023-2024年openEuler技术委员会会议

科技热闻

短视频评论ID提取采集软件|评论批量爬取下载工具

Geek_16d138

短视频创业 评论系统 好用的软件分享

双线高防服务器的选择与配置:保障在线业务的安全与稳定

一只扑棱蛾子

高防服务器

DBAIOps关键SQL监控功能

DBAIops社区

数据库 运维 监控

PIRF-395

EchoZhou

English

阿里巴巴1688商品API实战:批量抓取价格、标题、图片及库存数据

技术冰糖葫芦

api 货币化 API 接口 pinduoduo API

借助淘宝评论API,商家轻松掌握市场动态与商机

技术冰糖葫芦

API 接口 API 文档 pinduoduo API

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