在 WWDC 2016 和 2017 都有提到启动这块的原理和性能优化思路,可见启动时间,对于开发者和用户们来说是多么的重要,本文就谈谈如何精确的度量 App 的启动时间,启动时间由 main 之前的启动时间和 main 之后的启动时间两部分组成。

图是 Apple 在 WWDC 上展示的 PPT,是对 main 之前启动所做事的一个简单总结。main 之后的启动时间如何考量呢?这个更多靠大家自己定义,有的人把 main 到 didFinishLaunching 结束的这一段时间作为指标,有的人把 main 到第一个 ViewController 的 viewDidAppear 作为考量指标。不管如何,我觉得都是一定程度上可以反映问题的。

Xcode 测量 pre-main 时间

对于如何测试启动时间,Xcode 提供了一个很赞的方法,只需要在 Edit scheme -> Run -> Arguments 中将环境变量 DYLD_PRINT_STATISTICS 设为 1,就可以看到 main 之前各个阶段的时间消耗。

Total pre-main time: 341.32 milliseconds (100.0%)dylib loading time: 154.88 milliseconds (45.3%)rebase/binding time:  37.20 milliseconds (10.8%)ObjC setup time:  52.62 milliseconds (15.4%)initializer time:  96.50 milliseconds (28.2%)slowest intializers :libSystem.dylib :   4.07 milliseconds (1.1%)libMainThreadChecker.dylib :  30.75 milliseconds (9.0%)AFNetworking :  19.08 milliseconds (5.5%)LDXLog :  10.06 milliseconds (2.9%)Bigger :   7.05 milliseconds (2.0%)复制代码

还有一个方法获取更详细的时间,只需将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设为 1 就可以。

  total time: 1.0 seconds (100.0%)total images loaded:  243 (0 from dyld shared cache)total segments mapped: 721, into 93608 pages with 6173 pages pre-fetchedtotal images loading time: 817.51 milliseconds (78.3%)total load time in ObjC:  63.02 milliseconds (6.0%)total debugger pause time: 683.67 milliseconds (65.5%)total dtrace DOF registration time:   0.07 milliseconds (0.0%)total rebase fixups:  2,131,938total rebase fixups time:  37.54 milliseconds (3.5%)total binding fixups: 243,422total binding fixups time:  29.60 milliseconds (2.8%)total weak binding fixups time:   1.75 milliseconds (0.1%)total redo shared cached bindings time:  29.32 milliseconds (2.8%)total bindings lazily fixed up: 0 of 0total time in initializers and ObjC +load:  93.76 milliseconds (8.9%)libSystem.dylib :   2.58 milliseconds (0.2%)libBacktraceRecording.dylib :   3.06 milliseconds (0.2%)CoreFoundation :   1.85 milliseconds (0.1%)Foundation :   2.61 milliseconds (0.2%)libMainThreadChecker.dylib :  42.73 milliseconds (4.0%)ModelIO :   1.93 milliseconds (0.1%)AFNetworking :  18.76 milliseconds (1.7%)LDXLog :   9.46 milliseconds (0.9%)libswiftCore.dylib :   1.16 milliseconds (0.1%)libswiftCoreImage.dylib :   1.51 milliseconds (0.1%)Bigger :   3.91 milliseconds (0.3%)Reachability :   1.48 milliseconds (0.1%)ReactiveCocoa :   1.56 milliseconds (0.1%)SDWebImage :   1.41 milliseconds (0.1%)SVProgressHUD :   1.23 milliseconds (0.1%)
total symbol trie searches:    133246
total symbol table binary searches:    0
total images defining weak symbols:  30
total images using weak symbols:  69复制代码

线上如何度量 pre-main 时间

如果不依靠 Xcode 我们也是可以对 main 之前的时间进行一个考量的。当然,这个时间的度量更多关注的是开发者可控的启动段。也就是第一个图展示的 Initializer 段,在这段时间里处理 C++ 静态对象的 initializer、ObjC Load 方法的执行。

度量 ObjC Load 方法

如何计算这一段时间呢?最容易想到的就是拦截打点,如何拦截成为难点。这里把目光转向 dyld 源码,看看有什么发现。整个初始化过程都是从 initializeMainExecutable 方法开始的。dyld 会优先初始化动态库,然后初始化 App 的可执行文件。

void initializeMainExecutable()
{// record that we've reached this stepgLinkContext.startedInitializingMainExecutable = true;// run initialzers for any inserted dylibsImageLoader::InitializerTimingList initializerTimes[allImagesCount()];initializerTimes[0].count = 0;const size_t rootCount = sImageRoots.size();if ( rootCount > 1 ) {for(size_t i=1; i < rootCount; ++i) {sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);}}// run initializers for main executable and everything it brings up sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);复制代码

那么不难想到,只要在动态库的 load 函数中 Hook App 中所有的 Load 函数,然后打点就可以啦。但是,现在很多项目库都是使用 Cocoapods 管理的,并且很多都使用了 use_frameworks,那么也就是说我们的 App 并不是一个 单一的可执行文件,它是有主 image 文件和很多动态库共同组成的。按照刚才那种方法,是没办法统计到自己引入的动态库的 load 函数的执行时间的。下一步要考虑的就是,如何找到最早加载的动态库呢?然后在其 load 函数中做 Hook 就可以。

动态库的 load 顺序是与 Load Commands 顺序和依赖关系息息相关的。如图所示:

就拿我们引入的动态库来说, AFNetworking 会优先 load ,被依赖的动态库会优先 load。下面是我自己打点测试的结果,LDXlog 被 Bigger 依赖,所以 AFNetworking 最早 load ,然后是 LDXlog,依次按照 Load Commands 顺序加载。

2017-09-23 13:45:01.683817+0800 AAALoadHook[27267:1585198] AFNetworking
2017-09-23 13:45:01.696816+0800 AAALoadHook[27267:1585198] LDXLog
2017-09-23 13:45:01.707312+0800 AAALoadHook[27267:1585198] Bigger
2017-09-23 13:45:01.708875+0800 AAALoadHook[27267:1585198] Reachability
2017-09-23 13:45:01.710732+0800 AAALoadHook[27267:1585198] REACtive
2017-09-23 13:45:01.712066+0800 AAALoadHook[27267:1585198] SDWE
2017-09-23 13:45:01.713650+0800 AAALoadHook[27267:1585198] SVProgressHUD
2017-09-23 13:45:01.714499+0800 AAALoadHook[27267:1585198] 我是主工程复制代码

上面的测试让我产生一个错觉,以为动态库加载是和字母顺序相关的,其实并不是这样,因为我使用的都是 pod 管理的动态库,这个顺序被 CocoaPods 排序过了,所以才会有如此结果。在此感谢@冬瓜@monkey的干货解答。

  • 参考:www.jianshu.com/p/84936d934…

也就是说,只要把我们的统计库命名为 A 开头的库(我们的库目前均使用 pod 管理),并在内部加入打点就可以啦。再次总结下整体的思路:

  • 找到最早 load 的动态库
  • 在 load 函数中获取 App 中的所有可执行文件
  • hook 对应的可执行文件的 load 函数
  • 统计每个 load 函数的时间、全部 load 函数的整体时间
  • 上报统计分析

由于代码比较多,粘贴过来的话博客太长了,所以想了解源码的话,可以点击这个链接:github.com/joy0304/Joy…

刚才的统计还有一些要注意的事项,就是不能为了统计性能,自己却造成了性能问题,获取所有的类并且 Hook load 函数还是比较耗时的,控制不好反而增加了启动时间。

度量 C++ Static Initializers

刚才提到了初始化的入口是 initializeMainExecutable,该函数会执行 ImageLoader::runInitializers 方法,然后会调用 ImageLoader::doInitialization,最后会执行到 doModInitFunctions 方法。

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{if ( fHasInitializers ) {const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];const struct load_command* cmd = cmds;for (uint32_t i = 0; i < cmd_count; ++i) {if ( cmd->cmd == LC_SEGMENT_COMMAND ) {const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {const uint8_t type = sect->flags & SECTION_TYPE;if ( type == S_MOD_INIT_FUNC_POINTERS ) {Initializer* inits = (Initializer*)(sect->addr + fSlide);const size_t count = sect->size / sizeof(uintptr_t);for (size_t j=0; j < count; ++j) {Initializer func = inits[j];// <rdar://problem/8543820&9228031> verify initializers are in imageif ( ! this->containsAddress((void*)func) ) {dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());}func(context.argc, context.argv, context.envp, context.apple, &context.programVars);}}}}cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);}}
}复制代码

这段代码实在是长,它会从 mod_init_func 这个 section 中读取所有的函数指针,然后执行函数调用,这些函数指针对应的正是我们的 C++ Static Initializers 和 __attribute__((constructor))修饰的函数。

因为它们的执行顺序在 load 函数之后,所以可以在 load 函数中把 mod_init_func 中的地址都替换成我们的 hook 函数指针,然后再把原函数指针保存到一个全局数据中,当执行我们的 hook 函数时,从全局数组中取出原函数地址执行。在这里张贴下主要代码,更多可以参考这个链接:github.com/everettjf/Y…

void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){++g_cur_index;OriginalInitializer func = (OriginalInitializer)g_initializer->at(g_cur_index);CFTimeInterval start = CFAbsoluteTimeGetCurrent();func(argc,argv,envp,apple,vars);CFTimeInterval end = CFAbsoluteTimeGetCurrent();
}static void hookModInitFunc(){Dl_info info;dladdr((const void *)hookModInitFunc, &info);#ifndef __LP64__const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;unsigned long size = 0;MemoryType *memory = (uint32_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
#elseconst struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;unsigned long size = 0;MemoryType *memory = (uint64_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
#endiffor(int idx = 0; idx < size/sizeof(void*); ++idx){MemoryType original_ptr = memory[idx];g_initializer->push_back(original_ptr);memory[idx] = (MemoryType)myInitFunc_Initializer;}
}复制代码

刚才 hook load 函数时遇到的问题,对于 C++ Static Initializers 会不会存在呢?是存在的,我想要在一个动态库中统计 App 中所有可执行文件的 C++ Static Initializers 的执行时间,但是 dyld 中有这么一段代码:

if ( type == S_MOD_INIT_FUNC_POINTERS ) {Initializer* inits = (Initializer*)(sect->addr + fSlide);const size_t count = sect->size / sizeof(uintptr_t);for (size_t j=0; j < count; ++j) {Initializer func = inits[j];// <rdar://problem/8543820&9228031> verify initializers are in imageif ( ! this->containsAddress((void*)func) ) {dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());}func(context.argc, context.argv, context.envp, context.apple, &context.programVars);}
}复制代码

if ( ! this->containsAddress((void*)func) ) 这里会做一个判断,判断函数地址是否在当前 image 的地址空间中,因为我们是在一个独立的动态库中做函数地址替换,替换后的函数地址都是我们动态库中的,并没有在其他 image 中,所以当其他 image 执行到这个判断时,就抛出了异常。这个问题好像无解,所以我们的 C++ Static Initializers 时间统计稍有不足。

Xcode For Static Initializers

Apple 在 developer.apple.com/videos/play… 中公布了一个新的追踪 Static Initializers 时间消耗的方案, Instruments 增加了一个叫做 Static Initializer Tracing 的工具,可以方便排查每个 Static Initializer 的时间消耗。(我还没更新最新版本,暂不实践)

main 之后的时间度量

main 到 didFinishLaunching 结束或者第一个 ViewController 的viewDidAppear 都是作为 main 之后启动时间的一个度量指标。这个时间统计直接打点计算就可以,不过当遇到时间较长需要排查问题时,只统计两个点的时间其实不方便排查,目前见到比较好用的方式就是为把启动任务规范化、粒子化,针对每个任务都有打点统计,这样方便后期问题的定位和优化。

优化?

其实优化的,很多公司都有博客写道。既然谈到了启动监控就稍微写一点个人觉得比较使用的优化方案吧。

  • 目前很多项目使用 use_frameworks 的 pod 动态库,系统的动态库有共享缓存等优化方案,但是我们的动态库变多了的话会非常耗时,所以合并动态库是一个有效且可行的方案
  • 把启动任务细分,不需要及时初始化,不需要在主线程初始化的,都选择异步延时加载
  • 监控好 load 和 Static Initializers 的时间消耗,一不小心就容易出现几百毫秒的时间消耗
  • 还有很多其他公司实践的方案,我都收集了下来,可以参考:github.com/joy0304/Joy…

如何精确度量 iOS App 的启动时间相关推荐

  1. IOS App 启动时间优化实战

    当用户使用一款IOS App,打开App界面时,过长的等待时间会使用户陷入焦虑,对用户的留存率产生不良影响,虽然精致的启动页能对等待焦虑有一定的缓解作用,但是最好还是尽可能地减少App的启动时间.最近 ...

  2. iOS App 启动性能优化

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

  3. ios APP性能检测

    转载至:Bugly 首先,要明确性能检测都需要关注哪些指标,笔者列举了以下几个主要的,后面会详细说: 启动时间 内存占用量,内存告警次数 CPU使用率 页面渲染时间,刷新帧率 网络请求时间,流量消耗 ...

  4. 《iOS APP 性能检测》

    colawyeeqiu | 导语 最近组里在做性能优化,既然要优化,就首先要有指标来描述性能水平,并且可以检测到这些指标,通过指标值的变化来看优化效果,于是笔者调研了iOS APP性能检测的一些方法, ...

  5. 如何用 React Native 创建一个iOS APP?(二)

    我们书接上文<如何用 React Native 创建一个iOS APP?>,继续来讲如何用 React Native 创建一个iOS APP.接下来,我们会涉及到很多控件. 1 AppRe ...

  6. iOS app性能优化的那些事

     iPhone上面的应用一直都是以流畅的操作体验而著称,但是由于之前开发人员把注意力更多的放在开发功能上面,比较少去考虑性能的问题,可能这其中涉及到objective-c,c++跟lua,优化起来相对 ...

  7. iOS APP启动函数调用顺序~详解

    //联系人:石虎  QQ: 1224614774昵称:嗡嘛呢叭咪哄 一.OC调用 C++ 会为静态创建的对象生成初始化器,与静态语言不同,OC基于Runtime机制可以用类的名字来实例化一个类的对象. ...

  8. 提高ios app性能 初中高级实践

    这篇文章来自iOS Tutorial Team 成员 Marcelo Fabri, 他是 Movile 的一个iOS开发者. Check out his personal website or fol ...

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

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

最新文章

  1. pytorch神经网络插件或可以提高所有网络的准确率(提高权重的利用率)
  2. linux nameserver导致的故障
  3. 利用Python语言Appium启动ios app
  4. 使用内存映射文件获取巨大的矩阵
  5. New Currency Rate IOS APP 上线
  6. 如何使用TCP/IP开发网络程序
  7. 大学计算机专业分流考试,北京林业大学计算机类(入学一年后分流至计算机科学与技术、数字媒体技术、网络工程、计算机科学与技术(物联网))专业2016年在江苏理科高考录取最低分数线...
  8. 梅特勒托利多xk3124电子秤说明书_托利多电子秤设置说明书1
  9. 拼多多商家后台开放平台API
  10. 支付宝提示服务器有【中危漏洞】任意邮件伪造检测
  11. Python案例1—人民币与美元的汇率兑换V_5.0
  12. bomb和mysql_Bigger Bomb
  13. 使用python爬取电子书_怎样用python3爬取电子书网站所有下载链接
  14. 涛思数据加入龙蜥社区,携手共建时序数据库生态
  15. e480换高分屏_ThinkPadE480电脑(i5-8250u 8G内存 256G固态 高分屏0QCD 14英寸) 京东5699元...
  16. mac如何显示/隐藏文件夹 mac如何不显示灰色文件
  17. 最全最常用的RTMP、RTSP、HTTP协议流常用直播流地址
  18. TT语音:游戏社交乱象难平
  19. 浙大python网_浙大
  20. 产品经理必备神器——Axure软件试用评测

热门文章

  1. 新版OpenWrt VLAN设置方法
  2. art-法线贴图制作
  3. 华工计算机网络随堂练习答案,华工计算机网络随堂练习答案
  4. vue设置浏览器自动打开网址为 http://0.0.0.0:8080/ 的网页可能暂时无法连接,或者它已永久性地移动到了新网址。
  5. 方案详解 | 如何设计和打造敏捷研发组织
  6. 【Flutter】Dart 数据类型 List 集合类型 ( 定义集合 | 初始化 | 泛型用法 | 初始化后添加元素 | 集合生成函数 | 集合遍历 )
  7. STM32L+BC20+MQTT连接阿里云传输温湿度数据并控制继电器
  8. VMware Workstation 安装 CentOS-7 (1804) 教程
  9. 后端学习关卡三习题归纳及问题解决1
  10. python elasticsearch bulk_Elasticsearch之批量操作bulk