iOS APP中pre-main都做了什么

1. 编译与连接的过程

<<程序员的自我修养>>中将了程序的编译与链接的过程的详细内容, 这里简单总结一下:

源文件->预处理->编译->汇编->链接
复制代码

其中主要的步骤功能如下:

  1. 源文件是我们常见的`c/c++/oc等源文件
  2. 预处理: 替换宏, 预编译指令等等, 生成.i
  3. 编译: 转化成汇编文件
  4. 汇编: 将汇编文件转化成二进制的机器码, 也就是 目标文件.o
  5. 静态链接: 将.o中合并, 符号解析与重定位, 并生成可执行文件.(依赖的外部动态库符号在启动以后, 由动态链接器进行动态链接)

链接的细分:

  1. 静态链接: 编译构建二进制时进行, 主要是符号解析, 重定位, 合并段, 创建虚拟内存空间等过程.
  2. 动态链接: 运行时, 依赖动态链接器dyld, 发生启动阶段, 在pre main时, 一般依赖操作系统.

2. 静态库与动态库

<<程序员的自我修养>>将目标文件分成有三种形式:

  • 可执行目标文件. 即我们通常所认识的, 可直接运行的二进制文件。
  • 可重定位目标文件. 包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件.
  • 共享目标文件. 它是一种在加载或者运行时进行链接的特殊可重定位目标文件.

我们常见的静态库就是其中的可重定位目标文件, 动态库就是共享目标文件. 一句话说明静态库和动态库:

  1. 静态库: 编译的中间体, .o的集合或者ar压缩包
  2. 动态库: 编译完全体, 与二进制可执行文件格式几乎一致(需要在运行时进行链接)

其他用的与动态库/静态库相关的内容:

  1. framework: iOS中的framework是一种包装格式, 具体是动态库or静态库,需要查看内部包装的二进制的格式
  2. xcframework: 多个库的外壳, 并包含一个配置索引表, xcode会识别根据APP编译的架构选择具体的framework
  3. Fat 二进制库: 多CPU架构的Mach-O合并的成的一个Mach-O文件.

3. dyld的执行过程

iOS APP启动

以下内容参考opensource.apple.com/tarballs/dy… 中的852.2的dyld源码. 方法入口是:__dyld_start:


//
// This is code to bootstrap dyld.
// This work in normally done for a program by dyld and crt.
// In dyld we have to do this manually.
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader,
                int argc, 
                const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, 
                uintptr_t* startGlue) {

    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // 如果dyld 需要 rebase, 传入 dyld的 Mach-O Header, 进行Rebase
	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    rebaseDyld(dyldsMachHeader);

    // 启动时 的环境变量
	// kernel sets up env pointer to be just past end of agv array
	const char** envp = &argv[argc+1];
	
	// kernel sets up apple pointer to be just past end of envp array
	const char** apple = envp;
	while(*apple != NULL) { ++apple; }
	++apple;

	// set up random value for stack canary
	__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
	// run all C++ initializers inside dyld
	runDyldInitializers(argc, argv, envp, apple);
#endif

	_subsystem_init(apple);

	// now that we are done bootstrapping dyld, call dyld's main
	uintptr_t appsSlide = appsMachHeader->getSlide();
	return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
复制代码

简单来说就是:

  1. 调用__dyld_start
  2. dyldbootstrap::start方法, 最关键的入参是const dyld3::MachOLoaded* appsMachHeader, 也就是App主二进制的Mach-O Header
  3. 然后就交给dyld::_main正式处理

该方法的逻辑比较复杂基本是pre-main的核心流程:


// Entry point for dyld.
// The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to

/*
从注释中能看到, 方法最终会返回 主二进制可执行文件的入口函数 main 函数
因此这个函数需要做前期的dyld的工作:
1. 环境变量的配置
2. 共享缓存加载, dyld3对启动闭包的编译/加载
3. 主二进制Mach-O的加载
4. 依赖的动态库加载
5. 链接主程序 -- rebase & rebind
6. 链接其他的插入的动态库 -- rebase & rebind
7. dyld的Mach中的初始化方法与, +load方法
8. 查找并返回, 主程序的入口函数main
*/

// ImageLoaderMachO is a subclass of ImageLoader which loads mach-o format files.
typedef ImageLoaderMachO* __ptrauth_dyld_address_auth MainExecutablePointerType;
static MainExecutablePointerType	sMainExecutable = NULL;

uintptr_t
_main(const macho_header* mainExecutableMH,
	  uintptr_t mainExecutableSlide,
	  int argc, const char* argv[],
	  const char* envp[],
	  const char* apple[],
	  uintptr_t* startGlue)
{
	//1. 环境变量配置
	// 前面都是操作 mainExecutableMH : 主二进制 Mach-O Header
	getHostInfo...
	//2. 共享缓存, dyld3对启动闭包的编译/加载
	sClosureMode & ClosureBuild
	// 3.主二进制Mach-O的加载 -- 从Mach-O Header --> Mach-O的加载
	  sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
	// 4. 依赖的动态库加载 -- 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);
		}
		
	// 5. 链接主程序 -- rebase & rebind
			// link main executable
		gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
		if ( mainExcutableAlreadyRebased ) {
			// previous link() on main executable has already adjusted its internal pointers for ASLR
			// work around that by rebasing by inverse amount
			sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
		}
#endif
		link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
}

	// 6. 链接其他的插入的动态库
		// link any inserted libraries
		// do this after linking main executable so that any dylibs pulled in by inserted 
		// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
		if ( sInsertedDylibCount > 0 ) {
			for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
				ImageLoader* image = sAllImages[i+1];
				link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
				image->setNeverUnloadRecursive();
			}
			if ( gLinkContext.allowInterposing ) {
				// only INSERTED libraries can interpose
				// register interposing info after all inserted libraries are bound so chaining works
				for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
					ImageLoader* image = sAllImages[i+1];
					image->registerInterposing(gLinkContext);
				}
			}
		}
		
		7. 主程序初始化方法
		// run all initializers
		initializeMainExecutable(); 

		// notify any montoring proccesses that this process is about to enter main()
		notifyMonitoringDyldMain();
		
		8. 查找并返回, 主程序的入口函数main
}
复制代码

从注释中能看到, 方法最终会返回 主二进制可执行文件的入口函数 main 函数
因此这个函数需要做前期的dyld的工作:

  1. 环境变量的配置
  2. 共享缓存加载, dyld3对启动闭包的编译/加载 –> buildClosureCachePath
  3. 主二进制Mach-O的加载 –> instantiateFromLoadedImage
  4. 依赖的动态库加载(在环境变量中) –> loadInsertedDylib
  5. 链接主程序 — rebase & rebind –> rebase() & link()
  6. 链接其他的插入的动态库 — rebase & rebind
  7. 主程序的初始化方法(+load方法也会在这里执行)– > initializeMainExecutable
  8. 查找并返回, 主程序的入口函数main

3. 主程序初始化 — initializeMainExecutable

主程序初始化的工作:

// 初始化主程序
void initializeMainExecutable() {
	// record that we've reached this step
	gLinkContext.startedInitializingMainExecutable = true;
	...
	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]);
	... 
}

// runInitializers() is normally called in link() 
// but the main executable must run crt code before initializers
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
    uint64_t t1 = mach_absolute_time();
    mach_port_t thisThread = mach_thread_self();
    ImageLoader::UninitedUpwards up;
    up.count = 1;
    up.imagesAndPaths[0] = { this, this->getPath() };
    processInitializers(context, thisThread, timingInfo, up);
    context.notifyBatch(dyld_image_state_initialized, false);
    mach_port_deallocate(mach_task_self(), thisThread);
    uint64_t t2 = mach_absolute_time();
    fgTotalInitTime += (t2 - t1);
}


void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    // let objc know we are about to initialize this image
    uint64_t t1 = mach_absolute_time();
    fState = dyld_image_state_dependents_initialized;
    oldState = fState;
    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

    // initialize this image --> 初始化这个 image!!! 调用objc_init!!!
    bool hasInitializers = this->doInitialization(context);

    // let anyone know we finished initializing this image
    fState = dyld_image_state_initialized;
    oldState = fState;
    context.notifySingle(dyld_image_state_initialized, this, NULL);
}
复制代码

基本的调用流程如下:

  1. initializeMainExecutable
  2. ImageLoader::runInitializers
  3. ImageLoader::processInitializers
  4. ImageLoader::recursiveInitialization
  5. ImageLoaderMachO::doInitialization –> Mach-O的init函数, C++ static 对象初始化执行
  6. context.notifySingle(dyld_image_state_initialized, this, NULL); –> OC的+load方法执行
ImageLoaderMachO::doInitialization 线条

另外一条线_objc_init()的调用链条:

bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
    // mach-o has -init and static initializers
    // mach-o 如果有init段, 或者c++ 静态初始化方法在这里执行!!!
    doImageInit(context);
    doModInitFunctions(context);
    return (fHasDashInit || fHasInitializers);
}
复制代码

ImageLoaderMachO::doModInitFunctions中会调用libSystem_initializer方法, 后续的流程:

libSystem_initializer -> libdispatch_init -> _os_object_init() -> _objc_init()

libSystem_initializer 可以通过符号断点 _objc_init 获取调用栈

因此, 会进行OC runtime的初始化, 例如注册OC类, Category, Selector唯一性检查等等, 调用完整流程如下:

_dyld_start -> dyldbootstrap::start -> dyld::_main -> dyld::initializeMainExecutable
-> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> ImageLoader::doInitializers 
-> libSystem.B.dylib(libSystem_initializer) 
-> _os_object_init(libdiapatch.dylib) 
-> _objc_init(libobjc.A.dylib)
复制代码
load 调用线

在初始, 调用回调方法, 核心的回调方法是sNotifyObjCInit, 这个方法是在sNotifyObjCInit中初始化, 实际是在_objc_init(void)方法中注册初始化的!!!

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
	...
	if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
		(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
	}
  ...
}

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;
  ...
}
复制代码

也就是_objc_init()方法注册了一个全局的sNotifyObjCInit方法, 具体的方法名字是load_images, 该方法会在initializeMainExecutable中回调通知!!!

因此+load方法的调用链条:

_dyld_start -> dyldbootstrap::start -> dyld::_main -> dyld::initializeMainExecutable
-> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> dyld::notifySignle(回调) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
复制代码

4. 通俗理解以上的过程

如果在Xcode中配置环境变量DYLD_PRINT_STATISTICS = YES打印启动耗时:

Total pre-main time:  74.37 milliseconds (100.0%)
       dylib loading time:  41.05 milliseconds (55.2%)
      rebase/binding time:   8.10 milliseconds (10.9%)
          ObjC setup time:   9.87 milliseconds (13.2%)
         initializer time:  15.23 milliseconds (20.4%)
         slowest intializers :
           libSystem.B.dylib :   6.58 milliseconds (8.8%)
 libBacktraceRecording.dylib :   6.27 milliseconds (8.4%)
复制代码

dylib load

简单来说就是主二进制加载, 以及依赖的动态库加载, 这个过程中主要有以下详细过程:

  1. 系统内核读取主二进制可执行文件的Mach-O Header, 分析依赖动态库
  2. 找到动态库的Mach-O, 然后校验文件
  3. mmap内存映射到虚拟内存

rebase/rebind

由于iOS中使用 ASLR方式, 主二进制可执行文件与动态库映射到内存以后的地址是随机的. 因此需要修复Mach-O中程序与数据的地址.

rebase: 修复指向当前Mach-O内部的符号的地址

rebind: 指向当前Mach-O外部的符号的地址

这两部过程中由于, Text端被加密过, 因此系统内核读取时,需要加解密验证, 有IO耗时.

Objc Setup

也就是objc_init()主要工作在在runtime中注册oc相关内容,并检查:

  1. 注册 OC对象
  2. 注册OC的Category
  3. 检查Selector的唯一性等

这个过程在dyld3中的启动闭包对很多OC对象, Category的注册有大量Cache优化

initializer 初始化

前面内容都是在静态(fix-up), 修改_DATA segment中的内容, 这里开始动态调整, 开始往虚拟内存的堆和栈中写入内容:

  1. 执行objc中的+load函数
  2. 执行使用gcc的编译器指令: attribute((constructor)) xxx 函数
  3. 执行C++的static对象的创建

启动 Pipeline

这里贴一个抖音的全链路的总结:

juejin.cn/post/688774…

image.png

  1. 点击图标,创建进程
  2. mmap 主二进制,找到 dyld 的路径
  3. mmap dyld,把入口地址设为_dyld_start
  4. 重启手机/更新/下载 App 的第一次启动,会创建启动闭包
  5. 把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段
  6. 对每个二进制做 bind 和 rebase,主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据
  7. 初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
  8. +load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
  9. 初始化 UIApplication,启动 Main Runloop
  10. 执行 will/didFinishLaunch,这里主要是业务代码耗时
  11. Layout,viewDidLoadLayoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间
  12. Display,drawRect 会调用
  13. Prepare,图片解码发生在这一步
  14. Commit,首帧渲染数据打包发给 RenderServer,启动结束
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享