第十四节—dyld与libobjc

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

关于dyld怎么关联到了objc上面,就要先明白dyld是什么?objc又是什么?dyld加载流程中已经有过介绍。可以了解到dyld是一个链接器,主要的作用还是链接动态库。

那为什么好好的动态库,你非要链接它?

  • 第一是因为动态库所要负责的功能和思想都是有差别的,为了更好的模块化管理,所以不可能把代码都写在一个动态库里面。
  • 第二个原因就比较主观,这么多的功能,一个人完成是很难的,一个组完成也是很难的,所以这是协作开发,那么每个组做的东西都不一样,但是功能需要有衔接性。

所以就需要dyld这么一个动态链接器,把这些动态库都可以合成到项目中来,也即是加载到内存中来,供我们使用。

从这些条件也可以看出来,一个APP需要多个动态库就需要dyld,那么APP的加载也就需要先把动态库搞定,所以APP的加载流程也是和dyld息息相关的。

dyld加载流程,发现了

  • 在执行初始化主可执行程序initializeMainExecutable中,会执行recursiveInitialization,进行通知的注册遍历循环初始化镜像(image)的实例
  • 并且在dyld通知的注册(notifySingle)的时候利用一个函数指针sNotifyObjCInit来链接到load_images(libobjc.A.dylib)在这里关联上_objc_init流程。
  • 从而和libobjc动态库进行交互。然后进行doInitialization,递归实例化镜像(image),并且必须第一个初始化libSystem,然后按照libdispatch--->libobjc的顺序完成初始化。
  • 根据上述的探索,知道了这些镜像文件(images)是从dyld链接过来的。找到了_objc_initmap_images(镜像映射)load_images(镜像加载),把动态库的内容加载到内存中以表的形式存储。

这个流程构成了dyldlibobjc之间的通讯。

本节总结一下dyld到底是如何和libobjc完成联动的。

一、_objc_init

根据上述的思路,想知道images是怎么加载的,就要看libobjc在程序加载的过程中是怎么去做的。所以就要从最开始的_objc_init步骤开始。

还是要用objc4-781源码。搜索_objc_init

图1.0.png

_objc_init源码实现 :

/**
 引导程序的初始化,dyld会注册镜像通知
 在libobjc初始化的之前,_objc_init被libSystem唤醒
 */
void _objc_init(void)
{
    //一堆判断条件,判断是否初始化了libobjc
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    //环境变量的初始化。读取可以影响运行时的环境变量,如果有需要的话,可以看环境变量的帮助文件export OBJC_HRLP = 1
    environ_init();
    //关于线程键(key)的一些绑定,设置静态键的析构函数等操作
    tls_init();
    //C++静态构造函数的运行,因为dyld在下面_dyld_objc_notify_register才会把镜像加载进来,才有静态构造函数进入
    //但是_objc_init在这之前就被调用了,又需要C++的静态构造函数,所以自己先做了
    static_init();
    //runtime运行时环境的初始化。里面是unattachedCategories和allocatedClasses
    //就是没有附着的分类和用objc_allocateClassPair分配的所有类(和元类)的表的初始化
    runtime_init();
    //libobjc的异常处理系统的初始化,比如下面会有一个注册异常的回调处理函数,用这个回调函数实现监控异常
    exception_init();
    //缓存系统的初始化
    cache_init();
    //回调机制的初始化。一般不会做什么,因为一般的初始化都是懒加载的,但是有一些进程不是,它们就很需要靠回调
    _imp_implementationWithBlock_init();

    /**
     这个见过的,在`dyld`里面注册的一个回调函数(*sNotifyObjCInit这个函数指针要拿镜像)
     - 这个函数仅仅供objc运行时使用
     - 注册处理的程序。在映射(map_images)、取消映射(unmap_image)和初始化objc的镜像的时候调用
     - dyld通过里面的函数指针把和objc_image_info相关的镜像文件数组回调给map函数
     
     param:
     (1) map_images : 映射镜像。在dyld将镜像文件加载到内存的时候,会调用map_images
     (2) load_images: 加载镜像。在dyld初始化镜像文件的时候会调用
     (3) unmap_image: 取消镜像的映射。在dyld将镜像移除的时候会调用。
     */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

有很详细的注释了。

分别说一下前面这一堆的初始化,有一些是我们平常都用过的。

1. environ_init();

环境变量的初始化。读取可以影响运行时的环境变量。

两种方法可以获取OBJC的环境变量。

  • 终端(terminal)里面输入export OBJC_HELP=1可以看到相关的手册。如图 :
图1.1.1.png
  • 进入environ_init();,把红框里面的循环打印直接去掉判断条件,放到外面来,也可以打印出来,不过还是第一个方法正常一点。
图1.1.2.png

效果 :

图1.1.3.png

这些环境变量都是我们可以进行配置的,可以通过打开xcodeEdit Scheme -- Run --Arguments -- Environment Variables进行配置。

例子 :

比如拿一个nonpointerisa来做个例子。在刚才控制台输出的环境变量中搜索nonpointer,然后你会找到OBJC_DISABLE_NONPOINTER_ISA

nonpointer_isa是纯净的isa就是除了类的地址,其他的类信息,对象引用计数等等信息都是不加进去了。
因为isaunion,共用体,(不太清楚这个的可以看第二节isa),所以验证这个OBJC_DISABLE_NONPOINTER_ISA会被使用的条件就是 :

OBJC_DISABLE_NONPOINTER_ISA设置成YES,如果一个实例对象的isa地址和它的类的地址一样,那么就证明这个environ_init();的确是做了环境的动态加载。

  • 先在main.mmain()函数中初始化一个继承于NSObject的自定义子类JDPerson,并且初始化一个JDPerson的实例对象person

  • p/x JDPerson.class查看JDPerson的十六进制地址。

  • x.4gx person查看person对象的前4个十六进制地址段存储的内容,拿到第一个内存段中的isa地址。

图1.1.4.png
  • 修改OBJC_DISABLE_NONPOINTER_ISAYES,然后把xcode缓存清理一下,免得有问题。
图1.1.5.png

重新执行程序,继续查看JDPerson类的内存地址和person对象的isa地址。

图1.1.6.png

纯净的指针了吧。这样子是可以做到一定程度的优化内存的效果的。

其他的环境变量我会在下面的附录中贴出来,也会写几个常见的环境变量的设置和其作用。

2. tls_init();

关于线程键(key)的一些绑定,创建线程静态键(key),设置静态键的析构函数等操作。

void tls_init(void)
{
    //这里面的宏都是libc库为我们保留的一些线程的键(key)
    //这个宏是1
#if SUPPORT_DIRECT_THREAD_KEYS
    //设置静态键的析构函数,比如说pthread_key_create()这个函数创建的是静态键
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    //创建线程静态键(key)
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

3. static_init();

系统级C++静态构造函数的运行。因为dyld在下面的_dyld_objc_notify_register才会把镜像加载进来,才有静态构造函数进入,但是_objc_init在这之前就被调用了,又需要C++的静态构造函数,所以自己先初始化。

这里主要初始化的不是我们自己在代码写的那些C++或者OC的变量的初始化。而是系统级C++构造函数,因为系统级的C++会在我们自定义的函数之前就运行,所以有必要提前进行构造函数的运行,进行初始化。

static void static_init()
{
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
}

4. runtime_init();

runtime运行时环境的初始化。里面是unattachedCategoriesallocatedClasses。即没有附着的分类和用objc_allocateClassPair分配的所有类(和元类)的表的初始化。

void runtime_init(void)
{
    //没有attach的分类的初始化
    objc::unattachedCategories.init(32);
    //使用`objc_allocateClassPair`进行分配的所有的类和元类的表,初始化
    objc::allocatedClasses.init();
}

5. exception_init();

初始化libobjc的异常处理系统。注册异常处理的回调,从而监控对异常的处理。会被map_images唤醒。

void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}

我们进入那个_objc_terminate来看看这个传入的函数指针是什么。

图1.1.7.png

也就是说,异常最开始是置空的。并且这个函数官方有注释告诉了我们,没有捕获到的异常回调由C++的terminate handler完成。

如何捕获异常信息,是经过如下判断 :

  • 检查是否有活动异常。
  • 如果有活动异常,检查它是否是Objective-C异常。
  • 如果是Objective-C异常,就用该对象调用我们注册的回调。也就是第二个红框。
  • 最后,调用前面的terminate处理程序。

从第三步可以看出来,那个e调用的就是objc注册的回调,跟进去看是不找到什么有用的线索的,全局搜索查找到 :

图1.1.8.png

可以看到fn是外界传进来的一个处理异常的函数,这个函数应该由app层面传过来,然后把这个uncaught_handler指针指向fn,这样就会把内部捕获的异常由外部传来的fn处理掉。

5.1 Crash的分类 :

crash发生的主要原因是因为接收到了有异常未处理的信号。那么未处理的异常一般来自于三个方面 :

  • kernel内核

  • 其他进程

  • APP自己

所以Crash的分类也是三种 :

  • Mach异常 : 这是最底层的内核异常对应的Crash。用户态的开发者可以通过Mach API设置threadtaskhost的异常端口来捕获Mach异常

  • Unix信号异常 : 也称BSD信号,如果开发者没有捕获到Mach异常,那么host层的ux_exception()函数会将异常转换成相对应的Unix信号,然后将通过threadSignal()将这个异常的Unix信号投递到出错的线程。信号的捕获则由signal(x, SignalHandler)完成。

  • NSException 应用级异常 : 它是未被捕获的Objective-C异常,异常向自身发送SIGABRT信号导致了Crash。未捕获的Objective-C异常可以通过try catch进行捕获,也可以通过NSSetUncaughtExceptionHandler()进行捕获,即是利用NSSetUncaughtExceptionHandler实现线程保活,然后收集并上传崩溃信息的日志。

其中,第三种NSException是我们在开发中可以进行crash的拦截处理。通过在代码中加入NSSetUncaughtExceptionHandler,利用它给系统传一个函数,比如我们定义一个函数getCrash,然后NSSetUncaughtExceptionHandler(&getCrash)getCrash函数可以是线程保活并且上传崩溃信息日志的函数,这样就可以在app层进行处理。

6. cache_init();

缓存系统的初始化。

void cache_init()
{
    //arm64架构下一定会初始化
#if HAVE_TASK_RESTARTABLE_RANGES
    mach_msg_type_number_t count = 0;
    kern_return_t kr;

    while (objc_restartableRanges[count].location) {
        count++;
    }

    kr = task_restartable_ranges_register(mach_task_self(),
                                          objc_restartableRanges, count);
    if (kr == KERN_SUCCESS) return;
    _objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
                kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}

7. _imp_implementationWithBlock_init();

回调机制的初始化。一般不会做什么事情,因为一般的初始化都是懒加载的。但是有一些进程不一样,它们就很需要靠回调。

void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}

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

上一节见过了。

dyld里面注册的一个回调函数(*sNotifyObjCInit这个函数指针要拿镜像)。

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);
  • 这个函数仅仅供objc运行时使用。
  • 这是一个注册处理的程序。在映射(map_images)、取消映射(unmap_image)和初始化objc的镜像的时候调用
  • dyld通过里面的函数指针把和objc_image_info相关的镜像文件数组回调给map函数

参数是三个函数指针。

  • map_images : 映射镜像。在dyld将镜像文件加载到内存的时候,会调用map_images
  • load_images: 加载镜像。在dyld初始化镜像文件的时候会调用。
  • unmap_image: 取消镜像的映射。在dyld将镜像移除的时候会调用。比如程序发生异常了,或者说程序停止了。

二、dyld与objc的关联

在(一)中,我们最后的一句代码,想要被调用起来,就需要dyld那边有人调用,这样被传过去的map_images的指针指向的函数和load_images指针指向的函数才会被调用。这个上节就见过了,在notifySingle里面,在往上走就是dyld的主可执行文件的执行。也就是说,libobjc进入到动态链接器是从_dyld_objc_notify_register开始的。

那么从_dyld_objc_notify_register就又回到了dyld的加载流程

_dyld_objc_notify_registerlibobjc中的内容。

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

只有函数名,没有函数实现,但是在dyld中搜索

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

找到了实现。而且可以把registerObjCNotifiers就看作是_dyld_objc_notify_register,因为_dyld_objc_notify_register只有这一句代码。

于是又回到上一节的,只不过这次反推回去。

搜索registerObjCNotifiers

图2.1.png

看红框里面的三个是什么。

图2.2.png

和参数的类型完全一致。再看参数类型到底是什么。

图2.3.png

函数指针,只不过是类型重命名定义。也就是说 :

  • sNotifyObjCMapped有着mapped的函数实现,等于有着map_images的函数实现。
  • sNotifyObjCInit有着init的函数实现,等于有着load_images的函数实现。
  • sNotifyObjCUnmapped有着unmapped的函数实现,等于有着unmap_image的函数实现。

map_imagesload_imagesunmapped_dyld_objc_notify_register的三个参数,是libobjc传过来的。

sNotifyObjCMapped即map_images在dyld的调用

直接搜*sNotifyObjCMapped。就这一个,带着*才是使用了函数指针。不带*的那是把函数指针给换了。

知道了是在notifyBatchPartial中调用。

图2.4.png

再看谁调用了notifyBatchPartial,全局搜notifyBatchPartial。看过的就不要看了,不要一个流程里面转圈,一共就4个,找了一下,找到了notifyBatch

图2.5.png

再看谁调用了notifyBatch(

图2.6.png

看红框,回到了dyld加载的入口_main。而且发现是在initializeMainExecutable初始化主可执行程序之前就调用了,那么就说明*sNotifyObjCMapped指向的map_imagesinitializeMainExecutable执行。

上一节说过,initializeMainExecutable--->runInitializers--->processInitializers--->recursiveInitialization才找到了notifySingle,而notifySingle中才找到了*sNotifyObjCInit,也就是才找到了load_images

那么就得出一个结论 :

libobjc中的map_imagesload_images前执行。

于是我们可以得到一个dyldlibobjc的联动关系图 :

dyld和libobjc联动.png

附录 :环境变量

先说几个可能会是常用的。

  • DYLD_PRINT_STATISTICS : 如设置为YES。则控制台打印APP加载的时长,包含整体加载时长和动态库加载时长。即是main函数之前的启动时间(也就是pre-main耗时),知道这个可以尝试启动优化

  • OBJC_DISABLE_NONPOINTER_ISA : 如设置为YES。则nonpointer = 0,表示纯isaisa共用体只有类的内存地址,不包含类的一些信息、对象的引用计数等信息。

  • OBJC_PRINT_LOAD_METHODS : 如设置为YES。则打印ClassCategory+ (void)load的调用信息。就是都有哪些类或者分类调用了+ (void)load方法。

  • NSDoubleLocalizedStrings : 如设置为YES。则可以查看翻译之后的文字的UI是什么样子。

  • NSShowNonLocalizedStrings : 如设置为YES。则经过翻译后的项目,依然没有被翻译的字符串会变成大写。

下面全是环境变量。

环境变量名 说明
OBJC_PRINT_OPTIONS 输出OBJC已设置的选项
OBJC_PRINT_IMAGES 输出已load的image信息
OBJC_PRINT_LOAD_METHODS 打印 Class 及 Category 的 + (void)load 方法的调用信息
OBJC_PRINT_INITIALIZE_METHODS 打印 Class 的 + (void)initialize 的调用信息
OBJC_PRINT_RESOLVED_METHODS 打印通过 +resolveClassMethod: 或 +resolveInstanceMethod: 生成的类方法
OBJC_PRINT_CLASS_SETUP 打印 Class 及 Category 的设置过程
OBJC_PRINT_PROTOCOL_SETUP 打印 Protocol 的设置过程
OBJC_PRINT_IVAR_SETUP 打印 Ivar 的设置过程
OBJC_PRINT_VTABLE_SETUP 打印 vtable 的设置过程
OBJC_PRINT_VTABLE_IMAGES 打印 vtable 被覆盖的方法
OBJC_PRINT_CACHE_SETUP 打印方法缓存的设置过程
OBJC_PRINT_FUTURE_CLASSES 打印从 CFType 无缝转换到 NSObject 将要使用的类(如 CFArrayRef 到 NSArray * )
OBJC_PRINT_GC 打印一些垃圾回收操作
OBJC_PRINT_PREOPTIMIZATION 打印 dyld 共享缓存优化前的问候语
OBJC_PRINT_CXX_CTORS 打印类实例中的 C++ 对象的构造与析构调用
OBJC_PRINT_EXCEPTIONS 打印异常处理
OBJC_PRINT_EXCEPTION_THROW 打印所有异常抛出时的 Backtrace
OBJC_PRINT_ALT_HANDLERS 打印 alt 操作异常处理
OBJC_PRINT_REPLACED_METHODS 打印被 Category 替换的方法
OBJC_PRINT_DEPRECATION_WARNINGS 打印所有过时的方法调用
OBJC_PRINT_POOL_HIGHWATER 打印 autoreleasepool 高水位警告
OBJC_PRINT_CUSTOM_RR 打印含有未优化的自定义 retain/release 方法的类
OBJC_PRINT_CUSTOM_AWZ 打印含有未优化的自定义 allocWithZone 方法的类
OBJC_PRINT_RAW_ISA 打印需要访问原始 isa 指针的类
OBJC_DEBUG_UNLOAD 卸载有不良行为的 Bundle 时打印警告
OBJC_DEBUG_FRAGILE_SUPERCLASSES 当子类可能被对父类的修改破坏时打印警告
OBJC_DEBUG_FINALIZERS 警告实现了 -dealloc 却没有实现 -finalize 的类
OBJC_DEBUG_NIL_SYNC 警告 @synchronized(nil) 调用,这种情况不会加锁
OBJC_DEBUG_NONFRAGILE_IVARS 打印突发地重新布置 non-fragile ivars 的行为
OBJC_DEBUG_ALT_HANDLERS 记录更多的 alt 操作错误信息
OBJC_DEBUG_MISSING_POOLS 警告没有 pool 的情况下使用 autorelease,可能内存泄漏
OBJC_DEBUG_DUPLICATE_CLASSES 当出现类重名时停机
OBJC_USE_INTERNAL_ZONE 在一个专用的 malloc 区分配运行时数据
OBJC_DISABLE_GC 强行关闭自动垃圾回收,即使可执行文件需要垃圾回收
OBJC_DISABLE_VTABLES 关闭 vtable 分发
OBJC_DISABLE_PREOPTIMIZATION 关闭 dyld 共享缓存优化前的问候语
OBJC_DISABLE_TAGGED_POINTERS 关闭 NSNumber 等的 tagged pointer 优化
OBJC_DISABLE_NONPOINTER_ISA 关闭 non-pointer isa 字段的访问
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343