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

·  阅读 1027

前言

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

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

realizeClassWithoutSwift

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

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

@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都包含什么

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

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

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

2572行是将我们创建的rw设置为我们的ro,2573行是将class的data重新复制为rw。我们验证下

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

这是因为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方法后,值再次发生变化

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改