1、往期文章

iOS 底层探索文章系列

我们在上一篇文章中已经分析过 dyld 的加载流程,那么 dyld 与我们的 objc 之间是怎样关联起来的呢,这是本文我们分析的重点。

关于这一块的分析,我们先抛出两个经典的面试题,这在我们之后的几篇文章中会慢慢进行分析探讨。

1. APP启动,在 main 函数之前都具体做了哪些内容? 2. load 在什么时候进行调用?子类、父类以及分类 load 的调用顺序?

2、APP 编译流程及 dyld 启动流程

image.png

注意: 只有 静态库会在编译阶段打包到可执行文件中动态库是在程序运行时才会被加入到可执行文件。如果对于它们之间的区别不怎么了解的同学,可以参考 iOS静态库与动态库的区别与打包

根据上面的编译流程图,我们可以得知,在应用程序启动前,会先对代码进行编译,在编译阶段会把静态库打包到可执行文件中,编译完成之后,就会进入启动阶段,涉及到 App 的启动流程,那么就肯定少不了我们的动态链接器 dyld,整个启动过程都由它来进行协调。

image.png

所以,我们可以把 App 的启动过程分为两个阶段,一个是 main 函数之前,另一个是 main 函数之后,我们先看一下 main 函数之前主要做了哪些操作

1. 调用 dyld 对主程序运行环境初始化。 2. 生成 imageLoader 将动态库生成对应的 image 镜像文件,然后载入到内存中进行链接绑定。 3. 初始化所有动态库,在执行所有插入的动态库初始化的时候,同时对 load_images 进行了绑定。 4. 在执行初始化的过程中,会优先初始化系统库 libSyatem,运行 Runtime,这个过程会进入到 Runtime的入口函数 _objc_init 中。 5. 把之前链接的动态库以及符号都交给 Runtime 进行 map_imagesload_images 操作,然后 Runtime 执行完 load_images 之后会回调到 dyld 内部。 6. 当 dyld 收到信息回调后,最后会查找 main 函数的入口 LC_MAIN,然后就会调用 main 函数。

3、_objc_init 源码分析

本文采用的是 objc4-781) 版本。

先从系统库 libSystemRuntime 入口函数 _objc_init 开始分析:

  1. /***********************************************************************
  2. * _objc_init
  3. * Bootstrap initialization. Registers our image notifier with dyld.
  4. * Called by libSystem BEFORE library initialization time
  5. **********************************************************************/
  6. void _objc_init(void)
  7. {
  8. static bool initialized = false;
  9. if (initialized) return;
  10. initialized = true;
  11. // fixme defer initialization until an objc-using image is found?
  12. // 读取影响运行时的环境变量。如果需要,还可以打印环境变量帮助(如添加 OBJC_PRINT_LOAD_METHODS、OBJC_DISABLE_NONPOINTER_ISA 等等)
  13. environ_init();
  14. // 关于线程 key 的绑定(比如每线程数据的析构函数)
  15. tls_init();
  16. // 运行 C++ 静态构造函数。在 dyld 调用我们的静态构造函数之前,libc 会调用 _objc_init(),因此我们必须自己做
  17. static_init();
  18. // runtime 运行时环境初始化,里面主要是unattachedCategories、allocatedClasses(分类初始化)
  19. runtime_init();
  20. // 初始化 libobjc 的异常处理系统
  21. exception_init();
  22. // 缓存条件初始化
  23. cache_init();
  24. // 启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载 trampolines.dylib。
  25. _imp_implementationWithBlock_init();
  26. // 注册回调函数
  27. // map_images: 当 dyld 将 image 镜像文件加载进内存时,会触发该函数。
  28. // load_images: 当 dyld 初始化 image 时,会触发该函数。
  29. // unmap_image: 当 dyld 移除掉 image 时,会触发该函数。
  30. _dyld_objc_notify_register(&map_images, load_images, unmap_image);
  31. #if __OBJC2__
  32. didCallDyldNotifyRegister = true;
  33. #endif
  34. }

3.1 environ_init: 环境变量初始化

下面贴出源码实现的关键代码部分

  1. /***********************************************************************
  2. * environ_init
  3. * Read environment variables that affect the runtime.
  4. * Also print environment variable help, if requested.
  5. **********************************************************************/
  6. void environ_init(void) {
  7. // Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
  8. if (PrintHelp || PrintOptions) {
  9. for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
  10. const option_t *opt = &Settings[i];
  11. if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
  12. if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
  13. }
  14. }
  15. }

我们可以通过以下两种方式打印所以的环节变量

  • 将某一个判断条件强行设置为 YES

image.png

  • 通过终端命令 export OBJC_HELP=1 输出环境变量

image.png

以上这些环境变量,可以在工程里面通过 target -> Edit Scheme -> Run -> Arguments -> Environment Variables 中配置。

image.png

3.2 tls_init: 线程 key 的绑定

  1. void tls_init(void)
  2. {
  3. #if SUPPORT_DIRECT_THREAD_KEYS
  4. // 本地线程池
  5. pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
  6. #else
  7. // 析构函数
  8. _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
  9. #endif
  10. }

3.3 static_init: 运行系统级别的 C++ 静态构造函数

主要是运行系统级别的 C++ 静态构造函数,保证 系统级别的 C++ 静态构造函数比我们自定义的 C++ 函数先行调用

  1. /***********************************************************************
  2. * static_init
  3. * Run C++ static constructor functions.
  4. * libc calls _objc_init() before dyld would call our static constructors,
  5. * so we have to do it ourselves.
  6. **********************************************************************/
  7. static void static_init()
  8. {
  9. size_t count;
  10. auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
  11. for (size_t i = 0; i < count; i++) {
  12. inits[i]();
  13. }
  14. }

3.4 runtime_init: 运行时环境初始化

主要是运行时的初始化,分为两个部分,分类的初始化类的表的初始化。这一部分我们后面的文章再进行详细分析。

  1. void runtime_init(void)
  2. {
  3. objc::unattachedCategories.init(32);
  4. objc::allocatedClasses.init();
  5. }

3.5 exception_init: 初始化 libobjc 的异常处理系统

  • 主要是初始化 libobjc 的异常处理系统,注册异常处理的函数回调,实现对异常的监控处理。
  1. /***********************************************************************
  2. * exception_init
  3. * Initialize libobjc's exception handling system.
  4. * Called by map_images().
  5. **********************************************************************/
  6. void exception_init(void)
  7. {
  8. old_terminate = std::set_terminate(&_objc_terminate);
  9. }
  • _objc_terminate 函数内部会对 OC 的异常,调用 foundationuncaught_handler(如果设置了的话)
  1. /***********************************************************************
  2. * _objc_terminate
  3. * Custom std::terminate handler.
  4. *
  5. * The uncaught exception callback is implemented as a std::terminate handler.
  6. * 1. Check if there's an active exception
  7. * 2. If so, check if it's an Objective-C exception
  8. * 3. If so, call our registered callback with the object.
  9. * 4. Finally, call the previous terminate handler.
  10. **********************************************************************/
  11. static void (*old_terminate)(void) = nil;
  12. static void _objc_terminate(void)
  13. {
  14. if (PrintExceptions) {
  15. _objc_inform("EXCEPTIONS: terminating");
  16. }
  17. if (! __cxa_current_exception_type()) {
  18. // No current exception.
  19. (*old_terminate)();
  20. }
  21. else {
  22. // There is a current exception. Check if it's an objc exception.
  23. @try {
  24. __cxa_rethrow();
  25. } @catch (id e) {
  26. // It's an objc object. Call Foundation's handler, if any.
  27. (*uncaught_handler)((id)e);
  28. (*old_terminate)();
  29. } @catch (...) {
  30. // It's not an objc object. Continue to C++ terminate.
  31. (*old_terminate)();
  32. }
  33. }
  34. }
  • objc-exception 中提供了读写 uncaught_handler 的方法
  1. /***********************************************************************
  2. * _objc_default_uncaught_exception_handler
  3. * Default uncaught exception handler. Expected to be overridden by Foundation.
  4. **********************************************************************/
  5. static void _objc_default_uncaught_exception_handler(id exception)
  6. {
  7. }
  8. static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
  9. /***********************************************************************
  10. * objc_setUncaughtExceptionHandler
  11. * Set a handler for uncaught Objective-C exceptions.
  12. * Returns the previous handler.
  13. **********************************************************************/
  14. objc_uncaught_exception_handler
  15. objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
  16. {
  17. objc_uncaught_exception_handler result = uncaught_handler;
  18. uncaught_handler = fn;
  19. return result;
  20. }

可以看到 uncaught_handler 就是一个函数指针。

3.6 cache_init: 缓存初始化

主要是缓存初始化

  1. void cache_init()
  2. {
  3. #if HAVE_TASK_RESTARTABLE_RANGES
  4. mach_msg_type_number_t count = 0;
  5. kern_return_t kr;
  6. while (objc_restartableRanges[count].location) {
  7. count++;
  8. }
  9. kr = task_restartable_ranges_register(mach_task_self(),
  10. objc_restartableRanges, count);
  11. if (kr == KERN_SUCCESS) return;
  12. _objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
  13. kr, mach_error_string(kr));
  14. #endif // HAVE_TASK_RESTARTABLE_RANGES
  15. }

3.7 _imp_implementationWithBlock_init: 启动回调机制

主要是启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载 trampolines.dylib。

  1. /// Initialize the trampoline machinery. Normally this does nothing, as
  2. /// everything is initialized lazily, but for certain processes we eagerly load
  3. /// the trampolines dylib.
  4. void
  5. _imp_implementationWithBlock_init(void)
  6. {
  7. #if TARGET_OS_OSX
  8. // Eagerly load libobjc-trampolines.dylib in certain processes. Some
  9. // programs (most notably QtWebEngineProcess used by older versions of
  10. // embedded Chromium) enable a highly restrictive sandbox profile which
  11. // blocks access to that dylib. If anything calls
  12. // imp_implementationWithBlock (as AppKit has started doing) then we'll
  13. // crash trying to load it. Loading it here sets it up before the sandbox
  14. // profile is enabled and blocks it.
  15. //
  16. // This fixes EA Origin (rdar://problem/50813789)
  17. // and Steam (rdar://problem/55286131)
  18. if (__progname &&
  19. (strcmp(__progname, "QtWebEngineProcess") == 0 ||
  20. strcmp(__progname, "Steam Helper") == 0)) {
  21. Trampolines.Initialize();
  22. }
  23. #endif
  24. }

3.8 _dyld_objc_notify_register: dyld 注册回调函数

我们在上一篇文章中已经详细分析过 dyld 的加载流程,我们看一下 _dyld_objc_notify_register 的源码声明部分

  1. //
  2. // Note: only for use by objc runtime
  3. // Register handlers to be called when objc images are mapped, unmapped, and initialized.
  4. // Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
  5. // Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
  6. // call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
  7. // dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
  8. // dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
  9. // initializers in that image. This is when objc calls any +load methods in that image.
  10. //
  11. void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
  12. _dyld_objc_notify_init init,
  13. _dyld_objc_notify_unmapped unmapped);

从注释里面,我们就可以看出:

  • 这个函数的调用是在 objc runtime 的时候
  • 注册在映射,取消映射和初始化 objc 镜像时要调用的处理程序。
  • dyld 将使用包含 objc-image-info 的镜像文件的数组回调 mapped 函数。

3.9 dyld 与 objc 的关联

我们通过源码来体现它们之间的关联关系

  1. void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
  2. _dyld_objc_notify_init init,
  3. _dyld_objc_notify_unmapped unmapped)
  4. {
  5. dyld::registerObjCNotifiers(mapped, init, unmapped);
  6. }

上面这处代码的调用之处是在 libobjc_objc_init 函数中

  1. _dyld_objc_notify_register(&map_images, load_images, unmap_image);

所以我们能看到它们之间的一一对应关系

  1. static _dyld_objc_notify_mapped sNotifyObjCMapped;
  2. static _dyld_objc_notify_init sNotifyObjCInit;
  3. static _dyld_objc_notify_unmapped sNotifyObjCUnmapped;

根据定义,结合我们上一节的分析,我们可以知道 load_images 也就是 sNotifyObjCInit 函数指针的调用时在 notifySingle 方法里面的

image.png

然后我们查找赋值 sNotifyObjCInit 的地方,可以找到是在 registerObjCNotifiers 函数中

  1. void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
  2. {
  3. // record functions to call
  4. sNotifyObjCMapped = mapped;
  5. sNotifyObjCInit = init;
  6. sNotifyObjCUnmapped = unmapped;
  7. // 省略代码......
  8. }

继续查找 registerObjCNotifiers 调用的地方,最终找到了 _dyld_objc_notify_register

  1. void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
  2. _dyld_objc_notify_init init,
  3. _dyld_objc_notify_unmapped unmapped)
  4. {
  5. dyld::registerObjCNotifiers(mapped, init, unmapped);
  6. }

所以这里就可以得出 dyld 与 objc 直接的关联关系了,也就是

  • sNotifyObjCMapped == mapped == map_images;
  • sNotifyObjCInit == init == load_images;
  • sNotifyObjCUnmapped == unmapped == unmap_image;

3.10 map_images 的调用时机

接下来我们看看 map_images 的调用时机,也是一样的套路,我们搜索 sNotifyObjCMapped 函数指针是在哪里进行调用的

image.png

然后我们继续搜索 notifyBatchPartial 调用的地方,看到是在 registerObjCNotifiers 中进行调用的。

image.png

所以我们在这里可以得出

  • map_images 是在 registerObjCNotifiers 中进行调用
  • map_imagesload_images 先前调用。