抖音团队去年针对系统虚拟内存缺页的情况,基于二进制重排的方案,给App启动速度提升了15%,各路大神也随后分享了几篇优质的二进制重排的文章,这里基于自己的项目做一下实践
基本原理
进程运行时使用的内存是操作系统提供的虚拟内存
,而不是直接操作物理内存
从虚拟内存
到物理内存
有一个映射表(页表)
进程的内存会进行分页
管理,以页为单位
程序启动的时候,并不会把所有内存都加载到物理内存中 ,而是用到的时候才加载,没有用到的内存,可能并没有加载到物理内存中
当程序访问到的内存地址(虚拟内存),如果还没有加载到物理内存时,就会触发Page Fault
,(对应System Trace
的File Backed Page In
),然后操作系统把数据加载到物理内存中,如果已经已经加载到物理内存了,则会触发Page Cache Hit
,后者是比较快的,这也是热启动比冷启动快的原因之一
基于上面原理. 我们的目标就是在启动的时候增加Page Cache Hit
,减少Page Fault
,从而达到优化启动时间的目的
我们需要确定,在启动的时候,执行了哪些符号,尽可能让这些符号的内存集中在一起,减少占用的页数,就能减少Page Fault
的命中次数
测试Page Fault 通过Instrument / System Trace
工具,可以查看我们的App,在启动过程中的Page Fault
数量(File Breaked Page In)
如果App比较大,Analizing
的过程会比较久,需要耐心等待
这里有个注意点,为了确保App是真正的冷启动 ,需要把内存清干净,不然结果会不太准,下图是我直接杀掉App,重新打开得到的结果
可以看到,和第一次测试差的有点多,我们可以在杀掉App后,重新打开多个其他的App(尽可能多),把原来的内存都覆盖掉,这样在重新打开App的时候,就会重新加载物理内存
确定代码执行顺序 接下来需要确定App在启动的时候,调用了哪些函数(使用了哪些符号),这里我们使用杨萧玉 写的一个工具AppOrderFiles ,使用Clang SanitizerCoverage
,通过编译器插装的方式,获取到调用函数的符号顺序
通过pod引入
并且添加编译宏OTHER_CFLAGS
和OTHER_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 AppOrderFilesoverride func viewDidLoad () { super .viewDidLoad() ... #if DEBUG 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
验证是否生效 Xcode里面Build Setting
有个Write Link Map File
,可以生成Link Map文件的选项,路径如下
1 2 3 4 5 Intermediates.noindex/xxxx.build/Debug-iphoneos/xxx.build/xxx-LinkMap-normal-arm64.txt 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
工具测试修改前后对比
page fault
减少了900,速度提升225ms
,这里的时间与具体的运行环境有关系,建议多次测试
引用