Objective-C类的加载原理上

Objective-C类的加载原理上

前言,前面进行了dyld动态链接器的工作过程相关知识的探究,了解了iOS系统在main函数之前做了哪些准备工作,为以后我们研究类的加载也做了一些铺垫工作。接下来我们就开始进行OC类的加载原理的分析和探究。

一、猜想类的信息如何到内存的?

1、回忆dyld的加载过程和类信息的获取

dyld —— images —— 内存 —— Person(方法、协议、分类信息...) 通过dyld动态链接器,我们可以读取到MachO镜像中的所有信息,而这些信息是以地址的形式存在的,像类、类中的方法、协议、分类信息等... image.png images(MachO) —— 地址 —— 表 —— 类 —— 初始化(ro、rw) 如上图所示的MachO结构图,我们获取到MachO可以拿到类的相关信息,然后通过地址把类信息给存储到一张表中,表的区分就以类为基准。然后流程我们需要对ro和rw进行初始化过程,然后MachO中的一些信息就可以通过ro和rw进行访问了。

2、dyld与类加载的联系

在之前的dyld的分析过程中,有一个很重要的函数:_dyld_objc_notify_register,然后我们新建个工程,添加符号断点,如图:

image.png

通过堆栈分析,程序在执行了dyld的加载之后,进入了_objc_init过程,而函数_dyld_objc_notify_register是在_objc_init中执行的,属于类的加载过程了,然后接下来,就开始对类的加载的分析吧!

二、类的加载原理预备节

1、类的加载入口函数_objc_init

我们可以通过在runtime的源码中搜索_dyld_objc_notify_register,然后可以进入到_objc_init函数中:

image.png

我们找到_objc_init的函数实现的地方,发现在_dyld_objc_notify_register之前,还有很多其他的函数操作,这些函数的作用是什么呢?这里我们简单的了解下。

2、_objc_init中几个重要的函数

//_objc_init中几个重要的函数
    environ_init() : 
    tls_init();
    static_init();
    runtime_init();
    exception_init();

#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();
    //dyld函数调用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    
复制代码

基于内容的重要性,关于上面几个函数的作用都已经注释说明,最终的目的都是为类的加载过程做的一系列的铺垫,下面我们简单的介绍下几个函数的作用

3、 environ_init()函数

image.png

通过函数内部的一些实现,可以通过里面的一些函数打印dyld加载的过程信息,还有一些环境变量的设置,比如进行下面操作:

  • 3.1 通过解注释下面帮助打印的代码,可以看到整个函数执行过程的objc库的加载情况

image.png

  • 3.2 通过设置环境变量可以控制特定环境下的打印数据

image.png

上面的可以控制 nonpointisa的使用与否,下面的可以控制所有使用load方法的类的打印。

  • 3.3 还可以通过 OBJC_HELP 命令导出程序的加载过程

image.png

4、 tls_init()

关于线程key的绑定的一些析构函数的操作,这里不多做讲解。 image.png

5、static_init()

这是一个C++的构造器函数,使_objc_init()在执行前可以先调用我们自己的constructors方法。 image.png 这里我们做一个测试,看是否会先来到我们自己的constructors方法执行?

5.1 我们先在_objc_init所在的命名空间写上自定义的constructors方法.

image.png

5.2 然后再在static_init()内部的getLibobjcInitializerOffsets执行后,添加断点,进行调试,发现是先打印了我们自定义的构造方法。

image.png

6、runtime_init()

runtime运行时环境初始化,里面主要是:unattachedCategories,allocatedClasses两张用于存储的表,后面会进行分析。 image.png

7、exception_init()

  • 7.1 函数作用与函数内部实现解析

初始化libobjc的异常处理系统,负责程序异常之后的事务处理工作。 image.png

可以看出这里传入的是一个引用传递方式,通过函数发生异常的时候,主动触发old_terminate来返回block的异常信息。 image.png

  • 7.2 异常捕获分析案例

image.png

通过NSSetUncaughtExceptionHandler函数,调用自定义的 void LGExceptionHandlers(NSException *exception)方法,来捕获程序运行中的异常信息。这里注意:异常不是错误!

8、cache_t::init()

这里对缓存条件初始化

9、_imp_implementationWithBlock_init()

启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib。这里也不做过多分析。

10、小结

以上我们对_objc_init中的关于环境变量控制和类加载的准备过程的进行了分析,大致了解了在类加载前的_objc_init中都做了哪些事情,接下来,就让我们直接进入到类的加载的原理分析中吧!

三、类的加载原理正式篇章

1、_dyld_objc_notify_register函数

_dyld_objc_notify_register(&map_images, load_images, unmap_image)

复制代码

image.png

  • &map_images 使用了引用类型,值拷贝操作,这样可以在内部函数执行的时候(存在递归调用images),通过指针传递实时返回相关的结果,防止在运算过程中,地址错误导致整个程序的错乱情况发生。
  • load_images 主要进行一些load方法的加载工作
  • unmap_image 对未加载的库的一些打印和内存释放操作

image.png

2、map_images_nolock

image.png

  • 这里主要是进行了images的读取操作_read_images,images也就是MachO二进制文件,我们需要把类的信息从二进制文件中读取出来,前面的操作是为images的读取做一些必要的准备工作。

3、_read_images【核心部分,下面分别介绍过程】

image.png

  • 上面的部分主要介绍控制循环和加载的一些条件,接着我们看下面的代码

image.png

上面主要进行了运行期间的打印、表的开辟工作、还有方法的对比和修复工作。

  • UnfixedSelectors:这里通过加载MachO中的sel和dyld获取的sel,以dyld中获取的sel为基准进行对比,然后获取到已经实现和修复的方法,下面小节专门介绍过程,这里简单概括作用。

3.1 initializeTaggedPointerObfuscator

  • 这个函数主要针对TaggedPointer做一些混淆工作,不做过多解释。 image.png

3.2 NXCreateMapTable函数

  • 创建一张全局的表,通过这张表,可以找到全局任何位置的任何类和函数

NXMapTable *NXCreateMapTable(NXMapTablePrototype prototype, **unsigned** capacity) { 

    return  NXCreateMapTableFromZone(prototype, capacity, malloc_default_zone());

}

复制代码
  • 这里通过建立一张全局表来存储从MachO读取的数据,以下是详细的操作过程 image.png
  • 表数据写入的过程是通过哈希插入的方式进行的,下面是哈希插入的过程 image.png

在这里我们可以看到几张表:

  • gdb_objc_realized_classes:全局表
  • objc::unattachedCategories 关联分类的表
  • objc::allocatedClasses 已经被开辟的表

关于表的扩容问题:

  • 扩容系数:4/3
  • 假如总大小为:8*3/4
  • 那么扩容规则为:x = 8* 3/4 * 4/3
  • 结果:x就是扩容后的大小

3.3 UnfixedSelectors 函数

  • 这是一个回调函数,返回一个SEL类型参数给调用者,具体调用的地方先不去跟踪,先分析内部的执行流程。

实际作用是,把MachO中的sel拿出来,放到总表之中,因为MachO中的sel相对位置不同,就需要进行一个重排和修复,这就是该函数的主要作用,下面分别介绍这个过程的细节。

image.png

  • 其中,hasPreoptimizedSelectors存在的情况下,继续执行。

  • 然后通过_getObjc2SelectorRefs去获取MachO中对应的sel。 image.png

  • 通过sel_registerNameNoLock从dyld得到的sel的名称,去获取MachO中的同名的sel。

  • 最后,获取到dyld和MachO的sel进行递归对比操作,如果dyld对比不一样,就把dyld的sel指向MachO的sel地址,最后更新的值以dyld加载的为准。

4、类的加载和修复工作

image.png 我们在执行完sel对比之后,在下面的条件判断中都假如断点,然后通过单步调试进入到Discover classes的过程。

4.1 我们来分析下下图lldb打印的内容的意义?

image.png

  • 这是在readClass前后的cls的打印结果,很显然,我们通过readClass从MachO中读取到了类的相关信息,而我们是从__NSStackBlock__中拿到的类信息,说明在这个过程中已经做了一些相关类的处理。下面一节将重点对readClass进行探索和分析。

  • 通过函数的相关注释,后面是对未来类或者新创建的类的一些处理,这里暂时不作为重点研究,接着看后面的过程。

5、readClass的探究和分析

image.png

首先我们看到readClass的实现代码,然后在几个条件判断中,分别加入断点进行调试,得到几个标志执行性的函数:

  • addNamedClass(cls, mangledName, replacing);
  • addClassTableEntry(cls);
  • addRemappedClass

5.1 顺序执行分析

我们进来readClass之后,首先根据mangledName获取到cls的类名,我们通过对比自己创建的LGPerson和cls类名是否一致,进行打印:

  • ① 获取mangledName

image.png

其实,是经过很多次对比过程才拿到的LGPerson的,不过最终我们还是拿到了!然后我们拿到名称为mangledName的cls之后,进行单步跟踪,进入到下面的条件中。

  • ② 进入addNamedClass

image.png 进入到addNamedClass,根据注释我们知道这里是进行类名的加载的,作用就是先在全局的哈希表做一下记录。

image.png addNamedClass内部的实现,这里对dyld获取的类和MachO的类进行了一些存储操作。

  • ③ 进入addClassTableEntry

image.png 然后进行下一步单步跟踪,会发现进入到addClassTableEntry函数中,我们进入其中进行查看:

image.png

  • 如果传入的类未知,就会加入到未知类的插入操作中去

  • 如果添加类的话,也会顺便把类的元类信息给加载进来

  • ④ 类的信息是否已经加进来了?

我们继续对readClass进行跟踪,发现走完所有的操作,并没有发现关于ro和rw的相关操作。那么这里为什么不做赋值处理呢?猜想系统只是在这里面做了占位的操作,把类的信息先塞进来,后续才进行其他的赋值操作。下面我们继续探索!

5.2 readClass总结

  • ① 通过类名匹配把相关的类,插入到表中
  • ② 利用类去查找元类,插入元类信息到表中
  • ③ 对类没有做其他的操作处理,所以在这里类中的信息都是空的

我们把类都已经加进全局的表里面了,但是又是怎么进行的分类、属性等信息的绑定的呢?让我们继续探索!

三、类的加载原理(类的信息绑定:引出中篇)

1、类是从哪里开始加载其他信息呢?

接着上面我们进行的readClass我们继续寻找关于类的处理条件

image.png 从众多处理方法中,我们找到了这两个条件,然后断点跟进函数内部。

image.png 通过单步调试跟踪,我们发现除了一些对类和方法的修复操作外,函数最终来到了realizeClassWithoutSwift中,我们就进去realizeClassWithoutSwift函数中,看下里面的实现情况:

2、realizeClassWithoutSwift函数

image.png

上图主要是ro和rw的一些读取操作,也就是我们在对类操作之后,在这个方法中对类的信息进行了其他操作。

image.png

上图是为了类和元类在绑定前的准备条件工作。

image.png

上图通过superclass和isa进行了类、元类的关系链的绑定操作。

3、methodizeClass

image.png

在完成ro、rw和类的处理之后,进行类和Categories的关联绑定操作。 到这里,类的加载原理上篇已经完成,篇幅有点长,但是满满都是干货,再进行最后的总结工作,以理清本文的主要思想和思路:

四、文末总结

1、先给个大致的流程图

image.png

2、流程总结

  • 我们从_objc_init开始进行类的加载原理探索,进行了一系列的环境准备工作,然后来到函数_dyld_objc_notify_register(&map_images, load_images, unmap_image),开始类的加载过程。

  • 通过 &map_images 我们找到了_read_images函数,在内部我们进行了类的加载操作:


1: 条件控制进行一次的加载

2: 修复预编译阶段的 `@selector` 的混乱问题

3: 错误混乱的类处理 

4:修复重映射一些没有被镜像文件加载进来的 类 

5: 修复一些消息!

6: 当我们类里面有协议的时候 : readProtocol 

7: 修复没有被加载的协议

8: 分类处理

9: 类的加载处理

10 : 没有被处理的类 优化那些被侵犯的类

复制代码

🌺🌺🌺 更多内容期待与你一起分享,喜欢的话,点个赞加个关注,持续为您创造好的内容。

分类:
iOS
标签: