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中移除