前言,前面进行了dyld动态链接器的工作过程相关知识的探究,了解了iOS系统在main函数之前做了哪些准备工作,为以后我们研究类的加载也做了一些铺垫工作。接下来我们就开始进行OC类的加载原理的分析和探究。
一、猜想类的信息如何到内存的?
1、回忆dyld的加载过程和类信息的获取
dyld —— images —— 内存 —— Person(方法、协议、分类信息...) 通过dyld动态链接器,我们可以读取到MachO镜像中的所有信息,而这些信息是以地址的形式存在的,像类、类中的方法、协议、分类信息等...
images(MachO) —— 地址 —— 表 —— 类 —— 初始化(ro、rw) 如上图所示的MachO结构图,我们获取到MachO可以拿到类的相关信息,然后通过地址把类信息给存储到一张表中,表的区分就以类为基准。然后流程我们需要对ro和rw进行初始化过程,然后MachO中的一些信息就可以通过ro和rw进行访问了。
2、dyld与类加载的联系
在之前的dyld的分析过程中,有一个很重要的函数:_dyld_objc_notify_register,然后我们新建个工程,添加符号断点,如图:
通过堆栈分析,程序在执行了dyld的加载之后,进入了_objc_init过程,而函数_dyld_objc_notify_register是在_objc_init中执行的,属于类的加载过程了,然后接下来,就开始对类的加载的分析吧!
二、类的加载原理预备节
1、类的加载入口函数_objc_init
我们可以通过在runtime的源码中搜索_dyld_objc_notify_register,然后可以进入到_objc_init函数中:
我们找到_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()函数
通过函数内部的一些实现,可以通过里面的一些函数打印dyld加载的过程信息,还有一些环境变量的设置,比如进行下面操作:
-
3.1 通过解注释下面帮助打印的代码,可以看到整个函数执行过程的objc库的加载情况
-
3.2 通过设置环境变量可以控制特定环境下的打印数据
上面的可以控制 nonpointisa的使用与否,下面的可以控制所有使用load方法的类的打印。
-
3.3 还可以通过 OBJC_HELP 命令导出程序的加载过程
4、 tls_init()
关于线程key的绑定的一些析构函数的操作,这里不多做讲解。
5、static_init()
这是一个C++的构造器函数,使_objc_init()在执行前可以先调用我们自己的constructors方法。
这里我们做一个测试,看是否会先来到我们自己的constructors方法执行?
5.1 我们先在_objc_init所在的命名空间写上自定义的constructors方法.
5.2 然后再在static_init()内部的getLibobjcInitializerOffsets执行后,添加断点,进行调试,发现是先打印了我们自定义的构造方法。
6、runtime_init()
runtime运行时环境初始化,里面主要是:unattachedCategories,allocatedClasses两张用于存储的表,后面会进行分析。
7、exception_init()
-
7.1 函数作用与函数内部实现解析
初始化libobjc的异常处理系统,负责程序异常之后的事务处理工作。
可以看出这里传入的是一个引用传递方式,通过函数发生异常的时候,主动触发old_terminate来返回block的异常信息。
-
7.2 异常捕获分析案例
通过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)
复制代码
- &map_images 使用了引用类型,值拷贝操作,这样可以在内部函数执行的时候(存在递归调用images),通过指针传递实时返回相关的结果,防止在运算过程中,地址错误导致整个程序的错乱情况发生。
- load_images 主要进行一些load方法的加载工作
- unmap_image 对未加载的库的一些打印和内存释放操作
2、map_images_nolock
- 这里主要是进行了images的读取操作_read_images,images也就是MachO二进制文件,我们需要把类的信息从二进制文件中读取出来,前面的操作是为images的读取做一些必要的准备工作。
3、_read_images【核心部分,下面分别介绍过程】
- 上面的部分主要介绍控制循环和加载的一些条件,接着我们看下面的代码
上面主要进行了运行期间的打印、表的开辟工作、还有方法的对比和修复工作。
- UnfixedSelectors:这里通过加载MachO中的sel和dyld获取的sel,以dyld中获取的sel为基准进行对比,然后获取到已经实现和修复的方法,下面小节专门介绍过程,这里简单概括作用。
3.1 initializeTaggedPointerObfuscator
- 这个函数主要针对TaggedPointer做一些混淆工作,不做过多解释。
3.2 NXCreateMapTable函数
- 创建一张全局的表,通过这张表,可以找到全局任何位置的任何类和函数
NXMapTable *NXCreateMapTable(NXMapTablePrototype prototype, **unsigned** capacity) {
return NXCreateMapTableFromZone(prototype, capacity, malloc_default_zone());
}
复制代码
- 这里通过建立一张全局表来存储从MachO读取的数据,以下是详细的操作过程
- 表数据写入的过程是通过哈希插入的方式进行的,下面是哈希插入的过程
在这里我们可以看到几张表:
- 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相对位置不同,就需要进行一个重排和修复,这就是该函数的主要作用,下面分别介绍这个过程的细节。
-
其中,hasPreoptimizedSelectors存在的情况下,继续执行。
-
然后通过_getObjc2SelectorRefs去获取MachO中对应的sel。
-
通过sel_registerNameNoLock从dyld得到的sel的名称,去获取MachO中的同名的sel。
-
最后,获取到dyld和MachO的sel进行递归对比操作,如果dyld对比不一样,就把dyld的sel指向MachO的sel地址,最后更新的值以dyld加载的为准。
4、类的加载和修复工作
我们在执行完sel对比之后,在下面的条件判断中都假如断点,然后通过单步调试进入到Discover classes的过程。
4.1 我们来分析下下图lldb打印的内容的意义?
-
这是在readClass前后的cls的打印结果,很显然,我们通过readClass从MachO中读取到了类的相关信息,而我们是从__NSStackBlock__中拿到的类信息,说明在这个过程中已经做了一些相关类的处理。下面一节将重点对readClass进行探索和分析。
-
通过函数的相关注释,后面是对未来类或者新创建的类的一些处理,这里暂时不作为重点研究,接着看后面的过程。
5、readClass的探究和分析
首先我们看到readClass的实现代码,然后在几个条件判断中,分别加入断点进行调试,得到几个标志执行性的函数:
- addNamedClass(cls, mangledName, replacing);
- addClassTableEntry(cls);
- addRemappedClass
5.1 顺序执行分析
我们进来readClass之后,首先根据mangledName获取到cls的类名,我们通过对比自己创建的LGPerson和cls类名是否一致,进行打印:
-
① 获取mangledName
其实,是经过很多次对比过程才拿到的LGPerson的,不过最终我们还是拿到了!然后我们拿到名称为mangledName的cls之后,进行单步跟踪,进入到下面的条件中。
-
② 进入addNamedClass
进入到addNamedClass,根据注释我们知道这里是进行类名的加载的,作用就是先在全局的哈希表做一下记录。
addNamedClass内部的实现,这里对dyld获取的类和MachO的类进行了一些存储操作。
-
③ 进入addClassTableEntry
然后进行下一步单步跟踪,会发现进入到addClassTableEntry函数中,我们进入其中进行查看:
-
如果传入的类未知,就会加入到未知类的插入操作中去
-
如果添加类的话,也会顺便把类的元类信息给加载进来
-
④ 类的信息是否已经加进来了?
我们继续对readClass进行跟踪,发现走完所有的操作,并没有发现关于ro和rw的相关操作。那么这里为什么不做赋值处理呢?猜想系统只是在这里面做了占位的操作,把类的信息先塞进来,后续才进行其他的赋值操作。下面我们继续探索!
5.2 readClass总结
- ① 通过类名匹配把相关的类,插入到表中
- ② 利用类去查找元类,插入元类信息到表中
- ③ 对类没有做其他的操作处理,所以在这里类中的信息都是空的
我们把类都已经加进全局的表里面了,但是又是怎么进行的分类、属性等信息的绑定的呢?让我们继续探索!
三、类的加载原理(类的信息绑定:引出中篇)
1、类是从哪里开始加载其他信息呢?
接着上面我们进行的readClass我们继续寻找关于类的处理条件
从众多处理方法中,我们找到了这两个条件,然后断点跟进函数内部。
通过单步调试跟踪,我们发现除了一些对类和方法的修复操作外,函数最终来到了realizeClassWithoutSwift中,我们就进去realizeClassWithoutSwift函数中,看下里面的实现情况:
2、realizeClassWithoutSwift函数
上图主要是ro和rw的一些读取操作,也就是我们在对类操作之后,在这个方法中对类的信息进行了其他操作。
上图是为了类和元类在绑定前的准备条件工作。
上图通过superclass和isa进行了类、元类的关系链的绑定操作。
3、methodizeClass
在完成ro、rw和类的处理之后,进行类和Categories的关联绑定操作。 到这里,类的加载原理上篇已经完成,篇幅有点长,但是满满都是干货,再进行最后的总结工作,以理清本文的主要思想和思路:
四、文末总结
1、先给个大致的流程图
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 : 没有被处理的类 优化那些被侵犯的类
复制代码
🌺🌺🌺 更多内容期待与你一起分享,喜欢的话,点个赞加个关注,持续为您创造好的内容。