iOS 应用加载流程

2,406 阅读5分钟

封面图 2.jpg

好好学习,不急不躁,每天进步一点点;

前言

本文将从底层原理出发,讲解iOS 应用加载流程;

程序加载框架

app加载流程-第 1 页.png

源文件通过预编译,将代码词法和语法进行分析,然后交给编译器;编译之后生成一些汇编文件,链接装载进应用内,最终变成可执行文件;

动态库/静态库

静态库: 链接时,会被完整的复制到可执行文件内,会被系统多次使用,拷贝多份;

  • 静态库形式:.a 和 .framework形式

动态库: 链接时不复制,程序运行时由系统动态加载进内存,系统只加载一次,多个程序共用,节省内存空间;

  • 动态库形式:.so、.tbd(之前叫.dylib) 和 .framework

静态库与动态库的区别,主要在链接时的区别,一个是静态链接,一个是动态链接;

app加载流程-第 2 页.png

静态库/动态库生成可执行文件后,接下来验证可执行文件 (提示::尽量使用MacO工程验证,如果使用iOS工程,在终端运行可执行文件的时候,会将模拟器或者是真机运行起来,这时候会需要一些访问权限,处理起来比较麻烦;此处我使用源码工程验证)

运行源码工程,生成可执行文件:

可执行文件.png

在 Finder 中,将可执行文件,拽入终端运行;

运行可执行文件.png

使用静态库或者动态库的优势:

  • 1、减少包体积大小;
  • 2、动态库热更新(目前苹果已杜绝次方法)
  • 3、提升程序运行效率

了解了库的部分原理后,那么在应用中,库是如何加载到内存中呢???

dyld

  • dyld 链接器

是通过链接器:也就是dyld动态链接器,加载到内存中的;

App启动,会加载程序需要的库(如:libSystem库),进入runtime 运行时,注册回调函数(_dyld_objc_notify_register函数),然后加载新image(image是库,是镜像文件,库加载的过程,就是一种映射的过程,将库映射一份到内存中) ,image加载完毕,执行map_images、Load_images函数,这两个函数执行完毕后,才是main()函数执行; 这就是库的加载流程;

app加载流程-第 3 页.png

  • dyld原理

接下来,我们着重查看,调用main()函数之前,dyld的链接过程;

运行工程,在main函数之前,先执行start流程;

main之前.png

通过全局断点,看看start流程是怎样的?

load.png

start断点没有执行,但是我们发现,在调用main()函数之前,日志区打印load方法log,这就表示,load方法在main()函数之前就已经调用。

既然load函数在main()之前,那么在load函数内断点查看底层执行流程,断点在load函数内,在log区通过bt打印栈队列:

fifo.png

通过栈流程,可以看到,最先执行的是_dyld_start,并且是在dyld源码内;

  • 下载dyld源码,可以通过苹果开源网下载,目前官网最新的是dyld-852.2版本,今天也就用这个版本解说;

  • dyld源码分析

dyld源码中,搜索_dyld_start函数,我们发现_dyld_start函数,是采用汇编的形式,并且,根据不同的架构,采用不同的流程;

  • i386 架构 i386.png

  • x86 架构 x86.png

  • arm 架构

arm64.png

汇编的源码,阅读起来不是很方便,但从注释中,我们阅读到_dyld_start函数,将会执行dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)函数,所以我们可以直接定位到这个函数;

由于这是C函数命名规范,可以解读为:在dyldbootstrap文件内,执行start函数; 如此,我们先查询dyldbootstrap文件,再查询start函数;

start函数内,最终返回的是dyld::_main()函数,而dyld::_main函数源码复杂,难已阅读,需要开启上帝视角,我们直接查看它的返回值,依据返回值,由下往上逆推;

dyld::_main函数返回的resultresult的赋值方式,更多的是来自于sMainExecutable函数

  • sMainExecutable初始化:
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

//instantiateFromLoadedImage:镜像文件加载器

加载镜像文件,赋值给sMainExecutable;

  • loadInsertedDylib获取并插入动态库:
// load any inserted libraries
**if** ( sEnv.DYLD_INSERT_LIBRARIES != **NULL** ) {
**for** (**const** **char*** **const*** lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != **NULL**; ++lib) 
loadInsertedDylib(*lib);
}

// record count of inserted libraries so that a flat search will look at 
// inserted libraries, then main, then others.
//获取动态库镜像文件数量
sInsertedDylibCount = sAllImages.size()-1;

获取镜像文件后,接下来开始链接;

  • link链接镜像文件:
//开始链接镜像文件
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, **true**, ImageLoader::RPathChain(**NULL**, **NULL**), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
    gLinkContext.bindFlat = **true**;
    gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
    //判断动态库镜像文件是否存在
if ( sInsertedDylibCount > 0 ) {
    for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
        ImageLoader* image = sAllImages[i+1];
        //link 动态库镜像文件
        link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        image->setNeverUnloadRecursive();
}
......

镜像文件链接结束,开始绑定程序;

  • weakBind弱引用绑定程序:
sMainExecutable->weakBind(gLinkContext);
gLinkContext.linkingMainExecutable = false;
......

linkweakBind结束,主程序开始运行;

  • initializeMainExecutable:运行主程序
initializeMainExecutable(); 

最后通知dyld,进入main()函数

  • notifyMonitoringDyldMain:通知dyld,可以进入主程序
notifyMonitoringDyldMain();

这就是dyld的大体流程,内部有很多细节,大家可以自行查看,在此就不一一展开讲述;

app加载流程-第 4 页.png

initializeMainExecutable 程序运行

  • initializeMainExecutable源码:

app加载流程-第 6 页.png

通过流程可以看到,初始化主程序系统会做一些准备,那么是什么准备呢,接下来我们看processInitializers()这个函数;

  • processInitializers()函数:
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.

//在当前线程,对images list 中的所有 image 调用递归init,建立一个新的images list
for (uintptr_t i=0; i < images.count; ++i) {
    images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
//递归流程
if ( ups.count > 0 ) 
    processInitializers(context, thisThread, timingInfo, ups);
}

processInitializers()函数是一个递归的过程,并且init所有的image,那么调用recursiveInitialization函数,又是什么情况呢?这个流程真的是,越看越迷糊

截屏2021-08-22 下午3.26.42.png

  • recursiveInitialization函数

app加载流程-第 7 页.png

先加载依赖文件,再加载当前文件;这是因为依赖文件没有加载的话,当前文件是无法加载的;举例: ViewA内有一个子ViewB,如果子ViewB没有加载完成的话,那么ViewA就没有办法引用ViewB,就导致ViewA无法完成;

recursiveInitialization函数中,加载文件后,系统都执行了notifySingle函数,接下来我们分析这个函数;

  • notifySingle函数

app加载流程-第 8 页.png

  • sNotifyObjCInit

app加载流程-第 9 页.png

initializeMainExecutable流程图: app加载流程-第 5 页.png