应用加载流程探索

0a000581fca74098bc53b876f5e957ae.jpeg 应用在启动的时候,系统做了什么?程序启动做了什么?

一. 程序启动

1、程序启动流程

我们用一个小demo运行一下:

@implementation ViewController
+(void)load {
    NSLog(@"%s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
@end
int main(int argc, char * argv[]) {

    NSString * appDelegateClassName;

    NSLog(@"66666");

    @autoreleasepool {

        // Setup code that might create autoreleased objects goes here.

        appDelegateClassName = NSStringFromClass([AppDelegate class]);

    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//打印
**2022-05-16 21:56:41.412521+0800 001--Test[1966:31438] +[ViewController load]**

**2022-05-16 21:56:41.424376+0800 001--Test[1966:31438] 66666**
复制代码

程序加载过程这些方法的调用顺序是:
load ——> C++ ——> main
main函数作为程序的入口,为什确实最后执行的,我们需要研究的是在main函数之前,程序到底做了什么? 我们在001--Test.app 包内容中看到了exec的可执行文件image.png 这个是mach-o格式的可执行文件。mach-o是什么?mach-o是一种文件格式。

程序编译过程

image.png 预编译: 预编译又称为预处理,主要做些代码文本的替换工作,处理#开头的指令。比如拷贝#include、#import包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做预备工作的阶段,主要处理#开始的预编译指令。生成.i文件

编译: 将高级语言转换为机器能识别的汇编语言。生成.s文件。(词法分析,语法分析,语义分析)。

汇编: 将汇编文件转换成机器码文件。生成.o文件

链接: 对.o文件中引用的其他的库进行引入,生成可执行文件。

动态库 和 静态库

image.png 静态库:在链接阶段,将可汇编生成目标程序与它所引用的库一起链接打包到可执行文件中。此时静态库就不会改变了,静态库是直接拷贝,复制到目标程序。

  • 优点:编译完成,库文件就没有作用了,运行时可以直接使用
  • 缺点:不同的静态库中引用了相同的文件,如上图中的B、D,他就会拷贝两份相同的库文件,导致目标程序体积增大,对性能、内存会有一定的影响。

动态库:编译时相同的库并不会拷贝到目标程序,而是在目标程序载入的时候,把那些相同的库用一份共享实例加载进来。(是一个已经链接完全的镜像image包含有可执行文件,dylib,bundle)。

  • 优点

    • 减少打包的体积:共享内存,节约资源
    • 更新动态库可以直接更新程序:由于运行时才载入的特性,因此可以随时对下层库进行更换,而我们的代码却不用改变。
  • 缺点:动态载入会带来一部分性能损失,如果当前环境缺少动态库,或者版本不正确,会导致程序无法运行。

    扫描二维码关注公众号,回复: 14157402 查看本文章

总结:在启动App时,真正的加载过程是从exec()函数开始,系统会调⽤exec()函数创建进程,并且分配内存。然后会执⾏以下的操作

  1. 把App对应的可执⾏⽂件加载到内存。
  2. 把dyld加载到内存。dyld也是⼀个可执⾏的程序
  3. dyld进⾏动态链接。

2、 dyld加载流程

dyld是苹果的动态连接器,加载到程序中的动态库,主要靠dyld来链接管理。 借助一下上面的工程,在load方法处加上断点,打印堆栈信息: image.png

  • bt

image.png 我们可以看到堆栈的第一个信息是_dyld_start,也可以直接在左边窗口查看。 现在真机环境用的是dyld3模拟器环境是dyld4目前我们还是以dyld3来阅读源码和探索。

  • 打开dyld源码,搜索_dyld_start找到__arm64__环境下的源码

image.png 往下查找发现它调起了dyldbootstrap::start方法 image.png 搜索dyldbootstrap找到start方法 image.png 其核心就是dyld::_main,返回调用了dyldmain函数。从左边的堆栈,也可以清晰的看到。其中macho_header就是Mach-o的头部。 image.png 点击跳转到dyld::_main函数 。_main函数主要做了以下事情:

1.环境变量配置(环境,平台,版本,路径,主机信息)
2.共享缓存(mapSharedCache):(UIKit、CoreFoundation等)
3.主程序的初始化(instantiateFromLoadedImage)
4.加入动态库(loadInsertedDylib)
5.link主程序
6.link动态库
7.weakBind弱引用绑定主程序
8.initializeMainExecutable初始化
9.notifyMonitoringDyldMain通知dyld可以进行main()。

附dyld流程分析图: dyld流程分析图.png 研究的重点和方向是这个函数initializeMainExecutable会运行所有的初始化操作:

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]);

}
复制代码

然后我们进入这个sMainExecutable->runInitializers函数

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);//核心代码1

context.notifyBatch(dyld_image_state_initialized, false);//核心代码2

mach_port_deallocate(mach_task_self(), thisThread);

uint64_t t2 = mach_absolute_time();

fgTotalInitTime += (t2 - t1);

}
复制代码

image.png processInitializers函数递归查找使用的动态库,并执行recursiveInitialization初始化。 ImageLoader::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);
.....
//runtime objc 依赖于runtime

// let objc know we are about to initialize this image

uint64_t t1 = mach_absolute_time();

fState = dyld_image_state_dependents_initialized;

oldState = fState;
//发送通知给objc-runtime进行准备
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

.....
recursiveSpinUnLock();
}
复制代码

notifySingle方法中 -> dyld_image_state_dependents_initialized 的判断里 image.png static _dyld_objc_notify_init sNotifyObjCInit;->_dyld_objc_notify_init image.png sNotifyObjCInit = init;在registerObjCNotifiers中被赋值。 image.png _dyld_objc_notify_register - >registerObjCNotifiers _dyld_objc_notify_register中调用了registerObjCNotifiers。 这个registerObjCNotifiers通知调用是在objc源码中调用的。 我们添加dyld_objc_notify_register符号断点,来看看dyld_objc_notify_register在objc源码中具体在何时何处调用。 image.png 我们通过符号断点得知,dyld_objc_notify_register在objc源码中的_objc_init中调用。 image.png 在_objc_init源码中进行断点->bt打印堆栈信息得知: image.png 在_objc_init之前调用了libdispatch.dylib->_os_object_init方法。
哪我们就打开libdispatch源码在进行探索。 image.png 在_os_object_init方法之前还调用了libdispatch_init方法。 在libdispatch_init方法之前有调用了libSystem.B.dylib->libSystem_initializer方法。

打开 libSystem 源码看到libSystem_initializer 里调用了libdispatch_init

static void
libSystem_initializer(int argc,

      const char* argv[],

      const char* envp[],

      const char* apple[],

      const struct ProgramVars* vars)

{

static const struct _libkernel_functions libkernel_funcs = {

.version = 4,

// V1 functions

#if !TARGET_OS_DRIVERKIT

.dlsym = dlsym,

#endif
......
#if TARGET_OS_OSX

__pthread_late_init(envp, apple, vars);

#endif

libdispatch_init();  //找到libdispatch_init 函数

_libSystem_ktrace_init_func(LIBDISPATCH);
......
errno = 0;

}
复制代码

发现在其中调用了__malloc_init、 _dyld_initializer、 libdispatch_init

我们回到dyld源码: image.png image.png image.png 所以libSystem.dylib在第一个初始化。动态库的依赖关系libSystem->libdispatch->objc_init。 在objc_init中_dyld_objc_notify_register(&map_images, load_images, unmap_image); 调用了map_images,load_images,unmap_image 3个方法。

总结dyld是⽤来加载可执⾏⽂件所依赖的动态库的。然后会对可执⾏⽂件和可执⾏⽂件所依赖的动态库进⾏初始化的操作。

在进⾏初始化的操作的时候⾸先会初始化libsystem,否则就会报错。因为在进⾏libsystem初始化 的时候,会初始化libdispatch,在进⾏libdispatch初始化的时候,会初始化libobjc,其他的库,可能需要依赖runtime基础或者线程相关的基础。所以libsystem的初始化必须放在第⼀位

在libobjc进⾏初始化的时候,会调⽤⼀个_dyld_objc_notify_register函数,这个函数会给dyld传递 三个回调函数。

  1. map_images: dyld将image镜像⽂件加载进内存时,会触发该函数

  2. load_images: dyld初始化image会触发该函数

  3. unmap_image: dyld将image移除时会触发该函数

然后,dyld会调⽤ map_images 和 load_images 来对image进⾏初始化的操作。

猜你喜欢

转载自juejin.im/post/7099087610496155685