0%
Theme NexT works best with JavaScript enabled
说在前面
网络上关于怎样做App启动优化的文章太多了,有些是对自己项目有用的,有些是对自己项目无用的。本文就是看了很多文章,经过自己的筛选和总结而成的。
本文讲解在NXPlayer 这款App中怎样做的启动优化,大家可以从AppStore上下载体验。
启动优化,顾名思义就是缩短App启动的时间。本文从动态库转静态库和二进制重排两个方向入手,详细描述怎样做启动优化的。
项目纯Swift编写,使用Cocoapods管理三方库。
启动时间
那么怎样获取App启动所需要的时间呢,其实Xcode已经自带这个工具,名字就叫App Launch
。
把Profile
调整到Debug
模式。如果没有调整,在App Launch
界面是不能点击Record
按钮,也就没有办法分析启动时间。
点击App Launch
后,会自动打开App,分析完毕后,会自动关闭App,并且生成启动时间相关的数据
从上图可以清楚地看到每个阶段的耗时情况,我们以最后一行的时间,也就是App运行在前台的时间,作为启动总耗时。
动态库转静态库
把动态库转静态库,减少了动态库数量,除了可以减小加载动态库阶段的耗时,还能额外减少包大小。
并不是所有的动态库都适合转成静态库。实践中发现,如果库中有Resources
文件夹,最好不要转换。转换后Bundle
发生了变化,有些资源就会访问不到。当然也有解决方案:把动态库的资源都拷贝到Main Bundle
中,这样也会有其它方面的问题,不在这里叙说。
项目中的动态库都是Pods管理的,选择我们使用的库,然后点击Build Settings
->找到Mach-O Type
修改为Static Library
。
目前的动态库很少,可以手动修改。如果动态库多,可以在Podfile
里面添加下面的代码,然后执行pod install
。1 2 3 4 5 6 7 8 9 10 11 12 dynamic_frameworks = ['AMSMB2' ,'MJRefresh' ,'IJKMediaFramework' ,'UnrarKit' ] post_install do |installer| installer.pods_project.targets.each do |target| if dynamic_frameworks.include ?(target.name) next end target.build_configurations.each do |config| config.build_settings['MACH_O_TYPE' ] = 'staticlib' end end end
从Targets Support Files
中找到Pods-NXPlayer-frameworks.sh
脚本,把需要转换成静态库的行都注释掉。已经转换成静态库了,没有必要再往NXPlayer.app/Frameworks
在拷贝一份。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 if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/AMSMB2/AMSMB2.framework" # install_framework "${BUILT_PRODUCTS_DIR} /Alamofire/Alamofire.framework" # install_framework "${BUILT_PRODUCTS_DIR} /FilesProvider/FilesProvider.framework" # install_framework "${BUILT_PRODUCTS_DIR} /GCDWebServer/GCDWebServer.framework" # install_framework "${BUILT_PRODUCTS_DIR} /MBProgressHUD/MBProgressHUD.framework" install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework" # install_framework "${BUILT_PRODUCTS_DIR} /PLzmaSDK/PLzmaSDK.framework" # install_framework "${BUILT_PRODUCTS_DIR} /SQLite.swift/SQLite.framework" # install_framework "${BUILT_PRODUCTS_DIR} /SSZipArchive/SSZipArchive.framework" # install_framework "${BUILT_PRODUCTS_DIR} /SnapKit/SnapKit.framework" install_framework "${BUILT_PRODUCTS_DIR}/UnrarKit/UnrarKit.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/AMSMB2/AMSMB2.framework" # install_framework "${BUILT_PRODUCTS_DIR} /Alamofire/Alamofire.framework" # install_framework "${BUILT_PRODUCTS_DIR} /FilesProvider/FilesProvider.framework" # install_framework "${BUILT_PRODUCTS_DIR} /GCDWebServer/GCDWebServer.framework" # install_framework "${BUILT_PRODUCTS_DIR} /MBProgressHUD/MBProgressHUD.framework" install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework" # install_framework "${BUILT_PRODUCTS_DIR} /PLzmaSDK/PLzmaSDK.framework" # install_framework "${BUILT_PRODUCTS_DIR} /SQLite.swift/SQLite.framework" # install_framework "${BUILT_PRODUCTS_DIR} /SSZipArchive/SSZipArchive.framework" # install_framework "${BUILT_PRODUCTS_DIR} /SnapKit/SnapKit.framework" install_framework "${BUILT_PRODUCTS_DIR}/UnrarKit/UnrarKit.framework" fi
网络上关于冷启动
和热启动
的讨论很多,App要在冷启动
的情况下,测试时间才是准确的。测试不能以一次时间为准,要多几次并取平均值。具体如下:
每测试完一次需要:卸载App,退出Instruments
,退出Xcode
。
再次测试需要:打开Xcode
,按快捷键command + i
,会自动安装App并启动Instruments
,点击App Launch
进行测试。
本文在转静态库之前进行了6次,总耗时12.35秒;转静态库之后进行了6次,总耗时9.112秒。时间虽然相差很少,但也算是优化了启动时间。
二进制重排 简单介绍
为什么重排
App启动会调用很多方法或函数,这些方法或函数在内存中的表现形式是地址
,姑且统称为函数地址
。
访问函数地址
其实是从映射表中寻找物理内存中对应的内存地址,如果找到就直接访问,没有找到,系统就会立刻阻塞整个进程,触发中断异常Page Fault
。当一个Page Fault
被触发,操作系统会从磁盘中重新读取这页数据到物理内存上,然后将映射表中函数地址
指向对应的物理地址。
这些启动时的函数地址
可能是放在很多页中的,那么启动时候就会产生多次的Page Fault
。我们可以把函数地址
放在一页或几页,减少Page Fault
次数,减少阻塞次数,减少启动时间
怎样查看Page Fault
次数
打开Instruments
,找到System Trace
打开。点击左上角Record(录制)
按钮,等待App完全显示出来,点击停止按钮,会自动进行分析。
分析完成后,按照下图所示,找到File Backed Page In
,这个就是Page Fault
的次数
启动函数
上文也提到我们需要把启动时要调用的函数地址
放在一页或几页,那么首先要知道启动时调用了哪些函数地址
,才能放到页中。
找到Build Settings
搜索other swift
,并添加-sanitize-coverage=func
和-sanitize=undefined
:
LLVM内置了一个简单的代码覆盖率工具SanitizerCoverage ,需要把它集成进项目。
发现是C语言实现的,而又是纯Swift项目,不能直接支持C语言,所以新建一个SanitizerCoverage.m
文件,并让它参与编译。
在文件SanitizerCoverage.m
里面写上如下内容,用于拦截函数、方法、Block、闭包等调用。使用Dl_info
可以知道当前调用的函数名,定义了一个number
可以知道启动时一共调用了多少个函数。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 #include <stdint.h> #include <stdio.h> #include <sanitizer/coverage_interface.h> #import <dlfcn.h> void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { static uint64_t N; if (start == stop || *start) return ; for (uint32_t *x = start; x < stop; x++) *x = ++N; } void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return ; void *PC = __builtin_return_address(0 ); Dl_info info; dladdr (PC, &info); static int number = 0 ; number += 1 ; printf ("方法函数名%s 当前个数--%d--\n" ,info.dli_sname,number); char PcDescr[1024 ]; }
经过测试,不管是静态库还是动态库里面的函数,如果在启动的时候有被调用,都会被打印。每次启动项目都会有3000多个函数被调用,并且这些函数怎么都是一些乱七八糟的符号,一脸懵逼。其实是因为Swift的名字重整机制 ,被重整后,就会变成这样,名字重整不在今天的讨论范围内,不再继续往下说。1 2 3 4 5 6 7 8 9 方法函数名main 当前个数-- 1 -- 方法函数名$s8NXPlayer11AppDelegateCMa 当前个数-- 2 -- 方法函数名$s8NXPlayer11AppDelegateCACycfcTo 当前个数-- 3 -- 方法函数名$s8NXPlayer11AppDelegateCACycfc 当前个数-- 4 -- 方法函数名$s8NXPlayer11AppDelegateCMa 当前个数-- 5 -- ... 方法函数名$sSo18NSComparisonResultV8rawValueSivg 当前个数-- 3572 -- 方法函数名block_destroy_helper.9 当前个数-- 3573 -- 方法函数名$sSS5value_Sb8isForcedSS5titleSS7messagetSgWOy 当前个数-- 3574 --
好了,到这里已经能够找到启动时要调用的函数了,下一步需要把这些函数排在一起,方便加载。
重排函数
在重排函数之前,需要知道当前的函数顺序是什么,重排之后的函数顺序又是什么。这里也很简单:
在Build Settings
中修改Write Link Map File
为YES
。
执行Command + Shift + K
清空编译文件夹
执行Command + B
重新编译,编译后会生成一个Link Map符号表txt文件
依次打开Products
->NXPlayer.app
->Show in Finder
->Intermediates.noindex/NXPlayer.build/Debug-iphoneos/NXPlayer.build/NXPlayer-LinkMap-normal-arm64.txt
内容很多,搜索# Symbols:
,现在的顺序如下所示,符号顺序明显是按照Build Phases
->Compile Sources
的文件顺序来排列的,这是现在的排列顺序。如果不进行重排,文件的顺序决定了方法、函数的执行顺序。1 2 3 4 5 6 7 8 9 10 11 # Symbols: # Address Size File Name 0x100007420 0x00000074 [ 1] ___sanitizer_cov_trace_pc_guard_init 0x100007494 0x000000B0 [ 1] ___sanitizer_cov_trace_pc_guard 0x100007544 0x00000038 [ 2] _$s8NXPlayer18AboutBodyViewModelC8fileNameSSvpfi 0x10000757C 0x00000084 [ 2] _$s8NXPlayer18AboutBodyViewModelC8fileNameSSvpACTK 0x100007600 0x000000A8 [ 2] _$s8NXPlayer18AboutBodyViewModelC8fileNameSSvpACTk 0x1000076A8 0x00000080 [ 2] _$s8NXPlayer18AboutBodyViewModelC8fileNameSSvg 0x100007728 0x00000094 [ 2] _$s8NXPlayer18AboutBodyViewModelC8fileNameSSvs 0x1000077BC 0x00000064 [ 2] _$s8NXPlayer18AboutBodyViewModelC8fileNameSSvM ...
接下来就是正式重排了
在项目目录下新建order.txt
文件,并在Build Settings
->Order File
里面设置一下路径
上面打印出来的启动函数
都是以'$s'
开头的,直接放在order.txt
文件中是无法被识别的,需要在前面加上_
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _ $s8NXPlayer11AppDelegateCMa _ $s8NXPlayer11AppDelegateCACycfcTo _ $s8NXPlayer11AppDelegateCACycfc _ $s8NXPlayer11AppDelegateCMa _ $s8NXPlayer11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtFTo _ $s8NXPlayer11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtF _ $sSo8UIWindowCMa _ $sSo8UIWindowC5frameABSo6CGRectV_tcfC ... _ $sSo18NSComparisonResultVABSYSCWl _ $sSo18NSComparisonResultVMa _ $sSo18NSComparisonResultVMa _ $sSo18NSComparisonResultVSYSCSY8rawValue03RawD0QzvgTW _ $sSo18NSComparisonResultV8rawValueSivg _ $sSo18NSComparisonResultVSYSCSY8rawValue03RawD0QzvgTW _ $sSo18NSComparisonResultV8rawValueSivg _ $sSS5value_Sb8isForcedSS5titleSS7messagetSgWOy
利用Xcode重新编译安装App,重复第一个步骤,查看NXPlayer-LinkMap-normal-arm64.txt
内容如下,发现确实重排了函数加载顺序1 2 3 4 5 6 7 8 9 # Symbols: # Address Size File Name 0x100007380 0x0000003C [ 19] _$s8NXPlayer11AppDelegateCACycfcTo 0x1000073BC 0x000000AC [ 19] _$s8NXPlayer11AppDelegateCACycfc 0x100007468 0x00000044 [ 19] _$s8NXPlayer11AppDelegateCMa 0x1000074AC 0x00000124 [ 19] _$s8NXPlayer11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtFTo 0x1000075D0 0x00000268 [ 19] _$s8NXPlayer11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0j6LaunchI3KeyaypGSgtF 0x100007838 0x00000068 [ 19] _$sSo8UIWindowCMa 0x1000078A0 0x00000064 [ 19] _$sSo8UIWindowC5frameABSo6CGRectV_tcfC
可以看到,经过二进制重排确实减少了Page Fault
的次数,总时间也有所减少。
说在最后
总结
把动态库转静态库,减少了动态库数量,减小加载动态库阶段的耗时。
让启动函数
重新排列,让排列更紧凑,减少了Page Fault
的次数,减少启动耗时
重排完成后需要恢复现场
把Profile
调整到Release
模式。
把Write Link Map File
改为NO
,
把other swift
的配置删除掉。
把SanitizerCoverage.m
从Compile sources
中移除