导读
本文将带您了解iOS APP从点击图标到显示画面的大致过程,本文只不深入解析相关源码,相关源码解析会在后续的章节详细讲解。
我们为什么要了解APP启动流程?
在开发过程中,随着业务的不断增加,代码量也不断增加,APP体量越来越大,APP的启动速度越来越慢,启动速度慢导致用户体验差,那么新需求就来了,降低APP的启动时间!
如何降低APP的启动时间呢?
这个问题可以拆分成
1、APP启动时间降低到多少才能让用户有飞一般的体验
2、如何降低APP启动时间?
在回答问题之前,我们先了解下怎么测量启动时间:
在Xcode菜单栏:Product->Scheme->Edit Scheme->Environment Variables
设置:key:DYLD_PRINT_STATISTICS value:1
然后运行工程,就会将启动时间以及过程耗时打印出来了。
当然了,打印的时间都是分模块的,如果要具体要每个函数的调用时间,就需要借助其他的工具,比如自己写一套hook objc_msgSend(),这个难度非常大,简单点的方式就是借助Xcode自带的Instrument工具,具体怎么操作就不展开讲解,有兴趣的同学可以自己找找资料。
回到之前的问题上来,先回答第一个问题:
在WWDC2016上苹果官方给出的APP启动时间建议是低于400ms,这样可以确保在 Springboard 的应用启动动画结束前,你的应用就做好准备可以使用了。而且如果启动时间超过20秒则会被系统杀死,意味着你的APP将启动失败。
Apple suggest to aim for a total app launch time of under 400ms and you must do it in less than 20 seconds or the system will kill your app
问题1已经解决,那么我们回到问题2。
App启动一般分两种,冷启动和热启动。
热启动是指 ,App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况 下,用户重新启动进入 App 的过程,这个过程做的事情非常少。(即有数据缓存为热启动)
冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给 它启动的情况。这是一次完整的启动过程。(重启设备后,任意APP的启动都为冷启动)
一般情况下,热启动由于有缓存的原因,速度都非常快,肯定是低于400ms的,这种情况我们不需要优化。
OK,重点来了,冷启动!
现在问题2变成了:如何降低冷启动的时间?
不知道!game over结束!
身为一个求生欲非常强的猿,我们肯定不能就这样回答结束。
想解决问题,就要先了解问题的本质。进入正题,启动流程!
我们先用Xcode新建一个Single View App工程,取名Launch_Demo,在工程中能接触到最开始执行的代码在main.m文件中的main()函数,我们先在main函数中打个断点,然后启动模拟器:
修改debug选项,用于显示汇编指令代码,然后点击左侧的start
可以看到图中的汇编代码是断在一个libdyld.dylib动态库的一个start()函数中,那libdyld.dylib这又是什么呢?
在这之前我们先了解下什么是dyld,dyld(dynamic link editor)是苹果系统内核中的动态连接器,是苹果系统的重要组成部分,负责加载可执行文件到内存中。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节,dyld下载地址:http://opensource.apple.com/tarballs/dyld
我们先把源码下载下来,目前最新版本是dyld-852.2
打开dyld源码工程,全局搜索_dyld_start,然后在dyldStartup.s这个汇编文件中找到了具体的实现,代码包含了各种架构的实现,我们只需要关注iOS的实现,摘取了arm64架构的实现如下:
可以看到有一行代码
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
从注释我们可以知道这句代码就是调用了start()函数,咦,这不就是上面我们断点里的那个start()函数吗?
再全局搜索start(,结果非常多,我们找跟bootstrap有关系的,然后我们在dyldInitialization.cpp文件中找到函数的具体实现:
start()函数做了一堆初始化的事情,具体的先不管,看重点,return dyld::_main()
初始化工作完成后,此函数调用到了dyld::_main(),再将返回值传递给__dyld_start去调用真正的main()函数。
光标放到_main()的位置,右键选中Jump to Definition直接跳转到main()函数的具体实现。然后就懵逼了,900行代码的具体实现(心里一阵草泥马狂奔而过)。
代码太长了,我们找重点,一番查找,找到如下一段代码:
看注释:
// run all initializers (执行所有的初始化器)
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main() (通知任何正在监听的进程,当前进程要进入main()函数了)
notifyMonitoringDyldMain();
很好,已经快要接近我们的目标了,继续往下读源码找重点:
这是什么?
看注释:find entry point for main executable (找到主程序的入口,就是我们的工程里的main()函数)。
目前都还是静态代码的分析,我们用一个demo来验证一下,我们都知道load()方法会在main之前调用,所以我们通过在load方法里打一个断点的方式追踪一下程序的调用堆栈。
打开之前创建的demo工程,在ViewController中新增一个+(void)Load方法,然后将断点打到方法里面,运行程序。
从图中可以看到,函数的调用顺序和我们分析的差不多,我们只分析到dyld::_main()函数,上图的很多函数调用都是_main()内部调用的。细心的同学会发现dyld最终调用了notifySingle,然后就进入libobjc.A.dylib的load_image函数了,我们熟悉的runtime就是在这个动态库里的,那么这部分是怎么跳转的呢?
看到这里,很多读者肯定会觉得很困惑,这个流程感觉什么都没有啊,没有干货啊,被大家熟知的runtime在哪启动的,类在哪加载的,category什么时候加载,load方法什么时候调用等等。
不要急,APP的启动是个非常复杂的过程,涉及到的源码非常多,不仅仅是dyld库被调用,还有上图的libobjc.A.dylib,以及还没有出现的libSystem.dylib libdispatch.dylib等。
我们目前了解到的还只是冰山一角,真正的想要完全吃透这一块是要有很多的知识积累,比如用户态和内核态,mach-o的一些知识,静态库与动态库相关知识,虚拟内存,共享缓存等等等等。
当然我们目前分析的都是基于用户态的启动过程,在内核态还有另外一套启动过程来启动dyld,由于iOS开发不涉及内核开发,所以不在此深入探究,有兴趣的同学可以自己找资料学习相关知识。
总结下今天的收获,目前我们了解到的调用流程是这样的:
_dyld_start --> dyldbootstrap::start()--> dyld::_main() -->_main()函数里一堆逻辑代码 -->主程序main()
中间省略的调用逻辑,最少包含了:
1、Xcode环境配置相关信息的检查
2、静态库、动态库的加载以及链接
3、runtime的初始化
4、类的初始化
5、catagory的初始化
。。。。。。。。。等等
这些流程将会分为几个小节进行详细的源码分析带领大家了解。
下一篇:重学iOS系列之APP启动-dyld(二)将带大家拉开dyld的神秘面纱,领略下苹果官方的开发是怎么编写一个app的启动逻辑的。
网友评论