0%

iOS二进制重排对缺页和启动时间的优化实践

抖音团队去年针对系统虚拟内存缺页的情况,基于二进制重排的方案,给App启动速度提升了15%,各路大神也随后分享了几篇优质的二进制重排的文章,这里基于自己的项目做一下实践

基本原理

  1. 进程运行时使用的内存是操作系统提供的虚拟内存,而不是直接操作物理内存
  2. 虚拟内存物理内存有一个映射表
  3. 进程的内存会进行分页管理,以页为单位
  4. 程序启动的时候,并不会把所有内存都加载到物理内存中,而是用到的时候才加载,没有用到的内存,可能并没有加载到物理内存中
  5. 当程序访问到的内存地址(虚拟内存),如果还没有加载到物理内存时,就会触发Page Fault,(对应System TraceFile Backed Page In),然后操作系统把数据加载到物理内存中,如果已经已经加载到物理内存了,则会触发Page Cache Hit,后者是比较快的,这也是热启动比冷启动快的原因之一
  1. 基于上面原理. 我们的目标就是在启动的时候增加Page Cache Hit,减少Page Fault,从而达到优化启动时间的目的
  2. 我们需要确定,在启动的时候,执行了哪些符号,尽可能让这些符号的内存集中在一起,减少占用的页数,就能减少Page Fault的命中次数

测试Page Fault

通过Instrument / System Trace工具,可以查看我们的App,在启动过程中的Page Fault数量(File Breaked Page In)

system trace page fault

如果App比较大,Analizing的过程会比较久,需要耐心等待

这里有个注意点,为了确保App是真正的冷启动,需要把内存清干净,不然结果会不太准,下图是我直接杀掉App,重新打开得到的结果

_

可以看到,和第一次测试差的有点多,我们可以在杀掉App后,重新打开多个其他的App(尽可能多),把原来的内存都覆盖掉,这样在重新打开App的时候,就会重新加载物理内存

确定代码执行顺序

接下来需要确定App在启动的时候,调用了哪些函数(使用了哪些符号),这里我们使用杨萧玉写的一个工具AppOrderFiles,使用Clang SanitizerCoverage,通过编译器插装的方式,获取到调用函数的符号顺序

通过pod引入

1
pod 'AppOrderFiles'

并且添加编译宏OTHER_CFLAGSOTHER_SWIFT_FLAGS(只在Debug生效即可)

1
2
3
4
5
6
7
8
9
10
11
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
case config.name
when "Debug"
config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
end
end
end
end

在App启动后,到第一个页面(HomePage)的viewDidLoad方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import AppOrderFiles

override func viewDidLoad() {
super.viewDidLoad()

...

#if DEBUG
// 延迟一下,让运行实践长一点,避免进入后因为PageFault造成卡顿
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
AppOrderFiles { (filePath) in
if let p = filePath {
print("output order file \(p)")
}
}
})
#endif
}

输出的文件在App沙盒,用模拟器运行更方便,得到文件app.order,这里面就是排好序的符号列表,根据App的执行顺序,如果项目比较大的话,会比较久

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
___swift_instantiateConcreteTypeFromMangledName
_main
_$s3jcm11AppDelegateCMa
_$s3jcm11AppDelegateCACycfcTo
_$s3jcm11AppDelegateCACycfc
_$s3jcm11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtFTo
_$s3jcm11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtF
_$s3jcm11AppDelegateC5setup13launchOptionsySDySo019UIApplicationLaunchF3KeyaypGSg_tF
_$s3jcm5ConstV11wechatAppIdSSvau
_globalinit_33_27D199AC10BAAE2783814C508183B809_func13
_$s3jcm5ConstV19wechatUniversalLinkSSvau
_globalinit_33_27D199AC10BAAE2783814C508183B809_func15
_$sSo12BaiduMobStatCMa
_$sSo12BaiduMobStatCs5Error_pIggzo_ABsAC_pIegnzo_TRTA
_$sSo12BaiduMobStatCs5Error_pIggzo_ABsAC_pIegnzo_TR

...

app.order放到工程目录,配置到Xcode里面Build Setting -> Order File -> $(PROJECT_DIR)/app.order

order file setting

验证是否生效

Xcode里面Build Setting有个Write Link Map File,可以生成Link Map文件的选项,路径如下

1
2
3
4
5
# Link Map文件
Intermediates.noindex/xxxx.build/Debug-iphoneos/xxx.build/xxx-LinkMap-normal-arm64.txt

# 生成app文件路径
Products/Debug-iphoneos/xxx.app

文件内容其实是描述链接器连接的详情,对应的是MachO文件的内存分布,文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Path: /Users/bomo/Library/Developer/Xcode/DerivedData/SwiftScaffold-fdswirgebkkdidcxcpxdffxxvxye/Build/Products/Debug-iphoneos/jcm.app/jcm
# Arch: arm64
# Object files:
[ 0] linker synthesized
[ 1] /Users/bomo/Library/Developer/Xcode/DerivedData/SwiftScaffold-fdswirgebkkdidcxcpxdffxxvxye/Build/Intermediates.noindex/SwiftScaffold.build/Debug-iphoneos/jcm.build/Objects-normal/arm64/JHCollectionViewFlowLayout.o
[ 2] /Users/bomo/Library/Developer/Xcode/DerivedData/SwiftScaffold-fdswirgebkkdidcxcpxdffxxvxye/Build/Intermediates.noindex/SwiftScaffold.build/Debug-iphoneos/jcm.build/Objects-normal/arm64/JHCollectionReusableView.o
...

# Sections:
# Address Size Segment Section
0x100004928 0x00ED5B08 __TEXT __text
0x100EDA430 0x00005550 __TEXT __stubs
0x100EDF980 0x00005190 __TEXT __stub_helper
0x100EE4B10 0x000684D9 __TEXT __cstring
...

# Symbols:
# Address Size File Name
0x100004928 0x00000094 [ 6] ___swift_instantiateConcreteTypeFromMangledName
0x1000049BC 0x00000088 [ 78] _main
0x100004A44 0x00000070 [ 78] _$s3jcm11AppDelegateCMa
0x100004AB4 0x00000044 [ 78] _$s3jcm11AppDelegateCACycfcTo
0x100004AF8 0x00000108 [ 78] _$s3jcm11AppDelegateCACycfc
0x100004C00 0x00000144 [ 78] _$s3jcm11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtFTo
0x100004D44 0x00000430 [ 78] _$s3jcm11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtF
...

# Dead Stripped Symbols:
# Size File Name
<<dead>> 0x00000006 [ 2] literal string: class
<<dead>> 0x00000014 [ 2] literal string: setBackgroundColor:
<<dead>> 0x0000000B [ 2] literal string: v24@0:8@16
<<dead>> 0x00000010 [ 3] literal string: backgroundColor
<<dead>> 0x00000014 [ 3] literal string: setBackgroundColor:
<<dead>> 0x0000000E [ 3] literal string: .cxx_destruct
<<dead>> 0x00000008 [ 3] literal string: @16@0:8
...

这里我们只关注符号表Symbols,这里的顺序就是MachO文件对应的顺序,如果与app.order的顺序一致,就表明改成功了

对比

通过System Trace工具测试修改前后对比

system trace page fault diff

page fault减少了900,速度提升225ms,这里的时间与具体的运行环境有关系,建议多次测试

引用