一款应用首先带给用户的就是启动体验,时间越短则体验越好,苹果更是建议应用第一个加载时间不宜超过 400 毫秒,可是据说 Swift 引用类型对应用的大小及启动速度有影响,这具体是怎么回事?

作者 | Noah Martin   译者 | 弯月

出品 | CSDN(ID:CSDNnews)

头图 | CSDN 下载自东方 IC

应用的启动体验是你带给用户的第一印象。在等待应用启动的过程中,每一毫秒对他们来说都很宝贵,他们完全可以将这些时间花在别处。如果你的应用很吸引用户,他们在一天内使用了很多次你的应用,那么他们肯定会一遍又一遍耐心地等待应用启动。苹果建议第一个画面的加载不应该超过 400 毫秒。这样可以确保在 Springboard 的应用启动动画结束前,你的应用就做好准备可以使用了。

由于只有 400 毫秒的时间,所以开发人员必须非常小心,应尽力避免意外增加应用的启动时间。然而,应用的启动过程非常复杂,有很多可变因素,因此我们很难准确地把握究竟哪些方面影响到了启动的速度。在构建自己的应用期间,我深入研究了应用大小与启动时间的关系。在本文中,我会揭开应用启动过程中较为神秘的一些方面,并向你展示 Swift 引用类型对应用的大小以及启动速度有何种影响。

Dyld

应用启动的时候,Dyld 会加载 Macho-O 可执行文件。Dyld 是苹果负责加载应用的程序。它的运行过程与你编写的代码相同,会在启动的时候加载所有依赖框架,包括系统框架。

Dyld 的任务之一是重定位二进制元数据中的指针,这些元数据描述了源代码中的类型。动态运行时功能需要这些元数据,但这些元数据也会导致二进制文件膨胀。以下是某个已编译的应用二进制文件中包含的 Obj-C 类的布局:

struct ObjcClass {let isa: UInt64let superclass: UInt64let cache: UInt64let mask: UInt32let occupied: UInt32let taggedData: UInt64
}

每个 UInt64 都是一段元数据的地址。由于它包含在应用二进制文件中,因此任何人从商店下载到的数据都是完全相同的。然而,由于地址空间布局随机化(Address Space Layout Randomization,简称 ASLR),因此每次启动应用时,这些数据在内存中的位置都会不同(并非总是从 0 开始)。这是一项安全功能,目的是为了防止他人猜测某个特定功能在内存中的位置。

ASLR 的问题在于,它会导致应用的二进制文件中硬编码的地址出错,实际的起始地址有随机的偏移量。Dyld 的任务就是重定位所有指针,纠正起始位置。可执行文件中的每个指针,以及所有依赖框架(包括递归依赖),都要经过这样的处理。此外,Dyld 还需要设置其他可能会影响启动时间的元数据,比如绑定,但是在本文中,我们只讨论重定位。

所有这些指针的设置都会导致应用的启动时间增加,因此减少指针设置可以缩减应用二进制文件的大小,加快启动速度。下面,我们来看一看这些指针设置源自何方,以及可能产生的影响。

Swift 和 Obj-C

上述,我们看到重定位的时间是由应用的 Obj-C 元数据引起的,但为什么 Swift 应用中会包含这些元数据呢?Swift 具有 @objc 属性,它可以让 Objective-C 代码看到 Swift 中的声明,但是即使 Obj-C 代码看不到 Swift 类型,也会生成元数据。这是因为所有 Swift 类型都包含苹果平台的 Objective-C 元数据。我们来看一看下面这个声明:

final class TestClass { }

这是纯 Swift 代码,并没有继承 NSObject,也没有使用 @objc。但是,它仍然会在二进制文件中生成一个 Obj-C 类元数据,而且还会产生 9 个需要重定位的指针!为了证明这一点,下面我们使用 Hopper 工具检查二进制文件,并查看“纯 Swift”类的 objc_class 条目:

图:应用二进制文件中的Obj-C元数据

将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设置成 1,就可以看到启动应用时需要重定位的指针数量。在应用启动完成后,控制台中就会输出重定位的总数。我们甚至可以准确地找出这 9 个指针的位置。

并非所有 Swift 类型都会添加相同数量的重定位。如果通过重载超类或遵循 Obj-C 协议的方式,将方法公开给 Obj-C,则添加的重定位更多。另外,Swift 类上的每个属性都将在 Objective-C 元数据中生成一个 ivar。

测量

根据设备类型以及运行的应用,重定位对实际启动时间的影响也会有不同。我测量了一台旧 iPhone 5S 上的实际情况。

iOS 的启动大致可分为:热启动和冷启动。热启动指的是,系统已经启动过了应用,并缓存了一些 Dyld 设置信息。由于我测试的首次启动是冷启动,因此速度略微慢一些。

类数量

重定位

重定位时间(ms)

0

17715

8.71

1000

26726

9.23

10000

107726

43.31

20000

197721

104.23

40000

377724

195.26

我们可以看到,每进行 2000 次重定位操作,启动时间就会增加大约 1 毫秒。但这些时间不会直接累加到启动时间,因为某些操作可以并行完成,但是这些操作的确有一个下限,当重定位超过 40 万个时,应用的启动时间就已经接近了苹果建议的 400 毫秒的一半。

示例

我测量了几款流行的应用中重定位操作的发生次数,并借以了解这些操作在实践中的普遍程度。

% xcrun dyldinfo -rebase TikTok.app/TikTok | wc -l
2066598

抖音有 200 多万个重定位,这导致它的启动时间超过了一秒钟!抖音使用了 Objective-C,但是我也测试了一些大型的 Swift 应用,它们使用了单体二进制体系结构,其中的重定位次数大约在 68.5 万~180 万次之间。

该怎么办?

尽管每个类都会增加重定位操作,但我并没有建议将每个 Swift 类都换成 struct。大型 struct 也会增加二进制文件的大小,而且在某些情况下,你需要的只是引用而已。与其他提升性能的手段一样,你应该避免过早优化,而且首先应该从测量开始。在发现问题之后,你可以寻找应用中需要改进的地方。以下是一些常见的情况:

  • 组合与继承

假设有如下这样的一个数据层:

class Section: Decodable {let name: Stringlet id: Int
}final class TextRow: Section {let title: Stringlet subtitle: Stringprivate enum CodingKeys: CodingKey {case titlecase subtitle}required init(from decoder: Decoder) throws {let container = try decoder.container(keyedBy: CodingKeys.self)title = try container.decode(String.self, forKey: .title)subtitle = try container.decode(String.self, forKey: .subtitle)try super.init(from: decoder)}
}final class ImageRow: Section {let imageURL: URLlet accessibilityLabel: Stringprivate enum CodingKeys: CodingKey {case imageURLcase accessibilityLabel}required init(from decoder: Decoder) throws {let container = try decoder.container(keyedBy: CodingKeys.self)imageURL = try container.decode(URL.self, forKey: .imageURL)accessibilityLabel = try container.decode(String.self, forKey: .accessibilityLabel)try super.init(from: decoder)}
}

这段代码会产生大量元数据,但是同样的功能可以通过值类型实现(更适合在数据层中使用),并最终减少 22% 的重定位。你需要用组合替换掉对象继承,例如具有关联值的枚举,或泛型等。

struct Section<SectionType: Decodable>: Decodable {let name: Stringlet id: Intlet type: SectionType
}struct TextRow: Decodable {let title: Stringlet subtitle: String
}struct ImageRow: Decodable {let imageURL: URLlet accessibilityLabel: String
}
  • Swift 中的类别

即使 Swift 没有使用类别,而是使用了扩展,但你仍然可以通过声明使用了 Objective-C 函数的扩展来生成类别二进制元数据。声明方式如下:

extension TestClass {@objcfunc foo() { }override func bar() { }
}

这两个函数都包含在二进制元数据中,但是由于它们是在扩展中声明的,因此可以通过 TestClass 的合成类别引用。将这些函数移到原始类声明中,可以避免二进制文件包含额外的类别元数据。

此外,你还可以使用基于闭包的回调(例如 iOS 14 引入的回调)完全避免 @objc。

  • 许多属性

Swift 类中的每个属性都会添加 3~6 个重定位,具体取决于该类是否为 final 类。如果有很多拥有 20 多个属性的大型类,那么这个数字就非常惊人了。例如:

final class TestClass {var property1: Int = 0var property2: Int = 0...var property20: Int = 0
}

将其转换为 struct,可以减少 60% 的 rebase!

final class TestClass {struct Content {var property1: Int = 0var property2: Int = 0...var property20: Int = 0}var content: Content = .init()
}
  • 代码生成

回报率最高的提升方法之一就是改进代码生成。代码生成的一种流行的用法是在多个代码库中建立共享的数据模型。如果你在多种类型上进行此操作,则需注意它们会增加多少 Obj-C 元数据。然而,即便是值类型,也会增加代码量以及重定位的开销。最佳解决方案是尽可能减少生成的类型数量,或者用生成的函数替换自定义类型。

上述这些示例只是由于二进制文件规模扩大,而导致启动时间增加的几种情况。还有其他导致启动时间增加的原因,比如从磁盘加载到内存的代码量越大,启动时间就会越长。

原文链接:https://medium.com/codestory/why-swift-reference-types-are-bad-for-app-startup-time-90fbb25237fc

声明:本文为 CSDN 翻译,转载请注明来源。

在第 111 个女神节到来之际,CSDN 向所有技术女神致敬!并特邀产学研界的技术女神代表,共同探讨女性开发者的职业发展机遇与挑战,助力更多程序媛谱写精彩的程序人生。

iOS 应用启动慢的原因找到了!相关推荐

  1. adhoc包无法安装_关于iOS 应用安装失败的原因找到了

    原标题:关于iOS 应用安装失败的原因找到了 iOS 的内测应用在安装时,很多人都遇到过安装失败的情况,安装失败的原因比较多,下面我们将一些常见原因总结如下,方便开发者进行排查. 启动应用时,出现提示 ...

  2. iPad连android热点掉线,苹果终于承认,iOS 13有这个问题,网络断连的原因找到了...

    原标题:苹果终于承认,iOS 13有这个问题,网络断连的原因找到了 3 月 21 日,据 MacRumors 报道,Apple 于近期向授权服务提供商发布内部文档,承认部分升级至 iOS / iPad ...

  3. fir.im Log Guru 正式开源,快速找到 iOS 应用无法安装的原因

    很开心的宣布 Log Guru 正式开源! Log Guru,是 fir.im 开发团队创造的小轮子,用在 Mac 电脑上的日志获取,Github 地址:FIRHQ/LogGuru. Log Guru ...

  4. iOS App 启动性能优化

    为什么80%的码农都做不了架构师?>>>    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq. ...

  5. iOS 页面的卡顿的原因以及如何解决. 如何优化app的启动速度

    1.死锁: 主线程拿到锁A, 需要获取锁B, 而同时子线程拿了锁B, 需要锁A, 这时主线程等待锁B的释放, 子线程等待锁A的释放, 相互等待. 2.抢锁: 主线程需要访问DB, 而这时某个子线程往D ...

  6. 马蜂窝 iOS App 启动治理:回归用户体验

    增长.活跃.留存是移动 App 的常见核心指标,直接反映一款 App 甚至一个互联网公司运行的健康程度和发展动能.启动流程的体验决定了用户的第一印象,在一定程度上影响了用户活跃度和留存率.因此,确保启 ...

  7. 如何在QEMU上执行iOS并启动一个交互式bash shell,内含整个安装流程并且提供了相关工具(二)

    我们在上一篇文章中介绍如何在QEMU上执行iOS并启动一个交互式bash shell,在第这篇文章中,我们将详细介绍为实现这些目标所进行的一些具体的项目研究. 本文的研究项目是以该项目为基础进行的,我 ...

  8. iOS程序闪退的原因以及处理办法

    iOS程序闪退是一种比较常见的现象.闪退的情况很多,造成程序闪退的原因也很多. ================================启动时闪退======================= ...

  9. 苹果M1用着舒服的原因找到了,英特尔:学到了,下次我也用

    梦晨 发自 凹非寺 量子位 报道 | 公众号 QbitAI 苹果M1又快又省电,除了跑分很高之外,实际体验上也有一种流畅感. 苹果到底怎么做到的? 原来除了硬件性能强大以外,软件层面也有优化技巧. 一 ...

最新文章

  1. EasyUI-datagrid 对于展示数据进行处理(formatter)
  2. excel vba 不可查看
  3. 神策数据张涛:企业服务客户全生命周期运营三步曲:执行反馈
  4. 快速复制数据库表中的数据SQL
  5. Linux sudo找不到命令:修改sudo的PATH路径
  6. java定义接口_一文知道Java中接口的定义
  7. java添加按钮点击事件_如何为odoo 10中的按钮点击事件添加一个java脚本处理程序?...
  8. miniMobile(手机)
  9. selenium-webdriver——让chrome跑起来
  10. 一位挪威博士的PolarDB资深架构师之路
  11. 初识C语言,入门小程序
  12. 小学数学思维导图集合 小学数学思维导图怎么画
  13. 撰写商业计划书的一些误区和建议
  14. hyperscan5.0编译方式整理
  15. Excel VBA 编程的常用代码
  16. ChatGPT的注册和使用教程
  17. java opengl 图片文字_如何通过opengl显示相机预览
  18. 虎牙在全球 DNS 秒级生效上的实践
  19. go中宕机与恢复 panic/recover 介绍
  20. 水平集——那些我膜拜过的牛人2

热门文章

  1. 星尘小组第十一周翻译-设计和优化索引
  2. 移动开发day4_京东移动页面
  3. 2-1 CPU多级缓存-缓存一致性.mkv
  4. Java类加载文章2(z)
  5. QSrcollBar样式表设置
  6. [模拟|数位] leetcode 9 回文数
  7. oracle典型安装配置,Oracle的安装配置一些有关问题
  8. python自动测试模型_Selenium+Python 自动化测试模型
  9. canoco5冗余分析步骤_基因富集分析|理解
  10. 鸡蛋掉落(动态规划)