iOS 底层探索 - 类的加载

iOS 底层探索系列数组

iOS 查漏补缺系列缓存

1、应用加载回顾

上一章咱们对应用的加载有了初步的认识,咱们知道了markdown

  • 系统调用 exec() 会咱们的应用映射到新的地址空间
  • 而后经过 dyld 进行加载、连接、初始化主程序和主程序所依赖的各类动态库
  • 最后在 initializeMainExecutable 方法中通过一系列初始化调用 notifySingle 函数,该函数会执行一个 load_images 的回调
  • 而后在 doModinitFuntions 函数内部会调用 __attribute__((constructor))c 函数
  • 而后 dyld 返回主程序的入口函数,开始进入主程序的 main 函数

main 函数执行执行,其实 dyld 还会在流程中初始化 libSystem,而 libSystem 又会去初始化 libDispatch,在 libDispatch 初始化方法里面又会有一步 _os_object_init,在 _os_object_init 内部就会调起 _objc_init。而对于 _objc_init 咱们还须要继续探索,由于这里面会进行类的加载等一系列重要的工做。数据结构

image.png

2、探索 _objc_init

首先来到 libObjc 源码的 _objc_init 方法处,你能够直接添加一个符号断点 _objc_init 或者全局搜索关键字来到这里:app

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);
}
复制代码

咱们接着进行分析:函数

image.png

  • 判断是否已经初始化了,若是初始化过了,直接返回。

2.1 environ_init

接着来到 environ_init 方法内部:工具

image.png

咱们能够看到,这里主要是读取影响 Runtime 的一些环境变量,若是须要,还能够打印环境变量帮助提示。oop

咱们能够在终端上测试一下,直接输入 export OBJC_HELP=1:post

image.png

能够看到不一样的环境变量对应的内容都被打印出来了。学习

2.2 tls_init

接着来到 tls_init 方法内部:

void tls_init(void) {
#if SUPPORT_DIRECT_THREAD_KEYS
    _objc_pthread_key = TLS_DIRECT_KEY;
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
复制代码

这里执行的是关于线程 key 的绑定,好比每线程数据的析构函数。

2.3 static_init

接着来到 static_init 方法内部:

/*********************************************************************** * static_init * Run C++ static constructor functions. * libc calls _objc_init() before dyld would call our static constructors, * so we have to do it ourselves. **********************************************************************/
static void static_init() {
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
}
复制代码

这里会运行 C++ 的静态构造函数,在 dyld 调用咱们的静态构造函数以前,libc 会调用 _objc_init,因此这里咱们必须本身来作,而且这里只会初始化系统内置的 C++ 静态构造函数,咱们本身代码里面写的并不会在这里初始化。

2.4 lock_init

接着来到 lock_init 方法内部:

void lock_init(void) {
}
复制代码

咱们能够看到,这是一个空的实现。也就是说 objc 的锁是彻底采用的 C++ 那一套的锁逻辑。

2.5 exception_init

接着来到 exception_init 方法内部:

/*********************************************************************** * exception_init * Initialize libobjc's exception handling system. * Called by map_images(). **********************************************************************/
void exception_init(void) {
    old_terminate = std::set_terminate(&_objc_terminate);
}
复制代码

这里是初始化 libobjc 的异常处理系统,咱们程序触发的异常都会来到:

image.png

咱们能够看到 _objc_terminate 是未处理异常的回调函数,其内部逻辑以下:

  • 检查是不是一个活跃的异常
  • 若是是活跃的异常,检查是不是 OC 抛出的异常
  • 若是是 OC 抛出的异常,调用 uncaught_handeler 回调函数指针
  • 若是不是 OC 抛出的异常,则继续 C++ 终止操做

2.6 _dyld_objc_notify_register

接下来使咱们今天探索的重点了: _dyld_objc_notify_register ,咱们先看下它的定义:

image.png

注意:仅供 objc 运行时使用 当 objc 镜像被映射(mapped)、**卸载(unmapped)初始化(initialized)**的时候,注册的回调函数就会被调用。 这个方法是 dlyd 中声明的,一旦调用该方法,调用结果会做为该函数的参数回传回来。好比,当全部的 images 以及 section 为 objc-image-info 被加载以后会回调 mapped 方法。 load 方法也将在这个方法中被调用。

_dyld_objc_notify_register  方法的三个参数 map_images 、 load_images 、 unmap_image  其实都是函数指针:

image.png

这三个函数指针是在 dyld 中回调的,咱们打开 dyld 的源码便可一探究竟,咱们直接搜索 _dyld_objc_notify_register :

image.png

接着来到 dyld 的 registerObjCNotifiers 方法内部:

image.png

image.png

经过上面两张截图的内容说明在 registerObjCNotifiers 内部, libObjc 传过来的这三个函数指针被 dyld 保存在了本地静态变量中。换句话来讲,最终函数指针是否能被调用,取决于这三个静态变量:

  • sNotifyObjCMapped
  • sNotifyObjCInit
  • sNotifyObjCUnmapped

咱们注意到 registerObjCNotifiers 的 try-catch 语句中的 try 分支注释以下:

call 'mapped' function with all images mapped so far 调用 mapped 函数来映射全部的镜像

那么也就是说 notifyBatchPartial 里面会进行真正的函数指针的调用,咱们进入这个方法内部:

image.png

咱们能够看到,在 notifyBatchPartial 方法内部,这里的注释:

tell objc about new images 告诉 objc 镜像已经映射完成了

而图中箭头所指的地方正是 sNotifyObjCMapped 函数指针真正调用的地方。

弄清楚了三个函数指针是怎么调用的还不够,接下来咱们要深刻各个函数的内部看里面究竟作了什么样的事情。

3、探索 map_images

首先是 map_images ,咱们来到它的实现:

/*********************************************************************** * map_images * Process the given images which are being mapped in by dyld. * Calls ABI-agnostic code after taking ABI-specific locks. * * Locking: write-locks runtimeLock **********************************************************************/
void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) {
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}C
复制代码

Process the given images which are being mapped in by dyld. Calls ABI-agnostic code after taking ABI-specific locks.

处理由 dyld 映射的给定镜像 取得特定于 ABI 的锁后,调用与 ABI 无关的代码。

这里会继续往下走到 map_images_nolock

map_images_nolock 内部代码十分冗长,咱们通过分析以后,前面的工做基本上都是进行镜像文件信息的提取与统计,因此能够定位到最后的 _read_images :

image.png

这里进入 _read_images 的条件是 hCount 大于 0, hCount 表示的是 Mach-O 中 header 的数量

OK,咱们的主角登场了, _read_images 和 lookupImpOrForward 能够说是咱们学习 Runtime 和 iOS 底层里面很是重要的两个概念了, lookUpImpOrForward 已经探索过了,剩下的 _read_images 咱们也不能落下。

image.png

3.1 _read_images 定义

Perform initial processing of the headers in the linked list beginning with headerList.  从 headerList 开始,对已经连接了的 Mach-O 镜像表中的头部进行初始化处理

咱们能够看到,整个 _read_images 有接近 400 行代码。咱们不妨折叠一下里面的分支代码,而后总览一下:

image.png

image.png

image.png

经过折叠代码,以及日志打印提示信息,咱们大体能够将 _read_images 分为下面几个流程:

3.2 _read_images 具体流程


doneOnce 流程
**
咱们从第一个分支 doneOnce 开始,这个名词顾名思义,只会执行一次:

image.png

  • 经过宏 SUPPORT_NONPOINTER_ISA 判断当前是否支持开启内存优化的 isa
    • 若是支持,则在某些条件下须要禁用这个优化
  • 经过宏 SUPPORT_INDEXED_ISA 判断当前是不是将类存储在 isa 做为类表索引
    • 若是是的话,再递归遍历全部的 Mach-O 的头部,而且判断若是是 Swift 3.0 以前的代码,就须要禁用对 isa 的内存优化

image.png

  • 经过宏 TARGET_OS_OSX 判断是不是 macOS 执行环境
  • 判断 macOS 的系统版本,若是小于 10.11 则说明 app 太陈旧了,须要禁用掉 non-pointer isa
  • 而后再遍历全部的 Mach-O 的头部,判断若是有 __DATA__,__objc_rawisa 段的存在,则禁用掉 non-pointer isa ,由于不少新的 app 加载老的扩展的时候会须要这样的判断操做。

image.png

预先优化过的类不会加入到 gdb_objc_realized_classes 这个哈希表中来, gdb_objc_realized_classes 哈希表的装载因子为 0.75,这是一个通过验证的效率很高的扩容临界值。

  • 加载全部类到类的 gdb_objc_realized_classes 表中来

咱们查看这个表的定义:

image.png

// This is a misnomer: gdb_objc_realized_classes is actually a list of  // named classes not in the dyld shared cache, whether realized or not.

这是一个误称:gdb_objc_realized_classes 表实际上存储的是不在 dyld 共享缓存里面的命名类,不管这些类是否实现

除了 gdb_objc_realized_classes 表以外,还有一张表 allocatedClasses :

image.png

  • 经过 objc_allocateClassPair 开辟以后的类和元类存储的表(也就是说须要 alloc )

其实 gdb_objc_realized_classes 对 allocatedClasses 是一种包含的关系,一张是类的总表,一张是已经开辟了内存的类表,


Discover classes 流程

image.png

Discover classes. Fix up unresolved future classes. Mark bundle classes. 发现类。修正未解析的 future 类,标记 bundle 类。

  • 先经过 _getObjc2ClassList 来获取到全部的类,咱们能够经过 MachOView 来验证:

image.png

  • 接着仍是遍历全部的 Mach-O 的 header 部分,而后经过 mustReadClasses 来判断哪些条件能够跳过读取类这一步骤
  • 读取 header 是不是 Bundle
  • 读取 header 是否开启了 预优化
  • 遍历 _getObjc2ClassList 取出的全部的类
    • 经过 readClass 来读取类信息
    • 判断若是不相等而且 readClass 结果不为空,则须要从新为类开辟内存

Fix up remapped classes 流程

image.png

修复 重映射类 类表和非懒加载类表没有被重映射 (也就是 _objc_classlist) 因为消息转发,类引用和父类引用会被重映射 (也就是 _objc_classrefs)

**

  • 经过 noClassesRemapped 方法判断是否有类引用(_objc_classrefs)须要进行重映射
    • 若是须要,则遍历 EACH_HEADER
    • 经过 _getObjc2ClassRefs 和 _getObjc2SuperRefs 取出当前遍历到的 Mach-O 的类引用和父类引用,而后调用 remapClassRef 进行重映射

image.png


Fix up @selector references 流程

image.png

修正 SEL 引用

  • 操做前先加一个 selLock 锁
  • 而后遍历 EACH_HEADER
    • 若是开启了预优化,contiue 到下一个 Mach-O
    • 经过 _getObjc2SelectorRefs 拿到全部的 SEL 引用
    • 而后对全部的 SEL 引用调用 sel_registerNameNoLock 进行注册

也就是说这一流程最主要的目的就是注册 SEL ,咱们注册真正发生的地方: __sel_registerName ,这个函数若是你们常常玩 Runtime 确定不会陌生:

image.png

咱们简单分析一下 __sel_registerName 方法的流程:

  • 判断是否要加锁
  • 若是 sel 为空,则返回一个空的 SEL
  • builtins 中搜索,看是否已经注册过,若是找到,直接返回结果
  • namedSelectors 哈希表中查询,找到了就返回结果
  • 若是 namedSelectors 未初始化,则建立一下这个哈希表
  • 若是上面的流程都没有找到,则须要调用 sel_alloc 来建立一下 SEL ,而后把新建立的 SEL 插入哈希表中进行缓存的填充

Fix up old objc_msgSend_fixup call sites 流程

image.png

修正旧的 objc_msgSend_fixup 调用

**
这个流程的执行前提是 FIXUP 被开启。

  • 仍是老套路,遍历 EACH_HEADER
    • 经过 _getObjc2MessageRefs 方法来获取当前遍历到的 Mach-O 镜像的全部消息引用
    • 而后遍历这些消息引用,而后调用 fixupMessageRef 进行修正

Discover protocols 流程

image.png

发现协议,并修正协议引用

**


Fix up @protocol references 流程

image.png

对全部的协议作重映射

**


Realize non-lazy classes 流程

image.png

初始化非懒加载类( **+load** 方法和静态实例)


Realize newly-resolved future classes 流程

image.png

初始化新解析出来的 future 类

**


Discover categories 流程

image.png

处理全部的分类,包括类和元类

**


到这里, _read_images 的流程就分析完毕,咱们能够新建一个文件来去掉一些干扰的信息,只保留核心的逻辑,这样从宏观的角度来分析更直观:

image.png

Q & A 环节 Q: dyld 主要逻辑是加载库,也就是镜像文件,可是加载完是怎么读取的呢? A: _read_images 是真正读取的地方

Q: SEL 方法编号什么时候加载? A: _read_images

3.3 read_class 分析

咱们探索了 _read_images 方法的流程,接下来让咱们把目光放到本文的主题 - 类的加载
既然是类的加载,那么咱们在前面所探索的类的结构中出现的内容都会一一重现。
因此咱们不妨直接进行断点调试,让咱们略过其它干扰信息,聚焦于类的加载。

  • 根据上一小节咱们探索的结果, doneOnce 流程中会建立两个哈希表,并无涉及到类的加载,因此咱们跳过
  • 咱们来到第二个流程 - **类处理 **


咱们在下图所示的位置处打上断点:

image.png

**
如上图所示,从 classList 中取出的 cls 只是一个内存地址,咱们尝试经过 LLDB 打印 cls 的 clas_rw_t :

image.png

能够看到 cls 的属性、方法、协议以及类名都为空,说明这里类并无被真正加载完成,咱们接着聚焦到 read_class 函数上面,咱们进入其内部实现,咱们大体浏览以后会定位到以下图所示的代码:

image.png

看起来类的信息在这里完成了加载,那么为了验证咱们的猜测,直接断点调试一下但发现断点根本走不进来,缘由在于这里的判断语句

if (Class newCls = popFutureNamedClass(mangledName))
复制代码

判断当前传入的类的类名是否有 future 类的实现,可是咱们刚才已经打印了,类名是空的,因此确定不会执行这里。咱们接着往下走:

image.png

  • addNamedClass 内部实际上是将 cls  插入到 gdb_objc_realized_classes 表

image.png

  • addclassTableEntry 内部是将 cls 插入到 allocatedClasses 表

image.png

分析完 read_class ,咱们回到 _read_images 方法

image.png

咱们能够看到 read_class 返回的 newCls 会进行一个判断,判断与传入 read_class 以前的 cls 是否相等,而在 read_class 内部只有一个地方对类的内容进行了改动,可是咱们刚才测试了是进不去的,因此这个 if 里面的内容咱们能够略过,也就是说 resolvedFutureClasses 的内容咱们均可以暂时略过。

总结一下 readClass :

  • 判断是否是要后期处理的类
    • 若是是的话,就取出后期处理的类,读取这个类的 data() 类设置 ro/rw
  • addNamedClass 插入总表
  • addClassTableEntry 插入已开辟内存的类的表

3.4 realizeClassWithoutSwift 分析

经过分析 read_class ,咱们能够得知,类已经被注册到两个哈希表中去了,那么如今一切时机都已经成熟了。可是咱们仍是要略过像 Fix up remapped classes 、 Fix up @selector references 、 fix up old objc_msgSend_fixup call sites 、 Discover protocols. Fix up protocol refs 、 Fix up @protocol references ,由于咱们的重点是类的加载,咱们最终来到了 Realize non-lazy classes (for +load methods and static instances) ,略去无关信息以后,咱们能够看到咱们的
主角 realizeClassWithoutSwift 闪亮登场了:

image.png

从方法的名称以及方法注释咱们能够知道, realizeClassWithoutSwift 是进行类的第一次初始化操做,包括分配读写数据也就是咱们常说的 rw ,可是并不会进行任何的 Swift 端初始化。咱们直接聚焦下面的代码:

image.png

  • 经过 calloc 开辟内存空间,返回一个新的 rw
  • cls 取出来的 ro 赋值给这个 rw
  • rw 设置到 cls 身上

那么是否是说在这里 rw 就有值了呢,咱们 LLDB 打印大法走起:

image.png

能够清楚地看到,此时 rw 仍是为空,说明这里只是对 rw 进行了初始化,可是方法、属性、协议这些都没有被添加上。

咱们接着往下走:

image.png

这里能够看到父类和元类都会递归调用 realizeClassWithoutSwift 来初始化各自的 rw 。为何在类的加载操做里面要去加载类和元类呢?回忆一下类的结构,答案很简单,要保证 superclass 和 isa 的完整性,也就是保证类的完整性,

image.png

上面的截图就是最好的证实,初始化完毕的父类和元类被赋值到了类的 superclass 和 isa 上面。

image.png

接着往下走能够看到,不光要把父类关联到类上面,还要让父类知道子类的存在。

最后一行代码是 methodizeClass(cls) ,注释显示的是 attach categories ,附加分类到类?咱们进入其内部实现一探究竟。

在探索 methodizeClass 前,咱们先总结一下 realizeClassWithoutSwift :

  • 读取 class  的 data()
  • ro/rw  赋值
  • 父类和元类实现
    • supercls = realizeClassWithoutSwift(remapClass(cls->superclass))
    • metacls = realizeClassWithoutSwift(remapClass(cls->ISA()))
  • 父类和元类归属关系
    • cls->superclass = supercls
    • cls->initClassIsa(metacls)
  • 将当前类连接到其父类的子类列表 addSubclass(supercls, cls)

3.5 methodizeClass 分析

image.png

对类的方法列表、协议列表和属性列表进行修正 附加 category  到类上面来

咱们直接往下面走:

// Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
复制代码
  • ro 中取出方法列表附加到 rw 上
property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
复制代码
  • ro 中取出属性列表附加到 rw 上
protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
复制代码
  • ro 中取出协议列表附加到 rw
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
复制代码
  • cls 中取出未附加的分类进行附加操做

咱们能够看到,这里有一个操做叫 attachLists ,为何方法、属性、协议都能调用这个方法呢?

image.png

image.png

image.png

咱们能够看到,方法、属性、协议的数据结构都是一个二维数组,咱们深刻 attachLists 方法内部实现:

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;//10
            uint32_t newCount = oldCount + addedCount;//4
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;// 10+4
   
            memmove(array()->lists + addedCount, array()->lists,
                    oldCount * sizeof(array()->lists[0]));
            
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
复制代码
  • 判断要添加的数量是否为 0,若是为 0,直接返回
  • 判断当前调用 attachLists 的 list_array_tt 二维数组有多个一维数组
    • 若是是,说明是多对多的关系
    • 这里会经过 realloc 对容器进行从新分配,大小为原来的大小加上新增的大小
    • 而后经过 memmove 把原来的数据移动到容器的末尾
    • 最后把新的数据拷贝到容器的起始位置
  • 若是调用 attachListslist_array_tt 二维数组为空且新增大小数目为 1,则直接取 addedList 的第一个 list 返回
  • 若是当前调用 attachListslist_array_tt 二维数组只有一个一维数组
    • 若是是,说明是一对多的关系
    • 这里会经过 realloc 对容器进行从新分配,大小为原来的大小加上新增的大小
    • 由于原来只有一个一维数组,因此直接赋值到新 Array 的最后一个位置
    • 而后把新数据拷贝到容器的起始位置

4、探索 load_images

咱们接着探索 _dyld_objc_notify_register 的第二个参数 load_images ,这个函数指针是在何时调用的呢,一样的,咱们接着在 dyld 源码中搜索对应的函数指针 sNotifyObjCInit :

image.png

能够看到,在 notifySingle 方法内部, sNotifyObjCInit 函数指针被调用了。根据咱们上一篇文章探索 dyld 底层能够知道, _load_images 应该是对于每个加载进来的 Mach-O 镜像都会递归调用一次。

咱们来到 libObjc 源码中 load_images 的定义处:

image.png

处理由 dyld 映射的给定镜像中的 +load  方法

  • 判断是否有 load 方法,若是没有,直接返回
  • 搜索 load 方法,具体实现经过 prepare_load_methods
  • 调用 load 方法,具体实现经过 call_load_methods

4.1 prepare_load_methods 分析

从这个方法名称,咱们猜想这里应该作的是 load 方法的一些预处理工做,让咱们来到源码进行分析:

void prepare_load_methods(const headerType *mhdr) {
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

/*********************************************************************** * 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

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

/*********************************************************************** * 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
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    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));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
复制代码
  • 首先经过 _getObjc2NonlazyClassList 获取全部已经加载进去的类列表
  • 而后经过 schedule_class_load 遍历这些类
    • 递归调用遍历父类的 load 方法,确保父类的 load 方法顺序排在子类的前面
    • 经过 add_class_to_loadable_list , 把类的 load 方法存在 loadable_classes 里面
    • image.png
  • 完成 schedule_class_load 以后,经过 _getObjc2NonlazyCategoryList 取出全部分类数据
  • 而后遍历这些分类
    • 经过 realizeClassWithoutSwift 来防止类没有初始化,若是已经初始化了则不影响
    • 经过 add_category_to_loadable_list ,加载分类中的 load 方法到 loadable_categories 里面
    • image.png

4.2 call_load_methods 分析


经过名称咱们能够知道 call_load_methods 应该就是 load 方法被调用的地方了。咱们直接看源码:

void call_load_methods(void) {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}
复制代码

call_load_methods 调用类和类别中全部未决的 +load 方法 类里面 +load 方法是父类优先调用的 而在父类的 +load 以后才会调用分类的 +load 方法

  • 经过 objc_autoreleasePoolPush 压栈一个自动释放池
  • do-while 循环开始
    • 循环调用类的 +load 方法直到找不到为止
    • 调用一次分类中的 +load 方法
  • 经过 objc_autoreleasePoolPop 出栈一个自动释放池

5、总结

至此, _objc_init 和 _dyld_objc_notify_register 咱们就分析完了,咱们对类的加载有了更细致的认知。 iOS 底层有时候探索起来确实很枯燥,可是若是能找到高效的方法以及明确本身的所探索的方向,会让本身从宏观上从新审视这门技术。是的,技术只是工具,咱们不能被技术所绑架,咱们要作到有的放矢的去探索,这样才能事半功倍。

相关文章
相关标签/搜索