iOS之武功秘籍⑧: 类和分类加载过程

2021-02-25  本文已影响0人  長茳

iOS之武功秘籍 文章汇总

写在前面

在上一篇文章iOS之武功秘籍⑦: dyld加载流程 -- 应用程序的加载中轻描淡写的提了一句_objc_init_dyld_objc_notify_register,本文将围绕它展开探索分析类和分类的加载.

本节可能用到的秘籍Demo

一、_objc_init方法

① environ_init方法

environ_init()方法是初始化一系列环境变量,并读取影响运行时的环境变量

有以下两种方式可以打印所有的环境变量

这些环境变量,均可以通过target -- Edit Scheme -- Run --Arguments -- Environment Variables配置,其中常用的环境变量主要有以下几个(环境变量汇总见文末!):

①.1 环境变量 - OBJC_DISABLE_NONPOINTER_ISA

OBJC_DISABLE_NONPOINTER_ISA为例,将其设置为YES,如下图所示

所以OBJC_DISABLE_NONPOINTER_ISA可以控制isa优化开关,从而优化整个内存结构

② 环境变量 - OBJC_PRINT_LOAD_METHODS

所以,OBJC_PRINT_LOAD_METHODS可以监控所有的+load方法,从而处理启动优化(后续文章会讲解启动优化方法)

② tls_init方法

tls_init()方法是关于线程key的绑定,主要是本地线程池初始化以及析构

③ static_init方法

static_init()方法注释中提到该方法会运行C++静态构造函数(只会运行系统级别的构造函数)

dyld调用静态构造函数之前,libc会调用_objc_init,所以必须自己去实现

④ runtime_init方法

主要是运行时的初始化,主要分为两部分:分类初始化类的表初始化(后续会详细讲解对应的函数)

⑤ exception_init方法

exception_init()主要是初始化libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理

① crash分类

crash的主要原因是收到了未处理的信号,主要来源于三个地方:kernel内核,其他进行,App本身.

所以相对应的,crash也分为了3种

针对应用级异常,可以通过注册异常捕获的函数,即NSSetUncaughtExceptionHandler机制,实现线程保活, 收集上传崩溃日志

② 应用级crash拦截

所以在开发中,会针对crash进行拦截处理,即app代码中给一个异常句柄NSSetUncaughtExceptionHandler,传入一个函数给系统,当异常发生后,调用函数(函数中可以线程保活、收集并上传崩溃日志),然后回到原有的app层中,其本质就是一个回调函数,如下图所示

上述方式只适合收集应用级异常,我们要做的就是用自定义的函数替代该ExceptionHandler即可

⑥ cache_t::init()方法

主要是缓存初始化,源码如下

⑦ _imp_implementationWithBlock_init方法

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

⑧ _dyld_objc_notify_register:dyld注册

这个方法的具体实现在iOS之武功秘籍⑦: dyld加载流程 -- 应用程序的加载已经有详细说明,其源码实现是在dyld源码中,以下是_dyld_objc_notify_register方法的声明

_dyld_objc_notify_register方法的注释中可以得出:

_dyld_objc_notify_register中的三个参数含义如下:

二、dyld与Objc的关联

其方法的源码实现与调用如下,即dyld与Objc的关联可以通过源码体现

dyld源码--具体实现

libobjc源码中--调用

从上可以得出

dyld源码--具体实现中,点击registerObjCNotifiers进去有

所以 有以下等价关系

load_images调用时机

iOS之武功秘籍⑦: dyld加载流程 -- 应用程序的加载中,我们知道了load_images是在notifySingle方法中,通过sNotifyObjCInit调用的,如下所示

map_images调用时机

关于load_images的调用时机已经在dyld加载流程中讲解过了,下面以map_images为例,看看其调用时机

现在我们在梳理下dyld流程:

所以有以下结论:map_images是先于load_images调用,即先map_images ,再load_images.

dyld与Objc关联

结合dyld加载流程,dyldObjc的关联如下图所示

下面我们看下map_imagesload_imagesunmap_image都做了什么.

其中代码通过编译,读取到Mach-O可执行文件中,再从Mach-O中读取到内存,如下图所示

三、map_images

在查看源码之前,首先需要说明为什么map_images&,而load_images没有

当镜像文件加载到内存时map_images会触发,即map_images方法的主要作用是将Mach-O中的类信息加载到内存.

map_images调用map_images_nolock,其中hCount表示镜像文件的个数,调用_read_images来加载镜像文件(此方法的关键所在)

_read_images

_read_images主要是加载类信息,即类、分类、协议等,进入_read_images源码实现,主要分为以下几部分:

①. 条件控制进行的一次加载 一一 创建表

doneOnce流程中通过NXCreateMapTable 创建表,存放类信息,即创建一张类的哈希表 -- gdb_objc_realized_classes,其目的是为了类查找方便、快捷

查看gdb_objc_realized_classes的注释说明,这个哈希表用于存储不在共享缓存且已命名类无论类是否实现,其容量是类数量的4/3.

②. 修复预编译阶段的@selector的混乱问题

主要是通过通过_getObjc2SelectorRefs拿到Mach_O中的静态段__objc_selrefs,遍历列表调用sel_registerNameNoLockSEL添加到namedSelectors哈希表中

其中selector --> sel并不是简单的字符串,是带地址的字符串.

_getObjc2SelectorRefs的源码如下,表示获取Mach-O中的静态段__objc_selrefs,后续通过_getObjc2开头的Mach-O静态段获取,都对应不同的section name

sel_registerNameNoLock源码路径如下:sel_registerNameNoLock -> __sel_registerName,如下所示,其关键代码是auto it = namedSelectors.get().insert(name);,即将sel插入namedSelectors哈希表

③. 错误混乱的类处理

主要是从Mach-O中取出所有类,在遍历进行处理

通过代码调试,知道了在未执行readClass方法前,cls只是一个地址

在执行readClass方法后,cls是一个类的名称

到这步为止,类的信息目前仅存储了地址+名称

经过调试并没有执行if (newCls != cls && newCls) {}里面的流程.

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

主要是将未映射的ClassSuper Class进行重映射,其中

⑤. 修复一些消息

主要是通过_getObjc2MessageRefs 获取Mach-O的静态段 __objc_msgrefs,并遍历通过fixupMessageRef将函数指针进行注册,并fix为新的函数指针

⑥. 当类里面有协议时:readProtocol 读取协议

⑦. 修复没有被加载的协议

主要是通过 _getObjc2ProtocolRefs 获取到Mach-O的静态段 __objc_protorefs(与⑥中的__objc_protolist并不是同一个东西),然后遍历需要修复的协议,通过remapProtocolRef比较当前协议和协议列表中的同一个内存地址的协议是否相同,如果不同则替换

其中remapProtocolRef的源码实现如下

⑧. 分类处理

主要是处理分类,需要在分类初始化并将数据加载到类后才执行,对于运行时出现的分类,将分类的发现推迟到对_dyld_objc_notify_register的调用完成后的第一个load_images调用为止

⑨. 类的加载处理

主要是实现类的加载处理,实现非懒加载类

苹果官方对于非懒加载类的定义是:

NonlazyClass is all about a class implementing or not a +load method.
所以实现了+load方法的类是非懒加载类,否则就是懒加载类

为什么实现load方法就会变成非懒加载类?

懒加载类在什么时候加载

⑩. 没有被处理的类,优化那些被侵犯的类

主要是实现没有被处理的类,优化被侵犯的类


我们需要重点关注的是 ③中 的readClass以及 ⑨中 realizeClassWithoutSwift两个方法

③中 的 readClass

readClass主要是读取类,在未调用该方法前,cls只是一个地址,执行该方法后,cls是类的名称,其源码实现如下,关键代码是addNamedClassaddClassTableEntry,源码实现如下

通过源码实现,主要分为以下几步:

所以综上所述,readClass的主要作用就是将Mach-O中的类读取到内存,即插入表中,但是目前的类仅有两个信息:地址以及名称,而mach-O的其中的data数据还未读取出来.

⑨中 的 realizeClassWithoutSwift:实现类

realizeClassWithoutSwift方法中有ro、rw的相关操作,这个方法在消息流程的慢速查找中有所提及,方法路径为:慢速查找(lookUpImpOrForward) -- realizeAndInitializeIfNeeded_locked -- realizeClassMaybeSwiftAndLeaveLocked -- realizeClassMaybeSwiftMaybeRelock -- realizeClassWithoutSwift(实现类)

realizeClassWithoutSwift方法主要作用是实现类,将类的data数据加载到内存中,主要有以下几部分操作:

① 读取 data 数据,并设置 ro、rw

读取classdata数据,并将其强转为ro,以及rw初始化ro拷贝一份到rw中的ro

② 递归调用 realizeClassWithoutSwift 完善 继承链

递归调用realizeClassWithoutSwift完善继承链,并设置当前类、父类、元类的rw

这里有一个问题,realizeClassWithoutSwift递归调用时,isa找到根元类之后,根元类的isa是指向自己,并不会返回nil,所以有以下递归终止条件,其目的是保证类只加载一次

realizeClassWithoutSwift

remapClass方法中,如果cls不存在,则直接返回nil

③ 通过 methodizeClass 方法化类

通过methodizeClass方法,从ro中读取方法列表(包括分类中的方法)、属性列表、协议列表赋值给rw,并返回cls

断点调试 realizeClassWithoutSwift (objc4-818.2版本)

如果我们需要跟踪自定义类,同样需要在_read_images方法中的第九步的realizeClassWithoutSwift调用前增加自定义逻辑,主要是为了方便调试自定义类

我们看值都为空其中ro_or_rw_extro或者rw_extro是干净的内存(clean memory),rw_ext是脏内存(dirty memory).

此时打印cls,我们发现最后的地址为空的

这是因为roread only是一块干净的内存地址,那为什么会有一块干净的内存和一块脏内存呢?这是因为iOS运行时会导致不断对内存进行增删改查,会对内存的操作比较严重,为了防止对原始数据的修改,所以把原来的干净内存copy一份到rw中,有了rw为什么还要rwe(脏内存),这是因为不是所有的类进行动态的插入,删除.当我们添加一个属性,一个方法会对内存改动很大,会对内存的消耗很有影响,所以我们只要对类进行动态处理了,就会生成一个rwe.

这里我们需要去查看set_ro的源码实现,其路径为:set_ro -- set_ro_or_rwe(找到 get_ro_or_rwe,是通过ro_or_rw_ext_t类型从ro_or_rw_ext中获取) -- ro_or_rw_ext_t中的ro

通过源码可知ro的获取主要分两种情况:有没有运行时

在这里会调用父类,以及元类让他们也进行上面的操作,之所以在此处就将父类,元类处理完毕的原因就是确定继承链关系,此时会有递归,当cls不存在时,就返回.

继续往下走,来到 if (isMeta) {代码处,此时的isMetaYES,是因为它确实是元类. cls->setInstancesRequireRawIsa();此方法就是设置isa.

我们看到此时的cls确实是元类.

methodizeClass:方法化类

其中methodizeClass的源码实现如下,主要分为几部分:

rwe的逻辑

方法列表加入rwe的逻辑如下:

方法如何排序

在消息流程的慢速查找流程iOS之武功秘籍⑥:Runtime之方法与消息文章中,方法的查找算法是通过二分查找算法,说明sel-imp是有排序的,那么是如何排序的呢?

验证方法排序

下面我们可以通过调试来验证方法的排序

所以 排序前后的methodlist对比如下,所以总结如下:methodizeClass方法中实现类中方法(协议等)的序列化.

我们看到此时的rweNULL,也就是rew没有赋值,没有走(即data()->ro->rw->rwe(没有走))??这是为什么?此问题我们后面分析....

小伙到这,你是否又想起了另一个问题呢?
在非懒加载的时候我们知道realizeClassWithoutSwift的调用时机,那么懒加载是什么时候调用realizeClassWithoutSwift的呢.

在我们的测试代码里把+load方法注释掉

同时在main方法里调用cj_instanceMethod1方法

realizeClassWithoutSwift方法中打断点,断点过来,我们打堆栈信息,如下

为什么能到realizeClassWithoutSwift方法呢?因为我们调用了alloc方法,进行了消息的发送.这个流程我们在前面讲iOS之武功秘籍⑥:Runtime之方法与消息的时候说了.这就是懒加载的魅力所在,就是在第一次处理消息的时候才去现实类的加载.

所以懒加载类非懒加载类数据加载时机如下图所示

attachToClass方法

attachToClass方法主要是将分类添加到主类中,其源码实现如下

因为attachToClass中的外部循环是找到一个分类就会进到attachCategories一次,即找一个就循环一次.

attachCategories方法

attachCategories方法中准备分类的数据,其源码实现如下

总结:本类中需要添加属性、方法、协议等,所以需要初始化rwe,rwe的初始化主要涉及:分类addMethodaddPropertyaddprotocol , 即对原始类进行修改或者处理时,才会进行rwe的初始化.

attachLists方法:插入

attachLists是如何插入数据的呢?方法属性协议都可以直接通过attachLists插入吗?

方法、属性继承于entsize_list_tt协议则是类似entsize_list_tt实现,都是二维数组.

进入attachLists方法的源码实现

attachLists的源码实现中可以得出,插入表主要分为三种情况:

针对情况③1对多,这里的lists是指分类

哼,只有原理没有操作,我信你个鬼,那接下来,我们就来验证一方.

rwe 数据加载(验证)

准备好测试代码本类TCJPerson,和分类TCJATCJB

rwe -- 本类的数据加载

下面通过调试来验证rwe数据0-1的过程,即添加类的方法列表

attachCategories增加自定义逻辑,在extAlloc添加断点运行并断住,从堆栈信息可以看出是从attachCategories方法中auto rwe = cls->data()->extAllocIfNeeded();过来的,这里的作用是开辟rwe

那么为什么要在这里进行rwe的初始化?因为我们现在要做一件事:往本类添加属性、方法、协议等,即对原来的 clean memory要进行处理了
rwe是在分类处理时才会进行处理,即rwe初始化,且有以下几个方法会涉及rwe的初始化 ,分别是:分类 + addMethod + addPro + addProtocol

总结 :所以 情况① -- 0对1是一种一维赋值.

rwe -- TCJA分类数据加载

接着前面的操作,继续执行一步,打印list, p list ,此时的listmethod_list_t结构

接上面,继续往下执行,走到method_list_t *mlist = entry.cat->methodsForMeta(isMeta);p mlist-->p *$12-->p $13.get(0).big() ,此时的mlist是 分类TCJA

if (mcount > 0) {部分加断点,继续往下执行,并断住

往下执行一步,此时的mlists集合的集合

其中mlists + ATTACH_BUFSIZ - mcount内存平移

进入attachLists方法, 在if (hasArray()) {处加断点,继续执行,由于已经有了一个list,所以 会走到 1对多的流程

执行到最后,输出当前的arrayp array()

这个list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>表示 array中会放很多的 method_list_tmethod_list_t中会放很多method_t.

总结:如果本类只有一个分类,则会走到情况③,即1对多的情况.

rwe -- TCJB分类数据加载

如果再加一个分类TCJB,走到第三种情况,即多对多

再次走到attachCategories -- if (mcount > 0) {,进入attachLists,走到 多对多的情况

查看当前 array 的形式 即 p array(),接着继续往下读,p *$25 ,第一个里面存储的TCJB的方法列表

也就是说经过一顿排序之后方法里面,最前面排的是分类TCJB的方法.信不信?不信是吧,我们把所有断点都关掉,来看看输出:

总结
综上所述,attachLists方法主要是将分类的数据加载到rwe

类从Mach-O加载到内存的流程图如下所示

都到这了,那就先顺便讲讲分类的情况吧.

分类的本质

在之前的测试代码的main.m文件中定义TCJPerson分类TCJ

① 通过clangOC代码转化为C++代码

clang指令xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

② 底层分析

cpp文件最下面看起,首先看到分类是存储在MachO文件的__DATA段的__objc_catlist

其次能看到TCJPerson分类的结构

发现TCJPerson改为_CATEGORY_TCJPerson_是被_category_t修饰的,我们看下_category_t是什么样的,搜索_category_

我们发现_category_t是个结构体,里面存在名字(这里的名字是类的名字,不是分类的名字),cls对象方法列表类方法列表协议属性.

为什么分类的方法要将实例方法和类方法分开存呢?

接着我们来看下方法

有三个对象方法和一个类方法,格式为:sel+签名+地址,和method_t结构体一样.

再来看看属性是啥情况

我们发现存在属性的变量名但是没有相应的setget方法,我们可以通过关联对象来设置.(关于如何设置关联对象,下文在说..)

看完cpp文件,在来看看objc4-818.2版本源码中的category_t

分类的加载

通过前面的介绍我们知道了类分为懒加载类非懒加载类,他们的加载时机不一样,那么分类又是如何呢?下面我们就依次来进行探究

准备工作:创建TCJPerson的两个分类:TCJATCJB

在前面的分析中的realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加载,其中分析了分类的data数据是如何加载到中的,且分类的加载顺序是:TCJA -> TCJB的顺序加载到类中,即越晚加进来,越在前面

其中查看methodizeClass的源码实现,可以发现类的数据分类的数据是分开处理的,主要是因为在编译阶段,就已经确定好了方法的归属位置(即实例方法存储在中,类方法存储在元类中),而分类是后面才加进来的

其中分类需要通过attatchToClass添加到类,然后才能在外界进行使用,在此过程,我们已经知道了分类加载三步骤的后面两个步骤,分类的加载主要分为3步:

分类的加载时机

下面我们来探索分类数据的加载时机,以主类TCJPerson + 分类TCJA、TCJB 均实现+load方法为例

通过 ②attachCategories准备分类数据 反推 ①的 加载时机

通过前面的学习,在走到attachCategories方法时,必然会有分类数据的加载,可以通过反推法查看在什么时候调用attachCategories的,通过查找,有两个方法中调用

* 一次在_read_images方法中![](https://img.haomeiwen.com/i2340353/0ffd699280f6ee95.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

所以综上所述,该情况下的分类的数据加载时机的反推路径为:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images

而我们的分类加载正常的流程的路径为:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories

其中正向和反向的流程如下图所示:

我们再来看一种情况:TCJPerson主类+分类TCJA实现+load分类TCJB不实现+load方法
断点定在attachCategories中加自定义逻辑部分,一步步往下执行,p entry.cat-->p *$0

继续往下执行,会再次来到 attachCategories方法中断住,p entry.cat-->p *$2

总结:只要有一个分类是非懒加载分类,那么所有的分类都会被标记位非懒加载分类,意思就是加载一次 已经开辟了rwe,就不会再次懒加载,重新去处理 TCJPerson

分类和类的搭配使用

通过上面的两个例子,我们可以大致将类和分类是否实现+load的情况分为4种.

分类 分类
分类实现+load 分类未实现+load
类实现+load 非懒加载类+非懒加载分类 非懒加载类+懒加载分类
类未实现+load 懒加载类+非懒加载分类 懒加载类+懒加载分类
非懒加载类 与 非懒加载分类

主类实现了+load方法分类同样实现了+load方法,在前文分类的加载时机时,我们已经分析过这种情况,所以可以直接得出结论,这种情况下

其调用路径为:

非懒加载类 与 懒加载分类

主类实现了+load方法,分类未实现+load方法

从上面的打印输出可以看出,方法的顺序是 TCJB—>TCJA->TCJPerson类,此时分类已经 加载进来了,但是还没有排序,说明在没有进行非懒加载时,通过cls->data读取Mach-O数据时,数据就已经编译进来了,不需要运行时添加进去.

* 走到`mlist->setFixedUp();`,在读取`mlist`![](https://img.haomeiwen.com/i2340353/fc38d8c09fef1304.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

通过打印发现,仅对同名方法进行了排序,而分类中的其他方法是不需要排序的,其中imp地址是有序的(从小到大) -- fixupMethodList中的排序只针对 name 地址进行排序

总结:非懒加载类懒加载分类的数据加载,有如下结论:

懒加载类 与 懒加载分类

主类和分类均未实现+load方法

其中realizeClassMaybeSwiftMaybeRelock是消息流程中慢速查找中有的函数,即在第一次调用消息时才有的函数

此时的baseMethodListcount还是16,说明也是从data中读取出来的,所以不需要经过一层缓慢的load_images加载进来

总结:懒加载类懒加载分类数据加载是在消息第一次调用时加载,data数据在编译期就完成了

懒加载类 与 非懒加载分类

主类未实现+load方法,分类实现了+load方法

其中baseMethodListcount8个,打印看看:对象方法3个+属性的set和get方法共4个+1个cxx方法 ,即 现在只有主类的数据.

总结:懒加载类 + 非懒加载分类的数据加载,只要分类实现了load,会迫使主类提前加载,即 主类强行转换为非懒加载类样式

分类和类的搭配使用总结

类和分类搭配使用,其数据的加载时机总结如下:

四、load_images

load_images方法的主要作用是加载镜像文件,其中最重要的有两个方法:prepare_load_methods(加载) 和 call_load_methods(调用)

① load_images 源码实现

② prepare_load_methods 源码实现

②.1 schedule_class_load方法

这个方法主要是根据类的继承链递归调用获取load,直到cls不存在才结束递归,目的是为了确保父类的load优先加载

②.1.1 add_class_to_loadable_list 方法

此方法主要是将load方法cls类名一起加到loadable_classes表中

②.1.2 getLoadMethod 方法

此方法主要是获取方法的sel为load的方法

②.2 add_category_to_loadable_list

主要是获取所有的非懒加载分类中的load方法,将分类名+load方法加入表loadable_categories

③ call_load_methods

此方法主要有3部分操作

③.1 call_class_loads

主要是加载类的load方法

其中load方法有两个隐藏参数,第一个为idself,第二个为sel,即cmd

③.2 call_category_loads

主要是加载一次分类的load方法

综上所述,load_images方法整体调用过程及原理图示如下

五、unmap_image

六、initalize分析

关于initalize苹果文档是这么描述的

Initializes the class before it receives its first message.
在这个类接收第一条消息之前调用.

然后我们在objc4-818.2源码中lookUpImpOrForward找到了它的踪迹

lookUpImpOrForward->realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass

initializeNonMetaClass递归调用父类initialize,然后调用callInitialize

callInitialize是一个普通的消息发送

关于initalize的结论:

写在后面

和谐学习,不急不躁.我还是我,颜色不一样的烟火.
最后附录一张环境变量汇总表


上一篇下一篇

猜你喜欢

热点阅读