OC底层原理之-类的加载过程-下( 类及分类加载)

前言

咱们上篇文章OC底层原理之-类的加载过程-上( _objc_init实现原理)讲了类的加载流程,咱们大体讲了read_image,load_image,unmap_image。上面的文章有些方法咱们没有提到,这篇文章咱们继续讲类的加载。算法

对懒加载类和非懒加载类的加载

realizeClassWithoutSwift

咱们提到若是是非懒加载类,就会调用realizeClassWithoutSwift方法,下面我来探究下realizeClassWithoutSwift方法。看下整个方法,其中2544-2554行代码是本身添加的,为了研究Person类写的辅助方法。 数组

上面咱们说了,只有非懒加载类才会调用realizeClassWithoutSwift进行初始化,因此咱们建立Person类,添加+load方法咱们准备下代码(加方法属性是为了更好的研究类的加载)markdown

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod1;
- (void)instanceMethod2;
- (void)instanceMethod3;
+ (void)sayClassMethod;
@end

@implementation Person
+ (void)load{
    
}
- (void)instanceMethod3{
    NSLog(@"%s",__func__);
}
- (void)instanceMethod1{
    NSLog(@"%s",__func__);
}
- (void)instanceMethod2{
    NSLog(@"%s",__func__);
}
+ (void)sayClassMethod{
    NSLog(@"%s",__func__);
}
@end
复制代码

咱们在2551行打断点,开始运行代码 此时调用realizeClassWithoutSwift传进来的cls为Person类。有时候咱们研究本身建立的类会更清楚,添加一些辅助方法去快速找到咱们须要的类,能够节省很多时间。这是后面研究源码的思路。 断点往下走,第2556行if (cls->isRealized()) return cls;若是类已经类加载过,则直接返回函数

对ro,rw操做

咱们看下251-2575行代码 咱们讲下这个判断都作了些什么 咱们看2561行,这个方法就是读取当前cls的data() 咱们将断点移到2562行,看下ro都包含什么 post

咱们发现ro里有类名,有方法列表,数目是8个,第一个方法名为instanceMethod3。性能

上面的方法就是咱们从组装的macho文件中读到data,按照必定输数据格式转化(强转为class_ro_t *类型),此时的ro和咱们的cls是没有关系的。 继续往下走2563行的判断是判断当前的cls是否为元类。这里不是元类,全部会走下面ui

2571行是申请和开辟zalloc,里面包含rw,此时的rw为空,咱们看下rw都有什么 咱们看值都为空其中ro_or_rw_ext是ro或者rw_ext,ro是干净的内存(clean memory),rw_ext是脏内存(dirty memory)。atom

2572行是将咱们建立的rw设置为咱们的ro,2573行是将class的data从新复制为rw。咱们验证下spa

此时断点在2572行,此时咱们打印cls 此时咱们发现最后的地址是为空的,当咱们将断点移到2578行,咱们在打印 发现最后的地址也为空。咱们上面的2574行代码说了,cls的data从新复制了,为啥还为空?3d

这是由于ro为read only是一块干净的内存地址,那为何会有一块干净的内存和一块脏内存呢?这是由于iOS运行时会致使不断对内存进行增删改查,会对内存的操做比较严重,为了防止对原始数据的修改,因此把原来的干净内存copy一份到rw中,有了rw为何还要rwe(脏内存),这是由于不是全部的类进行动态的插入,删除。当咱们添加一个属性,一个方法会对内存改动很大,会对内存的消耗颇有影响,因此咱们只要对类进行动态处理了,就会生成一个rwe。

下面咱们看下ro的读取: 上面咱们看到ro的读取有两种状况,class_rw_ext_t存在和不存在。

咱们继续往下走,来到重要的方法,以下图所示: 在这里会调用父类,以及元类让他们也进行上面的操做,之因此在此处就将父类,元类处理完毕的缘由就是肯定继承链关系,此时会有递归,当cls不存在时,就返回

继续往下走,来到2604行代码,此时的isMeta是YES,是由于它确实是元类。 cls->setInstancesRequireRawIsa();此方法就是设置isa 在2642行是将继承链跑完了,继续往下走,来到2649行 咱们发新此时的cls是个地址,而不是以前的Person了。这是为啥?这是由于上面metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);方法会取到元类。咱们来验证一下。 咱们看到此时的cls确实是元类。继续往下走: 下图的方法就是系统帮咱们设置Cxx方法,继续走,就来到咱们另外一个重点方法 看到这个方法咱们看注释是跟分类有关,咱们看下methodizeClass()方法

methodizeClass方法

咱们看下methodizeClass方法 一样写了辅助代码,去快速找到咱们想要的类,上面咱们知道realizeClassWithoutSwift会不断的递归循环,并且会将data()从新复制(等于ro),可是咱们没有看到对rw,rwe处理咱们看下这个方法是否是对rw,rwe进行了处理

咱们知道ro里存在methodlist,咱们在进行方法查找的时候是使用了二分查找,中间对sel进行排序

咱们先看下方法列表顺序 先放着无论,咱们继续往下走 此时的list是存在的,因此进入判断内,会走prepareMethodLists。prepareMethodLists会对方法进行排序

咱们看下prepareMethodLists方法 咱们在1239行处加断点,也是为了快速找到Person类,之因此在1238行作如此判断,为了防止元类形成影响

代码往下走,就回来到下面判断 这个就会走fixupMethodList方法 就会走到1215行,在1204-1212行咱们将sel名字处理完毕,sort是外界传的是true,因此此时进入进入1216行,其中1216-1217行代码就是进行排序(是根据SELAddress),方法不重要,咱们运行到最后,咱们从新打印下方法

这个跟咱们上面打印的方法顺序不同,因此prepareMethodLists是对方法进行序列化了。以前咱们在讲方法查找的时候说过,在查找的时候是用的二分法进行查找

回到methodizeClass方法 咱们看到此时的rwe为NULL,也就是rew没有赋值,没有走。这是为何?

咱们先把这个问题放一下,在非懒加载的时候咱们知道realizeClassWithoutSwift调用时机,那么非懒加载是何时调用realizeClassWithoutSwift的呢,咱们在main函数写以下代码,同时将+load方法删除,运行代码: 在realizeClassWithoutSwift方法中打断点,断点过来,咱们打堆栈信息,以下

经过上面咱们知道当向Person第一次发送消息时,就会走realizeClassWithoutSwift。由于类有不少代码,不少方法排序和临时变量,若是都放在main函数前加载,会致使加载时间很长,若是类历来没有被调用,那他不须要提早加载。因此懒加载提升性能。

其实在消息发送的时候有这部分的代码展现

上面三张图就是当类进行alloc时,进行方法查找,若是类没有被加载,就去加载类。这也就是说在建立类对象,以及方法调用的前提就是类已经被加载完成了。

下面用一张图来看下懒加载和非懒加载的流程

补充:分类(category)

咱们在main.m函数写以下代码

@interface Person (C)

@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;

- (void)cate_instanceMethod1;
- (void)cate_instanceMethod3;
- (void)cate_instanceMethod2;
+ (void)cate_sayClassMethod;

@end

@implementation Person (C)

- (void)cate_instanceMethod1{
    NSLog(@"%s",__func__);
}

- (void)cate_instanceMethod3{
    NSLog(@"%s",__func__);
}

- (void)cate_instanceMethod2{
    NSLog(@"%s",__func__);
}

+ (void)cate_sayClassMethod{
    NSLog(@"%s",__func__);
}
@end
复制代码

而后用clang生成.cpp文件,看下分类在.cpp是什么样的 打开main.cpp文件,咱们看到以下图所示 发现Person改成_CATEGORY_Person_是被_category_t修饰的,咱们看下_category_t是什么样的,全局搜一下 咱们发现_category_t是个结构体,里面存在名字,cls,对象方法列表,类方法列表,协议,属性 之因此分类有两个列表是由于分类是没有元分类的,分类的方法是在运行时经过attachToClass插入到class的

这个跟咱们被category_t修饰的结构是同样的,此时的instance_methods被赋值为_CATEGORY_INSTANCE_METHODS_Person_,咱们全局搜一下 看到这个是对象方法,存在3个,咱们看到有方法名,签名,地址,这个和method_t结构体同样。

可是咱们发现咱们的属性在.cpp不存在set和get方法的,咱们看下属性的赋值_PROP_LIST_Person_,搜索一下 咱们发现存在属性可是没set和get方法,因此分类中没有实现属性的set和get属性,须要咱们用runTime进行属性关联

咱们发现分类本质就是一个category_t的形式 下面咱们就分析下分类是如何加载到内存中的

分类的加载

下面咱们建立分类,分类写下以下方法

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod1;
- (void)instanceMethod2;
- (void)instanceMethod3;
+ (void)sayClassMethod;
@end

@interface Person (A)
- (void)instanceMethod1;
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end

@interface Person (B)
- (void)instanceMethod1;
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
复制代码

咱们上面建立了两个Person的分类,分别是A,B同时写了几个方法,其中instanceMethod1是三个共有的,咱们在.m中分别实现+load方法,而后运行代码 咱们在methodizeClass处打上断点。(由于咱们上面知道了,若是类被加载,就必定会走realizeClassWithoutSwift方法,进而会调用methodizeClass方法) 咱们在1468行进行断点,发现会走到1480行,此时的cls为Person,咱们看到unattachedCategories这个方法,它的初始化在runtime_init中进行的。 咱们看下attachToClass方法实现 咱们运行的时候发现代码没有进入1150-1161行,直接出来了,这是为何?缘由就在1154行代码的attachCategories方法。下面咱们具体分析一下

attachCategories方法

咱们先进行方法总览

纵览整个方法,咱们要知道研究的重点在哪,咱们要研究的是分类方法如何加载的,看方法1400行,咱们发现这个方法rwe->methods.attachLists就是方法插入的,这个方法的参数mlists,在1367行,知道研究重点,咱们下面开始研究。

下面在咱们写的确认当前类为person类处打断点,让代码运行进来,果断点 此时咱们发现cats_count,咱们有两个分类,为何此处是1呢?缘由是这个attachCategories是循环进来的,每次只有一个,此时咱们看下mlist。 咱们看到这个mlist已经存在方法,第一个是Person(B)的方法,而有4个方法,咱们打印下entry.cat

还记得咱们在对分类进行补充的时候,在.cpp文件看到的分类的name为Person,而此时是分类的名字B,这就说明在编译赋值的时候默认赋值是Person,而在运行时会改成分类的名字B。

咱们继续往下看1369行,此时的mcount值,是不等于ATTACH_BUFSIZ,那么那就回走1374行,对mlist进行处理,咱们看下怎么处理的。

总共有64个位置,[ATTACH_BUFSIZ - ++mcount]这个方法其中ATTACH_BUFSIZ是64也会是让64减去mcount不断的+1,获得的位置等于list,这就是倒序插入。咱们看到第63位是0x0000000100003360,咱们在上面获取entry.cat的时候它的instanceMethods = 0x0000000100003360。也应证了倒序插入。

咱们看到mlist是method_list_t类型,是个一维数组,将mlist存到mlists,因此mlists是个二维数组

extAllocIfNeeded实现

下面继续走就会来到1400行 咱们看到此时的rwe是有值的,前面咱们说rwe一直没有值,何时赋值的呢? 咱们看方法的1348行 点击方法extAllocIfNeeded,看下实现 为何此时要初始化rwe呢?由于后面咱们要向本类里添加方法、协议,要对原来的clean memory进行处理了。那么何时会初始化rwe呢?咱们搜索extAllocIfNeeded咱们发现有这几种状况将会调用extAllocIfNeeded初始化rwe。1.分类 2.addMethod 3.class_addProtocol 4._class_addProperty(不只限于这几种)

下面咱们看一下extAlloc方法 停在断点处,咱们打印下rwe

在1283行进行了初始化,此时打印什么都没有,由于rwe只是初始化了,并无进行赋值。

继续向下进行,此时会运行到1294行,此时获取到List,咱们打印list 咱们看到此时的list为本类的list,继续往下走就会来到attachLists。

attachLists方法实现

咱们看下attachLists方法,1399行代码prepareMethodLists,上面讲到的是进行方法排序。咱们看下attachLists方法实现 咱们看到attachLists添加方法有三种状况,第一种就是906行,传进来的addedCount为1且list不存在,则让list等于addedLists的第一个元素,此时list是个一维数组,咱们再看else的代码 上面就是将新插入的放在lists的最前面,而将旧值放到后面,之因此这样是由于新加入的价值大于老的,相似于YYCache的LRU算法。这个也说明一个问题分类和本类有相同方法的时候,优先调用分类方法 这是验证结果。

咱们继续最后一种状况就是,就是多个list里面加入多个list 这个和oldList只有一个的状况是一致的,都是将新加入的放到表的最前面。对上面作个图更直观

下面咱们验证一下

验证流程

上面的Person原本进来,进行下一步来到908行,在这打印下 咱们发现只有addedLists只有一个方法,但为何p addedLists[1]有值,缘由是指针是连续的,它的值是地址,但里面可能没东西。p addedLists[0]取的是instanceMethod1方法的指针,因此此时是一维赋值。咱们继续向下进行 咱们看到此时的list是method_list_t结构。咱们果断点继续进行,方法走完,到此原本的方法执行完毕。也就是在建立初始化rwe时就将本类的方法加载完毕,后面就开始进行分类加载

回到attachCategories方法,来到mlist,咱们打印下 此时就是咱们的分类方法,当通过一系列处理后,又会来到 咱们再次进入attachLists方法,在进入attachLists前,咱们打印些东西 咱们看到attachLists传入的mlists + ATTACH_BUFSIZ - mcount就是mlists的最后一位地址

此时进入的是else方法 代码运行带最后,打印 咱们看到list_array_tt含有method_t,而method_t有包含method_list_t,以下图所示

list_array_tt是个二维数组,里面包含不少method_t,而method_t是一维数组,包含method_list_t。

当咱们再加一个分类的话是否是就走最上面,咱们放断点,再次来到Person类,走到attachLists,此时走上面的判断 此时咱们打印一下 说明此时的array()第一个地址存放的是分类B的instanceMethod1方法。 当这个方法执行完的时候,咱们在作相同的打印 这个时候咱们将分类A的方法加入到array()中了。因此attachLists方法将方法加入到array()的最前面。验证了咱们上面说内容。

经过上面的讲解,咱们明白了attachCategories就是准备分类数据,将他插入到本类方法列表里。,上面咱们研究的线:map_images->map_images_nolock->_read_images->realizeClassWithoutSwift->methodizeClass->attachToClass->attachCategories->attachLists(将方法加载到class())。下面咱们看看还有别的地方调用attachCategories没有,咱们全局搜索一下 咱们看到1红框就是attachToClass,而红框二就是load_categories_nolock方法,那么这个方法走不走呢?咱们在房里打印log,运行项目 咱们发现这个方法是执行的,下面咱们来研究下这个方法的执行线。 咱们继续往上找又发现一条线:load_image->loadAllCategories->load_categories_nolock->attachCategories

上篇文章OC底层原理之-类的加载过程-上( _objc_init实现原理)咱们探究过load_image,知道它是读取全部类的load方法,并调用的。上面的探索是咱们将类,分类都实现了+load方法。下面咱们来看下类,分类实现或者不实现+load方法,会有什么样的状况。走不走方法的加载,咱们只须要查看attachCategories方法咱们在attachCategories方法打断点 下面咱们验证:

1.其中一个分类存在+load方法,类存在+load方法

咱们看到只加载实现+load的分类,没有实现的,则不加载

2.分类都存在+load方法,类存在+load方法

加载实现+load的分类,没有实现+load方法的则不加载

3.分类都不实现+load方法,类存在+load方法 咱们发如今realizeClassWithoutSwift打印的时候,方法(包括分类的)已经都加在进去了,并且没有进行排序。继续往下走来到methodizeClass,发现同名方法进行了排序,而非同名的方法未进行排序。咱们在排序方法prepareMethodLists打断点(讲过prepareMethodLists是排序方法),进行打印 咱们看到addedLists是一维数组,后面会调用fixupMethodList方法,来到这个方法 1216行就是经过方法名字的地址进行排序 此时咱们看到同名方法进行了排序,非同名的方法经过imp,从小到大进行排列的。同上上面说明fixupMethodList先处理同名,处理完同名就会根据imp从小到大,可是这是同一类的方法,不一样类的方法不按这个处理

若是主类实现,分类没有实现,那么分类的方法是从data()里拿到的,只处理同名方法。

4.主类没有实现+load方法,分类也没有实现+load方法 咱们发现readClass后调用realizeClassWithoutSwift说明是在一次方法调用的时候去加载方法,咱们此时在readClass打断点

看到ro中方法有16个,说明此时已经存在分类和本类方法了,方法也是经过data()拿到的。

5.主类没有实现+load方法,分类实现+load方法,运行代码: 咱们发现走了attachCategories方法,可是咱们发现没有走_getObjc2NonlazyClassList方法,在readClass中咱们发现count为8 咱们打印下,看下这8个都是什么方法 咱们发现这8个都是Person方法,没有分类方法,分类方法是在load_categories_nolock中调用,也就是咱们说的第二条线上加载的

咱们发现当分类实现了,主类没有实现,会迫使主类成为一个非懒加载类,提早加载。

最后

上面的文章和这篇都是再说类的加载,内容和多,这篇是上篇的补充。写到凌晨2点,总算写的差很少了(拓展内容明天继续写)。最后画了个图来大体说明整个过程吧

拓展

咱们在readClass的时候打断点,对cls进行打印发现bits为0x00000000。 可是咱们发现2561行代码又调用了data()方法,若是认为bit是没有值得,那系统调用data(),就至关于与null.data(),这有什么意义?因此咱们验证下bit究竟有没有值 上面能够看出来bits是存在值的,之因此x/4gx cls打印的最后地址为0x0000,那是由于cls此时的内存还未完善,因此才会是0x00000。这也说明当内存还未完善的时候,是能够经过地址指针进行识别的

那在何时内存上的bit才存在值呢?咱们继续往下走 咱们看这个判断instancesRequireRawIsa,这个条件是初始化isa的必要条件 回到realizeClassWithoutSwift方法,继续往下走 当执行完setInstanceSize值发生了变化,咱们继续往下走 咱们发现当执行完setHasCxxDtor方法后,值再次发生变化

相关文章
相关标签/搜索