04-OC类的加载过程

OC底层原理探索文档汇总

前面三篇博客我们已经分析了对象和类的底层结构,并且通过方法调用的底层分析对对象和类的结构有了更深刻的认识,我们知道对象调用方法包含了对类的cache和方法列表的查找,可是我们虽然已经知道了类的结构,但是类的结构是如何形成的我们并不知道,因此本文就探索类的加载过程。

主要内容:

APP启动过程的简单认识
类的加载流程
分类的加载流程
方法列表的实现方式
load方法的调用过程

1. APP启动过程的简单认识

这里仅做简单说明,如果想要详细理解,现在可以查看我之前写的一篇笔记应用程序加载,后续会再输出博客详细说明APP从点击到启动的全过程。

1.1 动态库和静态库的认识

1.1.1 介绍

库是已写好的、供开发者使用的可复用代码,每个程序都要依赖很多基础的底层库。从本质上,库是一种可执行代码的二进制形式。可以被操作系统载入内存执行。库分为两种:静态库(.a .lib)和 动态库 (framework .so .dll)。
.a是纯二进制文件,.framework中除了有二进制文件外还有资源文件,.a文件不能直接使用,至少需要.h文件配合,而.framework可以直接使用。
.a + .h + sourceFile = .framework

所谓静态和动态是指链接过程,动静态是相对于编译期和运行期的,静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要载入静态库。而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

1.1.2 静态库

在链接阶段,会将汇编生成的目标文件.o 与 引用的库一起链接到可执行文件中。对应的链接方式称为 静态链接。
静态库中的所有指令都会包含进最终生成的文件中,静态库不能再包含其他的动态库或静态库,在动态链接库中还可以再包含其他的动态或静态链接库。


静态库.png

如果多个进程需要引用到【静态库】,在内存中就会存在多份拷贝,如上图中进程1 用到了静态库1、5,进程2也用到了静态库1、5,那么静态库1、5在编译期就分别被链接到了进程1和进程2中,假设静态库1占用2M内存,如果有20个这样的进程需要用到静态库1,将占用40M的空间。

特点:

  • 静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。
  • 使可执行文件变大,浪费空间和资源(占空间)
  • 程序的更新、部署与发布不方便,需要全量更新。如果 某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。

优缺点:
优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖
缺点:由于静态库会存在多分,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大

1.1.3 动态库

动态库在程序构建时并不会链接到目标代码中,而是在运行时才被载入,不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。

理解:

  • 动态库包含一些可供应用程序或其他动态链接库调用的函数

  • 在应用程序调用一个动态链接库里面的函数的时候,操作系统会将动态链接库的文件映射到进程的地址空间中,这样进程中所有的线程就可以调用动态链接库中的函数了

  • 动态链接库加载完成后,并没有将代码编译到可执行文件中,这个时候动态链接库对于进程来说只是一些被放在地址进程空间附加的代码和数据

  • 动态库在内存中只有一个,操作系统也只会加载一次到内存中。只是针对不同的进程进行各自的映射

  • 代码段在内存中的权限都是只读的,所以多个程序虽然使用同一个动态库,但是并不会修改源代码

  • 动态函数库的名字一般是libxxx.so,相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。


    动态库.png
  • 【动态库】在内存中只存在一份拷贝,如果某一进程需要用到动态库,只需在运行时动态载入即可。

特点:

  • 动态库把对一些库函数的链接载入推迟到程序运行时期(占时间)。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单,不需要重新编译,属于增量更新。

优缺点:

优点:

  • 减少打包后APP的大小,因为不需要拷贝至目标程序中
  • 共享内存、节约资源,因为同一份库被多个程序使用
  • 通过更新动态库即可更新程序,因为不需要重新编译
    缺点:
  • 动态库载入会带来一部分性能损失

注意:系统的.framework是动态库,自己建立的.framework是静态库

1.2 Mach-O的简单认识

程序想要运行起来,它的可执行文件格式就需要被操作系统所理解,对于 OS X 和 iOS 来说 Mach-O 是其可执行文件的格式。【Mach-O】 为 Mach Object 文件格式的缩写,是 iOS 系统不同运行时期 可执行文件 的文件类型统称。它是一种用于 可执行文件、目标代码、动态库、内核转储的文件格式。

1.2.1 文件类型:

Mach-O.png

有三种文件类型Executable、Dylib、Bundle.

Executable

Executable 是 app 的二进制主文件,我们可以在 Xcode 项目中的 products 文件中找到它

Executable.png
Dylib

Dylib 是动态库,动态库分为 动态链接库 和 动态加载库。
动态链接库: 在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中,随着程序启动而启动.

动态加载库: 当需要的时候再使用dlopen等通过代码或者命令的方式加载,程序启动后。

Bundle

Bundle 是一种特殊类型的Dylib,你无法对其进行链接。所能做的是在Runtime运行时通过dlopen来加载它,它可以在macOS 上用于插件。

Image 和 Framework
  • Image (镜像文件)包含了上述的三种类型;
  • Framework 可以理解为动态库

1.3 dyld的简单认识

  • the dynamic link editor动态链接器
  • 在 iOS / macOS 系统中,仅有很少的进程只需内核就可以完成加载,基本上所有的进程都是动态链接的
  • Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容填充,这个填充的工作就是由 dyld 来完成的。
  • 对于链接生成的MachO可执行文件,在程序启动时通过dyld动态链接器进行链接载入
  • APP启动过程主要研究的就是dyld的执行过程
dyld在APP启动时所处的位置.png

1.4 编译过程

10-文件加载过程.png
  1. 源文件:
    1. 载入.h、.m、.cpp等文件
  2. 预处理:
    1. 也叫预编译,可以替换所有的宏;处理条件预编译指令,比如#if;删除所有注释;展开头文件;产生.i文件
  3. 编译
    1. 词法分析、语法分析、语义分析
    2. 还会进行一些优化,比如真值判断,假值判断
    3. 将.i文件转换为汇编语言,产生.s文件
  4. 汇编
    1. 将汇编文件转换为机器可以执行的指令,产生.o文件
    2. 每一条汇编语句几乎都对应一条机器指令
  5. 链接
    1. 对.o文件中引用其他库的地方进行引用,生成最后的可执行文件MachO
    2. dyld就在此处起作用

1.5 APP启动过程简单说明

1.5.1 APP启动的起点

APP最早启动的是在哪里,通过堆栈信息打印发现是通过dyld库的_dyld_start来实现的。

在load方法中加一个断点,之后通过堆栈信息查看,可以看到_dyld_start是起始点。

因此需要下载一份dyld的源码来查看dyld源码

APP启动的起点.png

1.5.2 __dyld_start的查看

主要做了三件事

dyld_start.png

  1. 开始进行初始化链接载入等操作:call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
  2. 清空栈和跳转到主程序的start(不用关注)clean up stack and jump to "start" in main executable
  3. 开始执行main函数:LC_MAIN case, set up stack for call to main()

1.5.3 初始化方法

通过上文找到了dyld::main源码,总共有九步,因后续会写博客专门进行APP启动的分析,因此这里均不说明,可以查看我的笔记应用程序加载,写的还算详细。

在第八步执行初始化方法中我们会回调map_images和load_images,后来经查找是在objc_init中执行的。所以接下来就是在objc_init的分析。

2. objc_init的认识

从上文我们得知在APP启动时,会在objc_init函数中加载数据。

源码:

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
 引导初始化。通过dyld注册我们的镜像通知程序
 在库初始化之前
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    //读取影响运行时的环境变量,如果需要,还可以打开环境变量帮助export OBJC_HELP = 1
    environ_init();//环境变量初始化
    //关于线程key的绑定,例如线程数据的析构函数
    tls_init();
    //运行C++静态构造函数,在dyld调用我们的静态析构函数之间,libc会调用_objc_init(),因此我们必须自己进行初始化
    static_init();
    //runtime运行时环境初始化,里面主要是unattachedCategories、allocatedClass--开辟类、分类的表空间
    runtime_init();
    //初始化libobjc的异常处理系统
    exception_init();
    //缓存条件初始化
    cache_init();
    //启动回调机制,通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
    _imp_implementationWithBlock_init();

    /*
     _dyld_objc_notify_register -- dyld注册的地方
     - 仅供objc运行时使用
     - 注册处理程序,以便在映射、取消映射和初始化objc镜像文件时使用,dyld将使用包含objc_image_info的镜像文件数组,回调mapped函数
     
     map_images:映射镜像,将image镜像文件加载进内存时,会触发该函数,在这个过程中会加载类、分类、协议等数据
     load_images:加载镜像,dyld初始化image会触发该函数,load方法就会在此执行
     unmap_image:dyld将image移除时会触发该函数
     */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

代码解读:

  1. environ_init方法:环境变量初始化
  2. tls_init:线程key的绑定,本地线程池的初始化以及析构。
  3. static_init:运行系统级别的C++静态构造函数
  • 主要是运行系统级别的C++静态构造函数
  • 在dyld调用我们的静态构造函数之前,libc调用_objc_init方法
  • 即系统级别的C++构造函数先于自定义的C++构造函数运行
  • 是调用的系统的库的静态函数,而不是我们自己写的静态函数,它在dyld之前
  1. runtime_init:运行时环境初始化(重点)
void runtime_init(void)
{
    objc::unattachedCategories.init(32);//开辟分类的表空间
    objc::allocatedClasses.init();//开辟类的表空间
}
  • 主要是运行时的初始化,开辟类、分类的表空间
  • 主要分为两部分:分类初始化、类的表初始化

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

  • 主要是初始化libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理
  • 具体的使用:当有crash(crash是指系统发生的不允许的一些指令,然后系统给的一些信号)发生时,会来到_objc_terminate方法,走到uncaught_handler扔出异常
    详情可以看我的笔记Crash

6、cache_init:缓存初始化,主要进行缓存初始化.

7、_imp_implementationWithBlock_init:启动回调机制

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

8、_dyld_objc_notify_register:dyld注册(重点)接下来分析数据的加载就是看这里的函数

  • map_images:映射镜像,将image镜像文件加载进内存时,会触发该函数,在这个过程中会加载类、分类、协议等数据
  • load_images:加载镜像,dyld初始化image会触发该函数,load方法就会在此执行
  • unmap_image:dyld将image移除时会触发该函数

3. 类的加载过程

3.1 查找源码过程

由上文可知,map_images回调函数中将镜像文件加载到内存中,因此从这个函数开始查找源码

3.1.1 map_images

只调用了map_images_nolock函数

源码:

/***********************************************************************
* 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
 处理给定的镜像文件,这些镜像文件是通过dyld映射进来的
**********************************************************************/
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);
}

3.1.2 map_images_nolock

调用_read_images开始读取镜像文件

源码:

map_images_nolock().png

3.2 读取镜像文件

读取镜像文件,代码太多,只捡重点说明。

功能:
该函数总有这么几大块功能:
1、修复selector引用
2、类的处理
3、协议处理
4、分类的处理
5、非懒加载类的实现(需要设置非懒加载的时候才会进入)******重点看

我们在这里主要分析类的处理和非懒加载类的实现。

在类的底层分析中我们得知,类的数据存储在rw中,但是我们从内存获取到的数据是ro,所以需要重新构造一下,将ro数据加到rw和rwe中。

3.2.1 类的处理

将类从macho读取到内存中了,这里的读取其实不包括构造类的数据结构,data并没有加载进来,只是把Name和地址加进来。

3.2.1.1 处理类

源码:

for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            //这里是类的读取
            /*
             做了两件事:
             1、给类映射类名
             2、添加类和元类到所有类的表上
             */
            /*
             验证,读取前没有名字,读取后才有类名字
             */
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

            /*
             这里重来不会进来,不用分析
             它的作用是预编译过程中一些类发生了混乱,已经被移动了但是没有被删除,需要在这里处理一下
             */
            if (newCls != cls  &&  newCls) {
                // Class was moved but not deleted. Currently this occurs 
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }

调用readClass函数做了两件事:

  • 给类映射类名
  • 添加类和元类到所有类的表上

验证:

验证.png

  • 读取前这里只有地址,不知道类名,读取后就知道类名了,
  • 可以判断是这条语句可以让我们读取类名到表中的(类的结构还没构造好)
3.2.1.2 readClass

这里只做了两件事,一个是将类名与类映射,一个是将类的地址添加到所有类的动态表中
源码:

readClass.png

核心代码就两个

  • addNamedClass用来给类添加类名
  • addClassTableEntry用来添加类或元类到表上

注:这里有个坑点,就是第一次进入查看肯定会以为if (Class newCls = popFutureNamedClass(mangledName))判断语句是重点,以为是在这里添加到内存中的,然而并不是

3.2.1.2 addNamedClass

通过NXMapInsert函数将类名映射到类上,类和类名的映射是通过一个表来关联的。以后可以更方便的通过类名获取到类

源码:

/***********************************************************************
* addNamedClass
* Adds name => cls to the named non-meta class map.
* Warns about duplicate class names and keeps the old mapping.
* Locking: runtimeLock must be held by the caller
 给一个需要命名的非元类添加映射关系:name=>cls
 如果出现重复的类名,则发出警告,并且使用旧的映射关系
**********************************************************************/
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
    runtimeLock.assertLocked();
    Class old;
    if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);

        // getMaybeUnrealizedNonMetaClass uses name lookups.
        // Classes not found by name lookup must be in the
        // secondary meta->nonmeta table.
        addNonMetaClass(cls);
    } else {
        //在这里将类名添加到类的映射中
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
    ASSERT(!(cls->data()->flags & RO_META));

    // wrong: constructed classes are already realized when they get here
    // ASSERT(!cls->isRealized());
}
3.2.1.2 addClassTableEntry

有一个所有类的表,这个表包含了所有的类,现在就将这个类添加到表中,如果是元类也要把元类添加进去

添加类的地址到一个表中,此处也是为了更好的查找类,以后可以直接通过类地址查找类

这里添加的表是之前在objc_init中allocatedClasses开辟的空间。

源码:

/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
 将一个类cls添加到所有类的表中
 如果addMeta为true,则自动的将其元类也添加进去
**********************************************************************/
static void
addClassTableEntry(Class cls, bool addMeta = true)
{
    runtimeLock.assertLocked();

    // This class is allowed to be a known class via the shared cache or via
    // data segments, but it is not allowed to be in the dynamic table already.
    /*
     这个类可以通过共享的内存或数据段成为一个已知的类,但是不可以因为早已加入到动态列表而称为已知类
     当然这是因为我们此时要加入,所以也就不能在动态列表中存在了。
     */
    /*
     得到之前分配的动态列表(在objc_init中分配的)
     */
    auto &set = objc::allocatedClasses.get();

    ASSERT(set.find(cls) == set.end());

    //如果不是一个已知类,也就是表中不存在cls,则将其加入
    if (!isKnownClass(cls))
        set.insert(cls);
    //再继续添加其元类
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}

3.2.2 类数据的懒加载

这里的实现是类的属性、方法列表、协议列表的载入到rwe的过程,而前面类的加载其实只加载了类的名称和地址到表上

  • 可以看到其实就是通过realizeClassWithoutSwift函数来实现类的加载的。
  • 正常懒加载的时候会在第一次调用方法时进行实现
  • 如果是非懒加载会在载入到内存时就会实现
// Category discovery MUST BE Late to avoid potential races
    // when other threads call the new category code before
    // this thread finishes its fixups.

    // +load handled by prepare_load_methods()

    // Realize non-lazy classes (for +load methods and static instances)
    //5、非懒加载类的实现,需要设置非懒加载的时候才会进入
    /*
     正常懒加载的时候会在第一次调用方法时进行实现,
     如果是非懒加载会在载入到内存时就会实现
     类的实现的方法就是realizeClassWithoutSwift(),
     */
    for (EACH_HEADER) {
        classref_t const *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            addClassTableEntry(cls);

            if (cls->isSwiftStable()) {
                if (cls->swiftMetadataInitializer()) {
                    _objc_fatal("Swift class %s with a metadata initializer "
                                "is not allowed to be non-lazy",
                                cls->nameForLogging());
                }
                // fixme also disallow relocatable classes
                // We can't disallow all Swift classes because of
                // classes like Swift.__EmptyArrayStorage
            }
            realizeClassWithoutSwift(cls, nil);
        }
    }
3.2.2.1 懒加载和非懒加载的认识

类的懒加载和非懒加载这里是指类的实现时机,类的实现函数是realizeClassWithoutSwift(),因此看的是realizeClassWithoutSwift的调用时机,在什么时候调用。
如果类是非懒加载,则会在_read_images()中类加载时进行实现。
如果类是懒加载,则在第一次调用方法时才会去实现。

懒加载和非懒加载的区别:区别在于是否实现load方法,有load()方法,就是非懒加载,如果没有则是懒加载。这是因为load方法的调用在load_image()中,而此时类如果还没有实现,是没法调用的,所以如果一个类带有load方法,就必须要非懒加载。

懒加载:

时机: 懒加载是在第一次调用方法的时候加载

调用过程:

  • 之前所学习的在方法列表中查找imp时会进行一次判断,如果没有被实现过则会进行一次实现(具体查看lookupImpOrForward函数,在上一篇博客已经详细分析过,不再赘述)


    懒加载过程.png

优点:

  • 把类的实现推迟到启动后,启动更快
  • 一些类可能难以使用到,不去实现可以减少内存的浪费

懒加载:

时机: 非懒加载是在APP启动加载类数据时就会加载

调用过程:

非懒加载过程.png

优点:

  • 可以提早执行load方法

注意:

  • 苹果默认的就是懒加载,但是为了给开发者更大的灵活性,所以也有非懒加载的流程
  • 没有必要就用懒加载,因为可能有的类并不会立马使用,如果在main函数执行前就去实现,就会导致启动时间慢,又占内存。
  • 想要在第一次调用方法前执行某些代码,可以用initialize来执行,而不是load

3.3 类列表数据的构造过程

上面只是将类的名称和地址添加到表中,并没有看到rwe的方法列表、协议列表、属性列表、变量列表的构造过程,现在看下它是什么时候构造的。

3.3.1 realizeClassWithoutSwift

代码比较长,只分析关键代码

函数描述:

  • realizeClassWithoutSwift
  • Performs first-time initialization on class cls, 展示第一次的类的初始化
  • including allocating its read-write data.包括分配它的rw数据
  • Does not perform any Swift-side initialization.不执行任何的swift端的初始化
  • Returns the real class structure for the class. 返回一个真实的类结构
  • Locking: runtimeLock must be write-locked by the caller 必须加锁

说明:

  1. realizeClassWithoutSwift用来第一次对类进行初始化
  2. 还包括分配类的rw数据
  3. 这里说的返回一个真实的类的结构很有道理,因为二进制文件加载进内存中并不是最终的类结构,因为还需要设置rwe,所以想要得到我们所需要的类结构,就需要通过类的实现将类信息按照类结构的形式赋值

代码结构:
1、设置rw
2、递归实现父类和元类的继承链
3、附着类别数据

1、设置rw

获取到原始数据ro,存放到rw中,由此我们可知类结构中的rw中包含ro
关于rw和ro的操作具体的可以查看我的博客02-OC类的底层分析

//将类的data()从ro变为rw
/*
 此时获取的ro是刚从内存中获取的,而我们所需要构造的是rw,rw中会存储有ro
 */
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    rw = cls->data();
    ro = cls->data()->ro();
    ASSERT(!isMeta);
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {//正常进这里
    // Normal class. Allocate writeable class data.
    //很简单,只是创建rw,并将ro赋值进去,之后将rw赋给类
    rw = objc::zalloc();//创建rw
    rw->set_ro(ro);
    rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
    cls->setData(rw);
}
  • rw里保存的就是数据信息
  • ro是保存在rw中的,所以需要赋值给rw
  • 先将信息从类中获取到,这个类就是macho中读取的类,此时虽然有data(),但这是从MachO中获取的原始数据
  • 原始数据其实就是ro,这是类本身的数据,也就是干净内存,所以可以通过强转为class_ro_t

2、递归实现父类和元类的继承链rw
递归realizeClassWithoutSwift函数。也就是说对当前类的实现,也需要把该类的继承链和元类继承链都要实现一下。

// Realize superclass and metaclass, if they aren't already.
    // This needs to be done after RW_REALIZED is set above, for root classes.
    // This needs to be done after class index is chosen, for root metaclasses.
    // This assumes that none of those classes have Swift contents,
    //   or that Swift's initializers have already been called.
    //   fixme that assumption will be wrong if we add support
    //   for ObjC subclasses of Swift classes.
    /*
     实现父类和元类,因为是递归执行,所以会将继承链上的都会实现
     一直执行到根类和根元类的实现完成后才会结束
     假设这些类没有swift内容或swift内容已经实现
     如果我们需要支持swift类的objc子类,那么这个假设是不成立的。
     注:这里的swift并不是swift语言的意思。
     */
    
    supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

3、附着类别数据rw
调用methodizeClass函数附着分类数据,所以接下来就是查看methodizeClass。

// Attach categories
    //附着分类数据
    methodizeClass(cls, previously);
  • 对类的rwe进行设置
  • 将分类信息添加到rwe中
  • 肃然这里注释写的是附着分类数据,其实rwe中也会把类的数据也就是ro添加到rwe中

小结:

  • 加载到内存中的原始的类的数据格式并不是我们所需要的格式,所以就需要重新设置,其实设置的就是rw
  • 类的实现其实就是将类的bits中的data进行重新赋值,基本上就是给rw进行赋值,包括ro和rwe
  • 而这个data()就是rw,rw包含ro和rwe
  • ro是类本身的数据,包括方法列表、属性列表、协议列表、成员变量列表
  • rwe是分类以及运行时改变的数据,包括方法列表、属性列表、协议列表,还有ro

3.3.2 methodizeClass

这个类名表示组织化类。
重点需要注意rwe的方法列表、协议列表、属性列表中也包含有ro的列表,而不单单是分类中或运行时创建的列表。

代码结构:

  1. 方法排序
  2. 添加ro数据到rwe中
  3. 附着分类数据到rwe中

源码:

/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
/*
 1、修复这个类的方法列表、协议列表、属性列表
    1.1 方法列表会进行排序
    1.2 ro的数据添加到rwe中
 2、附着分类(未进行附着的分类)
 */

static void methodizeClass(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    //先初始化一些数据
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro();
    auto rwe = rw->ext();

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    // Install methods and properties that the class implements itself.
    /*
     void attachLists(List* const * addedLists, uint32_t addedCount),注意第一个参数是List,也就是可以存放多个列表,所以第二个参数是列表的数量
     */
    //先拿到这个类自身的方法列表
    method_list_t *list = ro->baseMethods();
    //1、进行排序;2、附着到rwe上
    if (list) {
        //进行一个准备工作,也就是对方法列表的修复,其实就是排序
        
        /*
         prepareMethodLists该方法可以传入多个方法列表,之后对多个方法列表分别进行排序
         此处传入的是ro的方法列表,所以只有一个方法列表,列表数量就为1
         */
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        
        //
        /*
         1、这个方法的意思是附着方法列表到原有列表上
         2、这里的做法是将原始类的方法列表附着到rwe的方法列表上
         3、也就是说rwe的methods包含了ro中的baseMethods,通过查看rwe的结构也可以证明这一点
         4、该方法可以传入多个列表,
         5、这里传入的是ro的方法列表,所以只有一个方法列表,列表数量就为1
         */
        if (rwe) rwe->methods.attachLists(&list, 1);
    }

    
    //下面同理
    property_list_t *proplist = ro->baseProperties;
    if (rwe && proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (rwe && protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    
    //这里没有进入,这里先不看
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }�
    }�
    //直接执行这里
    //上面是把ro的基础方法列表加入到rwe中,接下来是将分类的方法列表加入到rwe中
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

#if DEBUG
    // Debug: sanity-check all SELs; log method list contents
    for (const auto& meth : rw->methods()) {
        if (PrintConnecting) {
            _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(meth.name));
        }
        ASSERT(sel_registerName(sel_getName(meth.name)) == meth.name); 
    }
#endif
}

代码说明

  • 先获取到ro的基本方法列表
  • 通过prepareMethodLists进行排序,这样方法查找时可以用二分查找法来查找imp
  • 通过attachLists将ro的数据附着到rwe上
  • 属性和协议列表也通过attachLists附着到rwe上
  • 上面是把ro的列表加到了rwe中,还需要通过objc::unattachedCategories.attachToClass将分类的方法列表加到rwe中
  • 列表有三种需要操作的,方法列表、协议列表、属性列表(变量列表不存放到rwe中,所以不需要进行操作)

3.3.3 prepareMethodLists

源码:

static void 
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                   bool baseMethods, bool methodsFromBundle)
{
    runtimeLock.assertLocked();

    if (addedCount == 0) return;

    // There exist RR/AWZ/Core special cases for some class's base methods.
    // But this code should never need to scan base methods for RR/AWZ/Core:
    // default RR/AWZ/Core cannot be set before setInitialized().
    // Therefore we need not handle any special cases here.
    if (baseMethods) {
        ASSERT(cls->hasCustomAWZ() && cls->hasCustomRR() && cls->hasCustomCore());
    }

    // Add method lists to array.
    // Reallocate un-fixed method lists.
    // The new methods are PREPENDED to the method list array.

    /*
     1、添加方法列表到数组中
     2、对方法列表进行排序
     3、新的方法需要放在方法列表的前面
     */
    //所以最终的方法列表中的方法是:后加载的分类放置在最前面(因为倒序插入,后面会看到),每一个分类和类自身的方法列表会根据方法选择器进行排序。
    //取出其中的每个方法列表进行排序,也就是说不是所有的方法排序,而是方法列表内部排序
    for (int i = 0; i < addedCount; i++) {
        //获取每一个方法列表,一个方法列表就是一个分类的所有方法
        method_list_t *mlist = addedLists[i];
        ASSERT(mlist);

        // Fixup selectors if necessary
        //如果需要修复方法选择器
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }

    // If the class is initialized, then scan for method implementations
    // tracked by the class's flags. If it's not initialized yet,
    // then objc_class::setInitialized() will take care of it.
    if (cls->isInitialized()) {
        objc::AWZScanner::scanAddedMethodLists(cls, addedLists, addedCount);
        objc::RRScanner::scanAddedMethodLists(cls, addedLists, addedCount);
        objc::CoreScanner::scanAddedMethodLists(cls, addedLists, addedCount);
    }
}

代码说明:
1、添加方法列表到数组中
2、对方法列表进行排序
3、新的方法需要放在方法列表的前面

  • 这里其实就做了一件事,就是将方法进行排序,这样就可以方便后面的获取了。在方法查找时可以通过二分查找法进行查找。
  • 取出其中的每个方法列表进行排序,也就是说不是所有的方法排序,而是方法列表内部排序

3.3.4 fixupMethodList

记住一点就行,方法列表是通过方法选择器的地址进行排序的

源码:

/*
 核心代码就是根据选择器地址进行排序
 */
static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    runtimeLock.assertLocked();
    ASSERT(!mlist->isFixedUp());

    // fixme lock less in attachMethodLists ?
    // dyld3 may have already uniqued, but not sorted, the list
    if (!mlist->isUniqued()) {
        mutex_locker_t lock(selLock);
    
        // Unique selectors in list.
        for (auto& meth : *mlist) {
            const char *name = sel_cname(meth.name);
            meth.name = sel_registerNameNoLock(name, bundleCopy);
        }
    }

    // Sort by selector address.
    //根据选择器地址进行排序
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
    
    // Mark method list as uniqued and sorted
    mlist->setFixedUp();
}

3.3.5 attachLists

源码:

//addedLists是添加的多个列表的集合
    //
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        //为什么list可以放在局部,因为前面已经定义过了,这里只是数组list_array_tt的一个方法
        /*
         1、先创建空间,之后将旧的插入到后面,新的插入到前面。
         */
        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]));
        }
        //如果list不存在,则说明第一次附着列表,所以把当前第一个的列表(方法、协议、属性)获取到并传入到list中
        //此时就是一维数组,此处只是一个指针
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        }
        //如果存在,且只有第一个,
        else {
            // 1 list -> many lists,这里得到的是二维数组
            /*
             1、旧的只有一个,新的有多个
             2、先生成新的数组
             3、将旧的放到addedCount的位置,也就是放到了最后
             4、将新的lists放到0-addedCount的位置,也就是一个一个放到前面
             5、这样就完成了方法列表的倒序插入
             */
            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;
            //附着addedLists到lists上
            /*
             第一个参数是开始位置
             第二个参数是插入的内容
             第三个参数是插入的大小
             */
            //所以这里是从首地址到最后一个位置中间插入addedLists
            //这里是指针数组
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

代码解析:

  • 第一种是首次添加,也就是添加原始类的数据时调用的,直接赋值
  • 第二种是第一次添加分类数据时,需要将一个数据放到最后,将新数据放到前面
  • 第三种是后续添加新方法时,需要将前面所有数据放到最后,将新数据放到前面

说明:

  • 数据列表是倒序插入,前面已经插入的放在后面,新插入的放在前面。
  • 这里将分类的数据列表添加到类的rwe中
  • 第一次添加的是类的ro中的数据列表,第二次和后面添加的是分类中的数据列表

3.3.6 attachToClass

将分类或运行时创建的数据添加到类的rwe上

源码:

//这里的previously,前面传入的一直是nil,不用处理
    /*
     获取到所有的分类,将分类数据一个一个添加到rwe中
     */
    void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        //当分类数据已经加载过了,就可以通过get()获取到
        auto &map = get();
        auto it = map.find(previously);
        if (it != map.end()) {
            //这里拿到所有的类别
            category_list &list = it->second;
            //也有类方法
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                /*
                 cls:原始类
                 list.array() :拿到所有的类别
                 list.count():类别的数量
                 */
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                //只有实例方法
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }

说明:

  • 这里通过get()拿到的是分类中的数据
  • load_images函数将分类数据加载后存放到数组中,在类进行加载时直接拿过来附着
  • 这里调用attachCategories函数传入的分类是这个类当前可以加载的所有分类。

3.3.7 attachCategories

附着分类数据到类上,最多不超过64个。

源码:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
/*
 1、附着分类的方法、协议、属性列表到类上
 2、假设cats中的类别都已经加载完成了,并且是按照加载顺序进行了排序,最早的类别优先执行,也就是在前面(cats就是所有的分类)
 */
/*
 cls:类原始类
 cats_list:所有类别的列表
 cats_count:类别的数量
 */
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    /*
     1、只有极少数类在启动期间拥有超过64个类别,所以这里限制为64
     2、这里使用了小堆栈,可以避免malloc,也就是不需要通过malloc去开辟空间
     3、类别的插入顺序是倒序,也就是从后往前插
     4、实现方式是:从前到后迭代cats列表
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    //创建一个空的rwe,extAllocIfNeeded()就是返回rwe的,如果已经存在则返回,如果不存在则创建
    //因此只要看看哪里调用了这个函数,就能知道哪里会进行,经查看runtime动态创建方法、协议、属性时会调用
    //也就是说rwe是会在类别和runtime时创建方法、协议、属性时会使用
    auto rwe = cls->data()->extAllocIfNeeded();

    /*
     
     */
    for (uint32_t i = 0; i < cats_count; i++) {//这里是这个原始类的分类的数量
        auto& entry = cats_list[i];

        //获取这个分类的类方法列表或实例方法列表(通过isMeta来判断)
        //此时的cat已经有自己的名称了
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        //从后往前插,倒序插入
        if (mlist) {
            //这里到64,就说明已经添加满了。下标为0的位置已经添加了。
            if (mcount == ATTACH_BUFSIZ) {
                /*
                 1、再次进行排序
                 2、此时排序就是对所有的分类的方法列表的排序
                 3、并且是每个分类的方法列表单独排序,而不是把所有的方法放在一起排序
                 */
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                //这里是大于64才会执行
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            
            //这里是先加后减,所以是从最后一个下标63开始的,并且不断减小
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    /*
     判断mcount>0,说明没有满64个分类,需要在这里附着
     将最终的mlists列表添加到rwe上,这个列表是类别+类总共的方法列表
     */
    if (mcount > 0) {
        /*
         prepareMethodLists
         第一个参数是原始类、第二个参数是方法列表,第三个是数量。
         mlists是第一个方法列表,mcount插入的方法列表的数量
         因为方法列表是从后往前插入的,所以想要平移到存有方法列表的最小位置,就需要平移个ATTACH_BUFSIZ - mcount大小,把这里作为开始位置
         */
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        /*
         第一个参数是起始位置
         第二个参数是数量
         */
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

代码解析:

  • 可以插入的方法列表限制为64,因为极少数类在启动期间拥有超过64个分类
  • 只要会修改方法、协议、属性就会往rwe中插入数据,而在类别和runtime运行时创建方法、协议、属性时会修改
  • rwe中的列表最终添加的是所有的分类+原始类

说明:

  • 这个函数的作用就是附着分类的方法、协议、属性列表到类上
  • 插入的过程是倒序插入,先插入的在后面,后插入的在前面
  • 插入是以一个方法列表一个方法列表的形式插入的,而不是一个方法一个方法插入的
  • 倒序插入导致了后加载的分类的方法比类的方法先执行
  • 方法选择进行排序导致了后面的二分查找(排序不是把所有的分类和类的方法混在一起排,而是各排各的)
  • 一个类并不是最多只有64个分类,而是在启动时加载的时候只能加载64个分类,因为只有极少数类在启动期间拥有超过64个类别
    • 为什么要限制为64,我猜可能是因为在进行方法列表的保存时,要8字节对齐,也就是8的倍数,因为开辟空间的话一般是以8的倍数开辟的
    • 如果再*8,就是512的大小了,但是基本上是没有这么多的分类的

3.4 总结

  1. 类加载到内存中,需要重新构造类,构造的就是rw,rw包括rwe和ro
  2. 类的处理包括给类映射类名、将类和元类添加到所有类的表中
  3. rw的构造过程就是给rw添加ro,并且生成rwe
  4. rwe的生成过程是先添加ro的数据,之后获取到分类数据,添加到rwe中
  5. ro以及分类中的方法列表均要按照方法选择地址排序,方便后续的二分查找法
  6. 在将方法列表添加到类的rwe时,需要倒序插入。后加载分类的在前面,后加载的分类在后面,类的数据在最后。
11-方法列表的存储格式.png
14-类、分类加载的代码流程图.png
14-类的加载和实现.png

4. 分类的加载过程

分类的数据是在load_images中加载的,需要从load_images开始分析

4.1 load_images

做了两件事,一个是分类的加载,一个是load的加载和调用,我们这里只分析分类的加载。

源码:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //分类的加载(内部会进行判断,如果类已经实现,就附着数据到类中,如果没有实现,就先保存到数组中等类实现的时候去获取)
        loadAllCategories();
    }

    // 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
    //1、准备load数据,也就是对load方法的加载
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    //2、调用+load方法
    call_load_methods();
}

4.2 loadAllCategories

调用load_categories_nolock函数实现

源码:

static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);

    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        load_categories_nolock(hi);
    }
}

4.3 load_categories_nolock

截取主要代码,该函数中用来附着分类到主类上

源码:

// Process this category.
            //处理分类
            if (cls->isStubClass()) {//这一块先不用管
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                /*
                 因为存根类永远不会实现(可能相当于Java的抽象类),而且存根类在初始化之前不知道他们自己的元类
                 所以我们必须把分类的类方法和属性添加到存根类自身
                 methodizeClass可以找出他们,并且添加加到元类
                 */
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {//这是我们正常处理的地方
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                //将该分类注册到目标类上
                //如果这个目标类已经被实现了,就重新构建这个类的方法列表
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    //如果类已经实现了,就直接附着
                    if (cls->isRealized()) {
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    }
                    //如果没有实现,就需要addForClass,等类的实现
                    else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                //类方法添加到元类上
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
  • 如果主类已经实现完成,直接将分类数据附着到主类上
  • 如果主类还没有实现,调用addForClass函数先添加到数组中,待主类实现时直接从数组中获取分类数据。

4.4 分类的懒加载和非懒加载

我们知道类的懒加载可以推迟加载时机到第一次调动方法时,那么分类的懒加载和非懒加载的加载时机是怎么样。分类的非懒加载也是通过实现load方法来判断的


类和分类的懒加载.png
  • 主类是非懒加载,其分类也会标记为非懒加载,提前进行加载
  • 如果分类是非懒加载,会促使主类也变为非懒加载
  • 如果主类和分类都是懒加载,则只会在第一次调用方法时加载

注:这里说的类的加载并非是从二进制文件加载到内存中,而是构造rwe,我称其为类的实现。

4.5 分类数据的加载总结

14-类、分类加载的代码流程图.png
  • 分类数据载入到内存后,需要附着到主类的rwe中
  • 如果主类还没有实现,也就是还没有rwe时,会通过addForClass将数据加载到数组中,待主类实现
  • 如果主类已经实现,直接调用attachCategories附着到主类的rwe中

5. load的加载过程

在load_images中先将带有load方法的类或分类存储在一个表中,之后再从表中获取该类或分类进行调用。重点注意存储的顺序和调用顺序

5.1 表结构

load数据存储在表结构中,下面会存储和获取,所以这里先简单认识表结构,有个印象就行

类load表:loadable_class

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

分类load表:loadable_category

struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

计数和开辟空间大小

// List of classes that need +load called (pending superclass +load)
// This list always has superclasses first because of the way it is constructed
static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;

// List of categories that need +load called (pending parent class +load)
static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;

5.2 load_images查看

先准备load数据,也就是将有load方法的类或分类存储在一个表中

代码:

/*
 1、分类的加载
 2、load的加载和调用
 */
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //分类的加载(内部会进行判断,如果类已经实现,就附着数据到类中,如果没有实现,就先保存到数组中等类实现的时候去获取)
        loadAllCategories();
    }

    // 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
    //1、准备load数据,也就是对load方法的加载
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    //2、调用+load方法
    call_load_methods();
}

5.3 添加load方法到表中

分别将带有load方法的类和分类添加到表中

5.3.1 prepare_load_methods准备load数据

源码:

//分别对类和分类进行添加
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
    runtimeLock.assertLocked();
    //所有的类
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    //将所有的类进行load  schedule表示排定计划,这里可以看做是添加
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
    //所有的分类
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    //分类的加载,需要先让目标类实现
    /*
     1、分类cat的目标类必须存在,而且分类已经被附着到目标类上,才可以添加
     2、目标类必须是变为可连接的,也就是添加到了类load方法表中,而执行了类的load方法之后才可以保存分类的load方法
     */
    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");
        }
        //***这一步就会附着分类数据,因为在前面loadAllCategorie()函数中已经执行了addForClass函数,将数据保存了下来,所以此时就可以直接使用分类数据进行附着了。
        //这里也就说明了,如果分类的load要加载的话,也必须要让目标类非懒加载
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        
        //添加到表上
        add_category_to_loadable_list(cat);
    }
}
  • 分别将类和分类加入到各自的load方法的表上
  • 但是分类还需要有一个操作,就是让目标类去实现一下,这是因为分类的load方法也是类去调用的,所以目标类必须实现。
  • 从这里也就可以很清楚的知道了:分类的load方法会促使目标类也进行非懒加载

5.3.2 schedule_class_load添加类到表中

将继承体系上所有的类都加到表中,通过调用add_class_to_loadable_list函数进行添加
而且先添加父类,再添加自己

/*
 1、安排列入这个镜像中的类的+load方法
 2、其他镜像中的父类和这个镜像的所有分类
 */
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确保父类优先添加
    //递归父类进行实现load
    schedule_class_load(cls->superclass);

    //添加类到所有的类load的表
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

5.3.3 add_class_to_loadable_list添加类到表中

  • 如果使用的空间已经等于了开辟的空间,就需要扩容,二倍再加16
  • 之后需要把这个类和load方法添加到loadable_classes表中
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
 类cls必须是变成可连接的
 如果他实现了一个load方法,就必须列入它到表中
**********************************************************************/
/*
 1、将类、load方法存入到loadable_classes表中,并计数+1
 2、如果容量不足,需要扩容,*2+16
 */
void add_class_to_loadable_list(Class cls)
{
    IMP method;
    loadMethodLock.assertLocked();
    //得到load方法
    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());
    }
    //如果使用的空间等于了开辟的空间,就需要扩容处理。*2+16
    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));
    }
    //加入这个类,和方法,并且计数+1
    loadable_classes[loadable_classes_used].cls = cls;//添加类
    loadable_classes[loadable_classes_used].method = method;//添加方法
    loadable_classes_used++;//计数+1
}

5.3.4 getLoadMethod得到load方法

这里是获取load方法,需要注意的是从元类中拿数据,而不是从类中拿,因为load是类方法

IMP 
objc_class::getLoadMethod()
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    
    ASSERT(isRealized());//当前类是否实现
    ASSERT(ISA()->isRealized());
    ASSERT(!isMetaClass());
    ASSERT(ISA()->isMetaClass());

    //从中可以看出是从元类拿出来的类方法,load方法是类方法
    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name);
            if (0 == strcmp(name, "load")) {
                return meth.imp;
            }
        }
    }

    return nil;
}

5.3.5 add_category_to_loadable_list分类的过程

其流程和类一样,就不再赘述了。
需要注意的是分类必须已经附着到目标类上,才可以添加。

/***********************************************************************
* 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.
 1、分类cat的目标类存在,而且分类已经被附着到目标类上,才可以添加
 2、目标类必须是变为可连接的,也就是添加到了类load方法表中,而执行了类的load方法之后才可以保存分类的load方法
 
 上面的条件已经在被调用的方法中进行了处理
 
 具体的处理流程和类load表一样,就不赘述了。
**********************************************************************/
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 (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    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++;
}

5.4 调用+load方法

重点注意调用的顺序

5.4.1 call_load_methods

考虑到可能理解的并不准确,因此我把英文原文也贴出来,有错误还请指正。

源码:

/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.调用所有待处理的类和分类的load方法
* Class +load methods are called superclass-first. 以父类优先的方式调用load方法
* Category +load methods are not called until after the parent class's +load.在起源类的+load方法之后才会调用分类load方法
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
 
 这个方法必须是可重入的,因为一个laod方法可能会触发更多的镜像文件来加载(动态库)
 在调用时,必须保留父类的第一顺序,因此只有该函数的最后一层调用的操作才是有效的
 这个调用将处理所有可加载的类,甚至是在运行时生成的类
 
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.
 
 下面的顺序是通过load方法时的顺序
 
* Sequence:顺序
* 1. Repeatedly call class +loads until there aren't any more反复调用class +laods,直到没有更多
* 2. Call category +loads ONCE.一次性加载分类+load
* 3. Run more +loads if:在以下情况下运行更多+load
*    (a) there are more classes to load, OR仍然有更多的类要去加载
*    (b) there are some potential category +loads that have 
*        still never been attempted.还有一些潜在的分类没有完成(这里的潜在是指它本身没有load方法,但是和他相同目标类的分类有,也会促使它也去加载)
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class. 
*
 分类只能运行一次,以确保起源类优先的顺序,因为还有分类促使了一个新可加载类,新的可加载分类附着到这个类上
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/

/*
 上面那些话说了这么几个事情:
 1、起源类优先加载,分类后加载
 2、越是最后加载的分类越是有效的,因为覆盖掉了
 3、先把所有的起源类调用一遍,之后查询所有有load方法的分类,
 4、可加载的分类会促使起源类的加载,而起源类的加载会促使一些潜在的分类进行加载。
 */

/*
 执行顺序:
 1. 反复执行类的load方法,直到没有更多的类
 2. 一次性加载所有的分类
 3. 再加载更多的分类
    1)仍然有更多的类加载了
    2)还有一些潜在的分类需要加载
 */

//需要注意的是:起源类的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();

    //这里循环遍历是因为起源类没有load方法,而分类有load方法,会促使起源类调动load方法。
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        //循环执行,避免当执行完成后又新增了。
        while (loadable_classes_used > 0) {
            //执行所有的类load表中的load方法
            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;
}

执行顺序:

  1. 反复执行类的load方法,直到没有更多的类
  2. 一次性加载所有的分类
  3. 再加载更多的分类
    1)仍然有更多的类加载了
    2)还有一些潜在的分类需要加载

重点说明:

  • 该方法是用来调用所有的类和分类的load方法
  • 并且先调用父类的load方法,再调用子类的。
  • 先调用起源类的+load方法,再调用分类的load方法。
  • 分类中越是最后加载的分类越后执行
  • 可加载的分类会促使起源类的加载,而起源类的加载会促使一些潜在的分类进行加载。

5.4.2 call_class_loads

这里就是最后的调用过程,将表中所有类取出来,调用load方法

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
 调用所有的挂起的类的load方法,也就是存放在类load表中的load方法
 如果有新的类变成可以load的状态,他们这些load不会被调用。(这里可能是说还没有被挂起)
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    //调用所有的loads
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        //调用
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

5.4.3 call_load_methods

  • 如果有一个类有了实现load方法的分类,那么此时这个类应当实现附着与它的所有分类
  • 调用load方法,需要注意的是获取到起源类来调用,分类本身是不能调用的,这也是前面为什么需要类实现。
/***********************************************************************
* call_category_loads
* Call some pending category +load methods.
* The parent class of the +load-implementing categories has all of 
*   its categories attached, in case some are lazily waiting for +initalize.
 如果有一个类有了实现load方法的分类,那么此时这个类应当实现附着与它的所有分类,以防还有分类正在惰性的等待类的+initalize.
* Don't call +load unless the parent class is connected.
 除非类已连接,否则不要调用+load。
* If new categories become loadable, +load is NOT called, and they 
*   are added to the end of the loadable list, and we return TRUE.
 如果有新分类是变为可加载的,而且没有调用load方法,需要添加到可加载列表的末尾,然后返回True
* Return FALSE if no new categories became loadable.
* 如果没有新的分类可以变为可加载的,就直接返回true
* Called only by call_load_methods().
**********************************************************************/
/*
 做了两件事:
 1、调用load方法
 2、删除loadable_category中已执行了load方法的分类
 3、将剩下的分类重新创建到loadable_category中
 */
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    //查询所有的实现了+loads方法的分类并进行判断后删除
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        //拿到目标类
        cls = _category_getClass(cat);
        //如果这个类存在且是可加载的
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            //调用,这里接受者是类,而不可能是分类
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;//删除这个分类
        }
    }

    // Compact detached list (order-preserving)
    //压缩列表,因为前面会有删除多个cat,所以这里把删除的位置清掉
    shift = 0;
    for (i = 0; i < used; i++) {
        //如果不是nil就向前走shift个位置
        if (cats[i].cat) {
            //[i-shift]是最新的位置
            //cats[i]是当前要判断的位置
            cats[i-shift] = cats[i];
        }
        //如果是nil,就+1,说明需要前进1位
        else {
            shift++;
        }
    }
    //得到删除后的总数
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    //下面是重新保存一下
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    //如果没有就初始化
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

5.5 调用顺序验证

创建一个父类WYPerson和子类WYStudent,以及分类WYStudent+cate1,都加上load方法进行打印

结果:

2021-10-20 19:59:19.020175+0800 类的加载[28551:322449] +[WYPerson load]
2021-10-20 19:59:19.020583+0800 类的加载[28551:322449] +[WYStudent load]
2021-10-20 19:59:19.020659+0800 类的加载[28551:322449] +[WYStudent(cate1) load]

可以看到调用顺序是父类、子类、分类。

5.6 总结

  1. 带有load方法的类和分类分别存储在一个表中
  2. 带有load方法的分类的起源类必须实现,也就是进行非懒加载,因为分类在调用时要通过起源类来调用,分类自己无法调用函数
  3. 调用load时,先调用父类、再调用子类,最后调用分类,越迟加载的分类越晚调用
  4. 分类的load会促使原始类也进行加载
  5. load方法分别放在了类的load表和分类的load表中,调用时是通过表里拿到的,而不是直接通过方法列表拿到的调用的
load调用流程.jpg

6. 总结

你可能感兴趣的:(04-OC类的加载过程)