OC底层原理10之应用的加载dyld

703 阅读13分钟

本章内容

  1. APP启动的简单了解
  2. 二进制重排(启动时间优化)
  3. 什么是DYLD,动静态库的区别是什么
  4. DYLD链接动静态库的流程

APP启动

我们都知道APP启动主要分为两个阶段:pre-main和main之后,而APP的启动优化也主要是在这两个阶段进行的,main之后的优化无非就是:1. 减少不必要的任务,2.必要的任务延迟执行,例如放在控制器界面等等。main之后的启动时间优化很容易做到,本文不赘述。

APP启动的大致过程:APP启动 -> 加载libSystem -> Runtime注册回调函数 -> 加载image(镜像文件) -> 执行map_images和load_images方法 -> 调用main函数。

查看pre-main耗时,添加DYLD_PRINT_STATISTICS到(Edit Scheme -> Run -> Arguments -> Environment Variables)就可以在控制台看到耗时

缺页错误

需要了解,因为这是程序加载运行的机制,这样你就会明白为什么要做启动优化。

我们应该要清楚:任何程序的运行是因为它在物理内存的缘故,也就是这个程序最终是加载到物理内存才得以运行,它加载到物理内存,也就是虚拟内存映射到物理内存的过程其实是懒加载方式,而这个过程是系统到CPU的交互(翻译)过程。而这个过程因为应用程序是懒加载的映射方式即有多少拿多少,所以我们会通过一页一页的方式也就是page方式去加载,iOS的页的大小是16kb,而macOS是4kb。因为是懒加载方式,所以如果需要用到的时候发现物理内存中没有这时候会报出page fault的缺页错误,然后缺的页会再加载放在物理内存。而这个过程很短,可能30ms,也可能是10ms。

pre-main(main函数前)

APP在执行main函数之前,我们也能想象到,无非就是做很多准备工作而已,例如:加载我们需要的库啊,系统自己调用加载一些依赖库啊,加载类到内存中去啊,加载分类方法并插入到类的方法列表中啊等等。但是具体的流程是什么呢?它是如何加载呢?这就想象不到了,这个过程我们其实不必要记下来,只要能了解就行。因为记下来对你没有任何帮助,除非你是开发编译器,IDE的。

二进制重排

二进制重排就是重排的符号即函数的顺序。排序的逻辑就是:启动时候必须要调用的方法排在一起,不去随机分布,这样启动的时候就不会发生中断即page fault

方案

当我们启动APP时,就会需要加载很多的页,正常都会有几千页,虽然一页耗时少,但是那个时刻要加载那么多页数,耗时会更长了。我们可以根据InstrumentsSystem Trace找到Main Thread进行查看应用的page in也就是启动加载页数。苹果自用了二进制重排方案就可以优化这个的耗时,例如抖音的二进制重排,怎么找到所有的函数加载,将不必须在前面执行的函数放在后面。

二进制重排的困难点

这是二进制重排的核心,也是最难点,order文件的符号顺序怎么来的,如何收集APP启动的符号顺序。可以通过HOOK,运行时的trace。但是你能保证能100%获取吗?抖音说他们在研究编译时的插桩,希望能获取100%的执行函数。目前会用clang插桩的方式进行获取80%左右,也就是抖音的方案,本文没有,但是后续会加上

苹果的objc进行的二进制重排

可以下载objc源码进行查看苹果的二进制重排 image.png

二进制重排流程

  1. 应用程序的启动时刻所加载的顺序是按照Build PhasesCompile Sources的顺序
  2. Build Settings中搜索 Write Link Map File设置为YES,就是写入。然后就是Path to Link Map File的地址。
  3. 找到build里面的txt格式的文件,如果是模拟器则为x86_64结尾的。这个就是现在的执行顺序

image.png 4. 打开终端,cd到目录下创建order文件,例如:touch test.order 5. 将你想要排序的函数依次写进去,然后再在Build Settings中的Order File的路径填写为test.order的文件路径,最后编译一下。

image.png

DYLD,动态链接器

dyld:是链接器,我们的各种库是怎么加载映射到内存中去的,其中dyld起了至关重要的作用,dyld从APP启动到进入main函数究竟是怎么做的呢?

编译的过程: 源文件 -> 预编译(相应语法等分析) -> 编译 -> 汇编 -> 链接(此过程也会链接动静态库) -> 可执行文件

APP启动的大致过程:APP启动 -> 加载libSystem -> Runtime注册回调函数 -> 加载image(镜像文件) -> 执行map_images和load_images方法 -> 调用main函数。

动态库和静态库

动静态库的区别就是他们链接的区别。一个是静态链接,一个是动态链接

静态库:.a,.lib等。静态库都是一个个排好的,例如 ABCBDA,它是已经被排序好且可能会有重复,所以它有个缺点就是内存的浪费

动态库:.dll,.framework等,它跟静态库的区别就是,例如上面的例子 B 和 A 在其内存只有一份,但是当A和C之间用到了B,以及C和D之间用到了B,所以B会是他们共享的。所以苹果大部分会用动态库

我们是怎么找到其执行函数的

我们知道load,以及cxx函数都是在main函数之前就会被调用的。所以我们可以断点load函数或Cxx函数里,然后查看其调用栈。然后得知其大致流程就是这样,而且一切的一切就是从dyld_start开始的。希望你能对照这个函数调用顺序去观察下面的流程

image.png

DYLD 源码剖析流程

dyld源码下载、dyld所依赖的libSystem以及libdispatch

我们首先要明确我们的目的是什么:我们看这个流程是为了看APP启动到main函数前,也就是dyld是如何将images(镜像文件:如动静态库等)链接到内存中去的。而在objc_init的时候是做了什么操作去调起dyld,以及dyld又如何回调至objc中。

1. _dyld_start

首先看_dyld_start函数,是用汇编写的,其实没什么需要展示的,无非就是分不同的框架,但是执行的内容都一样。最主要的就是调用start方法,以及dyld层加载结束后调用我们的main方法。这里源码不展示可以自己去源码搜这个方法看

# call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
有一个这样的备注,dyldbootstrap是命名空间,start是函数名,可以根据这个搜start方法

2. dyldbootstrap::start

找到start方法后,我们什么也不看记得上面我给的函数调用栈,下一步就是dyld的main函数了。其实这个函数也是其中间过程,我们不必关心

3. _main 重要

这个整段只有正文重要,其他都是闲扯。

到这里已经是dyld重中之重了,这个函数的代码行数为849近1000行代码。其实上面的函数调用栈的最大作用也就是引导我们到这里。而这里也大概就是dyld的执行流程了,包括主程序的实例化再到通知进入程序的main函数这个过程。

大致流程

闲扯:(这短话不需要看)直接总结出来,因为这整个过程太过于繁琐复杂,而这个dyld的加载流程也是很多公司面试也会提起的,甚至于某些公司会问dyld2与dyld3的区别,虽然知道这个过程没什么卵用,对于你的开发帮助也不存在,奈何有的面试官就感觉知道这个流程就是比你厉害一样,实质无任何卵用,你问问哪些面试官,他会开发编译器吗,还是说要找你开发编译器呢。吐槽归吐槽,流程我们还是要了解的,毕竟知道加载流程你就知道为什么load方法和CXX方法在main函数前面,而且还会明白下一过程是类加载的过程。

正文main函数包括了: 这个比较重要需要了解看看

可以看到,除了前期的准备,最主要的流程就是5-11。其实这一流程只是大概流程,其中还包括了符号绑定一些乱七八糟的东西但是这个不重要。而最关键的就是10,就是跑起来了跑起来我们的主程序

  1. 条件准备:环境,平台,版本,路径,主机信息等等;
  2. 确定是否有共享缓存并去加载(一般是非模拟器情况)
  3. 载入GDB调试器通知。(老版本的不重要,没用,不知道这个名词没关系)
  4. 添加dyld到UUID列表中,启用堆栈符号化。(没用,不需要知道)
  5. 实例化主程序,instantiateFromLoadedImage(镜像文件加载器,就是以mach-o的header方式加载,就是mach-o的格式。这一块我们将一个可执行文件放在烂苹果里面,看到mach-o的格式就是这一块按照这样的格式加载的。如果想要了解去慢慢摸索到sniffLoadCommands这个方法你会发现是一一对应)
  6. 加载任何插入的库,loadInsertedDylib
  7. link(链接)主程序
  8. link 镜像文件(前面插入的库)
  9. 弱引用绑定主程序
  10. 运行所有初始化的程序。initializeMainExecutable
  11. 通知dyld可以进入main函数了。notifyMonitoringDyldMain

4.initializeMainExecutable 不重要,但是是中间过渡

从3引入到4,其实这个方法往下的流程里面就是去调用map_images、load_images等,但其实它也是个中间流程。其实本不想放这个源码,因为它也就那回事。

源码

void initializeMainExecutable()
{
	// record that we've reached this step
	gLinkContext.startedInitializingMainExecutable = true;

	// run initialzers for any inserted dylibs
	ImageLoader::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]);
	
	// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
	if ( gLibSystemHelpers != NULL ) 
		(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

	// dump info if requested
	if ( sEnv.DYLD_PRINT_STATISTICS )
		ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
	if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
		ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

5. runInitializers 不重要,但是是中间过渡

该方法我也不知道是干啥的,但是我们可以看到最重要是这两个方法的调用

源码

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
	uint64_t t1 = mach_absolute_time();
        // 线程,mach_port其实对于我们线程间通讯也可以用到
	mach_port_t thisThread = mach_thread_self();
	ImageLoader::UninitedUpwards up;
	up.count = 1;
	up.imagesAndPaths[0] = { this, this->getPath() };
        // 这个方法就是递归去实例化我们哪些镜像文件。然后看它的实现
	processInitializers(context, thisThread, timingInfo, up);
        // 这个方法最主要调用notifyBatchPartial,这里我们暂且记住有这个方法调用
	context.notifyBatch(dyld_image_state_initialized, false);
	mach_port_deallocate(mach_task_self(), thisThread);
	uint64_t t2 = mach_absolute_time();
	fgTotalInitTime += (t2 - t1);
}

6. recursiveInitialization 递归加载

希望你看到这里的时候能够想起前面那个函数调用栈的图,看看里面的函数调用顺序。我们走到这里就意味着上面的图已经完成了使命。你们看到这里肯定也是困惑。为什么我走这个流程是什么意思呢?其实不要着急,后面会说

源码

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);
    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles
        fState = dyld_image_state_dependents_initialized-1;
	try {
            // initialize lower level libraries first
            // 基础文件的加载,也就是依赖文件
            for(unsigned int i=0; i < libraryCount(); ++i) {
                ImageLoader* dependentImage = libImage(i);
                if ( dependentImage != NULL ) {
                // don't try to initialize stuff "above" me yet
                    if ( libIsUpward(i) ) {
                        uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
                        uninitUps.count++;
                     }
                     else if ( dependentImage->fDepth >= fDepth ) {
                     //依赖文件的递归初始化
                     dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                     }
                }
           }
           // record termination order
           if ( this->needsTermination() )
           context.terminationRecorder(this);
           // let objc know we are about to initialize this image
           uint64_t t1 = mach_absolute_time();
           fState = dyld_image_state_dependents_initialized;
           oldState = fState;
           //我们要看这个函数,这个函数是对一系列,mach-o文件注册到位。但是你自己去查看的时候就会发现这个函数我们能找到实现,其实是一个赋值操作。我们通过这个方法要看到一个重点
           //sNotifyObjCInit这个方法的调用,而他也是一个赋值操作,看到registerObjCNotifiers方法里面对它赋值
           context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
           
           // initialize this image
           // 我们要看这个函数,这个与后面呼应
           // 这里去调用CXX方法,但是这里是调用的objc的CXX方法,而不是我们工程的。所以load在CXX前面
           bool hasInitializers = this->doInitialization(context);
           
           // let anyone know we finished initializing this image
           fState = dyld_image_state_initialized;
           oldState = fState;
           // load方法的调用
           context.notifySingle(dyld_image_state_initialized, this, NULL);
           
           if ( hasInitializers ) {
               uint64_t t2 = mach_absolute_time();
               timingInfo.addTime(this->getShortName(), t2-t1);
            }
       }
       catch (const char* msg) {
           // this image is not initialized
           State = oldState;
           recursiveSpinUnLock();
           throw;
           }
	}
	recursiveSpinUnLock();
}

7. registerObjCNotifiers

然后我们通过这个方法可以找到一个很重要的函数_dyld_objc_notify_register,也就是这个函数调用了registerObjCNotifiers

源码

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
        //map_images的赋值
	sNotifyObjCMapped	= mapped;
        //load_images的赋值
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
        //注册以后就开始去调用 map_images。
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
                        
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}

其他函数 doInitialization 以及 doModInitFunctions

doInitialization 调用了 doModInitFunctions(去加载依赖库,例如libsystem和libdispatch) 而doInitialization是谁调用呢,请看方法6。

总结1

我相信看这个DYLD流程的人肯定会迷惑由1-7,这几个函数调用有什么意义呢?其实是有意义的,我最终要引出的函数是_dyld_objc_notify_register,为什么要引出这个函数,因为我们可以看到objc源码中objc_init也就是objc初始化方法中有一个调用就是该函数。这里是objc -> dyld的过度。这么说也不对。其实应该是dyld调用了objc_init方法。为什么这么说请看下图

image.png image.png

总结2

根据上图得到流程,也就是我们本文的核心应用程序加载的具体过程: dyld -> libSystem -> libdispatch ->objc。

而从函数调用栈 frame13 - frame4都是在dyld中进行的。可见其重要性,但是要提醒的是dyld其实有个地方在加载依赖库的时候会判断一下libSystem库是否加载,如果未加载会让他加载不然就报错。因为libSystem是其底层依赖库,是第一要加载的库

补充

dyld 最主要的执行了两个方法。而且dyld其实对于load方法加载还有CXX加载都在里面,但是呢dyld加载的CXX方法是objc的CXX方法,而objc是自己去调用自己的CXX方法,这个在objc_init方法里面有解释。本文没有去赘述

map_images与load_images什么时候调用

因为每个镜像文件的加载时机我们是不知道的,所以当镜像文件加载完毕后得有个回调(下句柄)告诉其处理完毕,接下来dyld得需要有个状态去标识,所以我们必须要用notifySingle进行通知。

  1. map_images :镜像文件的加载,引出read_images。该方法很重要
  2. load_images :load方法的加载

map_images 是在notifyBatchPartial调用的,也就是注册完通知就立马去调用。 而load_images 是在notifySingle调用。

结束语

整个dyld -> main 函数,这个过程,我们只需要了解大概就足够。我们要知道,dyld最关键就是调用了两个方法map_images(镜像加载包含类加载)与load_images(load方法加载)。