前言:
在前面的学习中,我们已经掌握了类结构里面的内容,并且前面我们是直接从objc_init
开始的,从这一节开始将是新的篇章,本次主要围绕的是关于app程序启动的时候,系统到底为我们做了什么?系统是如何把我们的代码“跑”起来的?这是以往的学习中都没有涉及过得,也是在开发中不需要用到的,但是确实是我们作为程序员本身,职业操守而言应该去了解的,不然人家不管是外行还是内行问起,你一无所知,然而却开发了好多年程序,着实有点可笑了,基于这部分也是比较晦涩难懂的,我看了两遍还是没有完全理解,甚至对于主流程线还是不太通畅,还是需要多看多做多探究亲力亲为才能有所收获~~于是本人本着寓教于乐
,也是帮助自己了解的心态,去更加仔细的梳理这其中的内容,本篇博客比较之前的会力求更加详细。
简单知识整理
关于库
我们在开发的时候,大多数的代码都依赖于底层的库,那么势必在装载时,事先把库文件装载到内存中。库分为动态库和静态库,两者的区别是链接
不同。在我们创建一个app的时候,如果没有编译,那么在products文件下,.app文件是红色,直到我们编译完成,这个文件才存在,点击文件位置,显示包内容,这里会根据你是mac程序还是app程序,在对应目录下会生成一个exec的可执行文件。其实我们在之前通过反编译,也是成功的把这个可执行文件变成了汇编形式的代码。
静态库是一个一个装载进去,而动态库不是直接加载,是共享
的,方便优化内存空间,减少了包的体积
,现在苹果的系统库基本都是动态库。
这个可执行文件是可以直接打印的,然而很遗憾,如果是模拟器或者真机,直接放到终端是会报错的,其实在源代码中也是有判断的,可能是要做路径处理,这里还没找到解决的办法(主要是懒也没得时间)
所以我们暂时就在mac环境下试一下,就蹭蹭看看,确实可以输出KC短细软
等一些内容。
关于dyld
简介
那么这些动态库是怎么加载到内存中的呢?这就关系到苹果一个很重要的链接器
——dyld!!!动静态库都是
依赖于这个链接器!
从宏观的角度来看,dyld是在程序启动时,加载libsystem,然后在注册notify的回调函数,加载所有的镜像文件,分别执行map_images和load_images方法,最终调用main函数。
科普:此处的image是镜像文件,也就是库文件,本身所有的库文件都存在于我们的mac系统下对应的文件下,但是在开发过程中,系统会将需要的库装载到内存中,而不是所有的库加载过去,这样也是合理的,那我们怎么看我们装载了多少库文件以及库文件所在的路径呢?很简单,在进入到main函数以后,打印image list
即可。
我原先以为是本程序所以在的路径下,其实这个corefundation是在xcode对应的架构下面的lib下的库路径下面,猜测应该是拷贝路径。
寻找
在main方法中打断点,我们发现并不能找到具体的流程信息,于是换个思路,我们知道load方法是先执行的,所以在load方法中尝试,果然,找到了,一目了然,发起的源头就是_dyld_start
,并且中间经过的步骤也是非常的清晰。
dyld底层系统库源码
本次下载的是最新版本的852,源码不可直接运行,因为依赖于更多底层的库。
上面的过程我们已经直到dyld的发起者是_dyld_start
,全局搜索,看到.s文件,很明显就是汇编了,可以看到注释里有提到calldyldbootstrap::start,是c++的写法类似于calldyldbootstrap这个类中调start方法,巴拉巴拉不管这不是重点,然后再搜索calldyldbootstrap
,找到start
方法即可,直接往下找,可以看到返回到了_main函数,这里都是跟上面的流程图接轨了,不多说。
关于_main函数
首先说明,这个_main
函数不是我们项目中的源头main函数,是dyld内部的main函数。前期大概一千行的代码讲的都是前期的准备,包括架构判断,环境,库文件路径等等。我这里只从宏观的角度来看,细节太多无暇顾及,具体每个模块的只需要看官方注释即可~~~
提一下:这里是系统级别的方法才能注册到共享缓存。。
思路转换
当代码过多,且前期的代码大多是准备代码不关乎到主流程时候,我们可以从后往前看,也就是循着结果去找逻辑——反推法
。
所有的result返回都跟sMainExecutable
这个方法有关。
sMainExecutable方法确实也做了一些系统库绑定的操作等等。那么就可以尝试继续定位。
找到sMainExecutable定义的地方
找到赋值的出处,这里第一个mh是一种格式,就叫mh,具体的你可以把可执行文件直接拖到烂苹果里面,里面展示的就是这一张张的表,这里就是读取这些表内容,不存在递归。
main函数还没执行完呢,在sMainExecutable以后,插入动态库,link主程序。
准备工作全部结束以后,run执行。
initializeMainExecutable
既然已经拿到了所有想要的数据,接下来如何run?如何处理呢?
拿到了所有的镜像文件,遍历并且初始化,可以看到每次遍历都会走runInitializers
,那么看看这个方法做了什么?
runInitializers里每次又调用了processInitializers
,这里从传参就可以看出这个方法就是核心方法。
processInitializers里每次又会调recursiveInitialization
,传参基本都相同。
recursiveInitialization
直接搜recursiveInitialization这个跳出来的会比较多,可能存在同名的构造方法,所以还是加上类型,就比较方便定位了。
这里通过存在两次调用notifySingle
的方法,从名字也可以看出来,这是单个注册通知的方法,从注释可以看出,第一个是注册依赖文件的通知,首先得加载必要的依赖文件。
问题补充1
为什么这里要这么写?
首先如果是系统库进来,那他就没有依赖库,走下面的doinit方法初始化,如果是存在依赖库进来,那么在依赖库进来的时候,就会一起初始化完毕了,然后再调用notify时没有问题的。
问题补充2
c++方法 load方法,main方法的调用顺序?如果在主程序的main方法里面写一个c++方法,为什么c+方法是先load后走c++?打印断点bt信息,明明发起者是doini方法。
load->c++->main ===== c+方法必然要写在main主程序中,而不是系统的文件中。
在进入recursiveInitialization递归初始化以后,拿到数据回调然后才会调用load方法,load方法是在notify调用的,所以必然是系统的c++先调用,然后再load,然后再到main。
接下来就看notify这个方法里的实现,在这个里面存在指针函数的调用,虽然下面有一个通知注册的方法,但是那个在注释里写了是unload images,可先不看。
全局搜索这个指针函数。
_dyld_objc_notify_init
作为第二个参数被registerObjCNotifiers
方法调用。
registerObjCNotifiers方法调用点。
竟然是_dyld_objc_notify_register
这个时候再比较一下我们第一个节课拿到的关于objc代码也就是libobjc.dylib,单个镜像文件的加载就会走到这个通知!!!
程序的加载!
离光明只差一步~
虽然说两边的回调函数已经接通,但是中间流程还是不清晰,如何更加清楚地找到中间的关联?这个时候可以从已知的libobjc.dylib中的objc_init方法去寻找思路。
方法
这里的方法很重要,既然已经知道会走到libobjc.dylib中的objc_init方法,那么直接在此处打下断点,看看前面的流程是怎么走的
具体的方法就是点进去,看他这个方法是哪个开源库里面的,一步一步验证。
libobjc.dylib objc_init
<- libdispatch.dylib _os_object_init:
<- libdispatch.dyliblibdispatch_init:
<- libSystem.B.dylib libSystem_initializer:
<- dyld ImageLoaderMachO::doModInitFunctions:
走到这,可以看到又回到了dyld,之前那边为什么不能继续探索了,因为真的是已经断了,无法再继续探索下去了,全局搜索_dyld_objc_notify_register这个搜不到了。
全局搜索doModInitFunctions
首先加载libSystem
,任何库都依赖于此。
判断是否加载完libSystem,再执行其他。
全局搜索doModInitFunctions
调用的地方,doInitialization
这个方法
再全局搜索doInitialization
这个方法,啊~~多么熟悉的陌生人,这里不就是recursiveInitialization方法吗??一切似乎都连起来了。
串联
那么还剩下一个问题,在recursiveInitialization方法中,notify方法和doInitialization方法之间的关系是什么?他们是怎么串起来的呢?
整理
有必要在这里整理一下,之前所探讨的内容。在进入到recursiveInitialization方法中的doInitialization这个方法时,必然会走之前那一套流程跳到objc_init方法初始化。这个方法里调用了_dyld_objc_notify_register,里面有三个参数(&map_images, load_images, unmap_image);map_images里包含一些分类、协议、类方法等等。。
个人理解是,其实在走到_dyld_objc_notify_register这个方法时,也拿到了map_images,但是何时调用,不得而知,调用完了以后如何又回到了notify中,notify必然是做了一个类似block的回调处理,来处理你初始化好的镜像文件。因为在image加载的过程中,每个image的加载是不一样的,内部的处理由他自己去实现罢了,主流程不用去管他。
dyld – > objc ??
什么意思?双胞胎?没错,他们就是一个意思,只是两个库里的名字不一样罢了。
在从libobjc库回来的时候,_dyld_objc_notify_register方法会调用并且赋值,全局搜索registerObjCNotifiers
registerObjCNotifiers方法被调起。
调用sNotifyObjCMapped函数
从dyld->main
直接跳过去的..
1.看dyld汇编代码
2.在主函数main之前用汇编通过寄存器打印。
后记:
1.终于在看了第二遍以后,一点点的摸索清楚了其中的大体流程,整个流程的主线大概就是从dyld的start到最后的回调notify回调完成整个通路,中间有经过好几个库的来回调用,其中最主要的就是要理清楚递归那一片notify的原理,其实就是区分系统库和其他库,做依赖关系。第一遍听还是云里雾里的,主要是因为第一,本来就不会,再加上自己没有摸索过这些代码,所在来回看的时候会更加晕第二,对计算机特别是底层基础比较薄弱。好在回看的时候可以配合视频讲解+源码自己一点点查看。要感谢KC老师的耐心讲解!
2.在拷贝跳转库的时候,我终于知道掘金里面的文字是怎么变色的了。。。之前试了好几次markdown的语法都不行?!
3.KC DXR