iOS底层收集

我所理解的iOS runtime 和 dyld

2020-05-20  本文已影响0人  是的蛮大人

前言

关于iOS的runtime和dyld,网上资料一搜一大把,不过很多都是复制来复制去的。本来是不打算再整理一下关于这方面的资料了,但是想了想,别人的始终还是别人的,不自己整理一下写点什么,总感觉没法很好的消化吸收,让它们能保存到自己的脑子里。所以这里就简单记录一下自己在看了一些网上的资料和objc的源码后的一点点理解吧。不可否认的是,还是得亲自跟了源码后,才能对一些结论认识的更加透彻。如果有表述错误的地方,欢迎指正。

dyld 动态链接库 the dynamic link editor

dyld是一个操作系统级别的组件,负责iOS系统中每个App启动时的运行时环境初始化以及加载动态库到内存等一些列操作。

dyld主要干了哪些事情?

关于dyld的详细内容,可以参考:dyld详解
我这里只简单搬了一点点过来,因为我觉得,大概知道点这些内容应该够用了。

第一步:设置运行环境。
第二步:加载共享缓存。(dyld会把一些基础的共用lib缓存起来,这样就不需要每个app启动时都加载一次了)
第三步:实例化主程序。
第四步:加载插入的动态库。
第五步:链接主程序。
第六步:链接插入的动态库。
第七步:执行弱符号绑定
第八步:执行初始化方法。
第九步:查找入口点并返回。

Mach-O 文件

Mach-object文件格式的缩写,用于可执行文件、动态链接库、目标代码的文件格式。
每个iOS App或者动态库中都会有一个Mach-O格式的文件。
Mach-O 文件可以被dyld加载到内存中。
参考:Objective-C runtime机制(前传)——Mach-O格式

Mach-O文件结构

也是简单搬一点点过来。

_objc_init

_objc_init是runtime的入口函数,它是在libSystem中被调用,libSystem是若干个系统lib的集合,也是由dyld动态加载的。
关于这个调用顺序,通过一个符号断点就可以看出来。至于怎么加这个断点,网上很多,这里就不再重复了。

_objc_init 方法的源码如下:
参考objc4源码: https://opensource.apple.com/tarballs/objc4/

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

其中通过_dyld_objc_notify_register注册了3个回调方法。

这个方法在dyld中定义。参考dyld的源码:https://opensource.apple.com/tarballs/dyld/

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

dyld在加载每个image(可以理解为动态库或可执行文件)的过程中,都会在对应的时候调用objc注册的这几个回调函数,来让objc完成对runtime环境的构建。只有将runtime环境都准备好了,一个iOS app才有运行的环境。

我所关注的重点就在这几个回调上。

_dyld_objc_notify_mapped回调

对应到objc中的是map_images方法。
map_images会调用到_read_images方法,这个方法里的内容比较多,主要是读取Mach-O文件对应的objc的section,并根据内容初始化runtime的内存结构。
通过源码中的注释可以看出,其大概做了以下这些事:

我们可以在Xcode的scheme中添加一个环境变量OBJC_PRINT_IMAGE_TIMES:YES就可以在控制台中看到上面这些步骤每一步所花费的时间。

在最后一步Discover categories中,上面已经给了结论:category中的method 会被添加到class的method list的前面。这个结论是从何得来的呢?
看下面这段源码:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        ...
    }

这里的重点是memmove和memcpy函数。

// 由src所指内存区域复制count个字节到dest所指内存区域
void    *memmove(void *__dst, const void *__src, size_t __len);
// 拷贝src所指的内存内容前n个字节到dst所指的内存地址上
void    *memcpy(void *__dst, const void *__src, size_t __n);

由此可见,objc先将class原有的method list向后移了addedCount位,然后将addedList添加到了前面空出的位置上。addedList就是在categories中定义的方法。
所以,结论就是:category中定义的方法会被添加method list的前面,从而造成一种category中的方法会“覆盖”class原有的方法的假象。“覆盖”是假的,但是目的却真实的达到了

_dyld_objc_notify_init回调

对应到objc中的是load_images方法。

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

可以看到,+ load方法正是在这里被调用的。

prepare_load_methods做了2件事情:

/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    // 如果已经处理过,则不再处理,保证每个在class中定义的+load方法只被添加一次
    if (cls->data()->flags & RW_LOADED) return; 
    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); //标识已经处理过
}

从这段代码可以看到,在检测每个class是否存在+load方法时,会优先检测class的superclass是否存在+load方法,如果存在,会先将super class的+load 方法添加到list里,这样就保证了super class的+load方法会先与class中的被调用。

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    ...
    // 这里是对loadable_classes进行扩容
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    // 保存class和+load方法的IMP到list里
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;
    ...
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

对比这2段代码可以看到,保存class的+load list和category的+load list是不一样的,并且这2个list都是以数组的形式存储,所以当一个class的多个category中都实现了+load方法时,这些+load方法都会被依次调用,调用顺序同category的编译顺序是一样的。

call_load_methods: 依次调用loadable_classes和loadable_categories中保存的class的+load方法。从这里可以看到,class本身的+load方法会先于category的+load方法调用。等所有classes的+load方法都调用完了,才会去调用category中定义的+load方法。

_dyld_objc_notify_unmapped 回调

对应到objc中是unmap_image方法。主要是做一些移除操作。

关于+load方法的几点总结

编译顺序指的是Compile sources中出现的顺序。

这里再补充几点关于+initialize的总结吧,人们往往很喜欢把这2个进行对比。

关于 +initialize 的几点总结

总结

在_objc_init中,runtime通过_dyld_objc_notify_register方法注册了3个回调,当某个image(二进制文件)被dyld加载到内存中的过程中,就会执行对应的回调方法,因此这3个回调可能会被调用多次。
等到dyld完成了所有的runtime环境准备后,就会调用main函数,让app启动起来。

参考

https://www.dllhook.com/post/238.html
https://blog.csdn.net/u013378438/article/details/80353267
https://blog.csdn.net/u013378438/article/details/86614815

上一篇下一篇

猜你喜欢

热点阅读