美文网首页
第十四节—dyld与libobjc

第十四节—dyld与libobjc

作者: L_Ares | 来源:发表于2020-10-31 02:34 被阅读0次

    本文为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 字段的访问

    相关文章

      网友评论

          本文标题:第十四节—dyld与libobjc

          本文链接:https://www.haomeiwen.com/subject/ztrdvktx.html