面试宝点

iOS底层-类的探索分析之isa及继承链

2021-06-17  本文已影响0人  似水流年_9ebe

前言

众所周知,OC是基于C/C++扩展而来的,那么也就是说OC也是面向对象的一门语言,即然是面向对象,必然就有类与对象的概念,我们就从以下几个方面分析一下OC的类。

ISA的分析

首先在我们的demo工程中打个断点,调试一下,如图所示:


1

然后我们通过p/x person 命令查下person对象的地址,如图:

2
我们再用x/4gx 0x0000000100707f30 读取这个地址,得到下图:
3
"0x011d800100008365"这个就是我们的对象isa,
我们p/x 0x011d800100008365 & 0x00007ffffffffff8(isa的mask)命令执行一下,得到下图:
4

接着,我们在po 0x0000000100008360 执行这个命令,打印下这个地址。

5

0x0000000100008360 这个就是我们的RoPerson类。

我们再执行一下x/4gx 0x0000000100008360命令,得到下图结果:

6

我们再po 0x0000000100008338 & 0x00007ffffffffff8一下,发现了一个奇怪的现象,如图:

7

也是指向我们的RoPerson类

0x00000001000083600x0000000100008338 都输出了RoPerson类
这也说明了当前的类和我们的对象一样,可以无限开辟,内存中不止有一个类

我们接着往下验证,分析。

//MARK: - 分析类对象内存存在个数
void RoTestClassNum(void){
    Class class1 = [RoPerson class];
    Class class2 = [RoPerson alloc].class;
    Class class3 = object_getClass([RoPerson alloc]);
    Class class4 = [RoPerson alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

我们看下这段代码,看下打印果,如图:


8

这个图可以看出来 0x0000000100008360是我们的类,而0x0000000100008338不是我们的类,那么0x0000000100008338是什么,我们知道对象有一个isa指向了类,类的isa指向了元素 这个元类在我们的代码中是没有存在的,我们用MachoView分析下我们demo的macho文件。

9
在我们的Symbol Table找到了RoPerson的元类,也就是说编译器帮我们生成了元类。
我们知道元类也有isa,那么它的isa又指向哪里,我们接下来分析。

我们先x/4gx 0x0000000100008338这个地址,得到下图:

10
我们再执行一下p/x 0x00007fff806b2360 & 0x00007ffffffffff8 得到下图:
11
我们再po 0x00007fff806b2360,得到的是NSObject,看图:
12

从上图可以看出,我们的元类的isa指向了根元类
接着我们再执行p/x NSObject.class,得到下图

13
再执行x/4gx 0x00007fff806b2388命令,得到下图:
14
继续执行p/x 0x00007fff806b2360 & 0x00007ffffffffff8命令,如图所示:
15

从上图可以看出根元类的isa又指向了自己,形成了一个闭环。

它的基本流程是:

对象的isa->类----isa------>元类----isa------>根元类----isa------>自身
我们可以看下面一张图:

16

继承链

我们先看下段代码

#pragma mark - NSObject 元类链
void RoTestNSObject(void){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // RoPerson元类
    Class pMetaClass = object_getClass(RoPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
}

我们看下打印结果,如图:


17

从上图我们发现,NSobject的元类与根元类是同一个,RoPerson元类的父类是NSObject。
我们在上述代码中再加入以下代码

    // RoTeacher -> RoPerson -> NSObject
    Class tMetaClass = object_getClass(RoTeacher.class);
    Class tsuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"%@ - %p",tsuperClass,tsuperClass);

我们再来看打印结果:


18

从上图看出RoTeacher的元类是RoPerson,RoPerson的元类是NSObject,
也就是说元类也是继承链,下图所示


19

图中可以看出:

  1. 类的继承: Subclass->Superclass->Root class(根类)
  2. 元类的继承:Subclass(meta)->Superclass(meta)->Root class(meta)
  3. Subclass(meta)为Subclass的元类,以此类推。

我们在上述代码再加入以下代码

 // NSObject 根类特殊情况
 Class nsuperClass = class_getSuperclass(NSObject.class);
 NSLog(@"%@ - %p",nsuperClass,nsuperClass);
 // 根元类 -> NSObject
 Class rnsuperClass = class_getSuperclass(metaClass);
 NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);

我们再来看下打印结果:

20
从上图的结果我们可以得出以下结论:
根类没有父类,根元类的父类就是这个类,也就是NSObject,所有的类都来自于NSObject这就是继承链。
我们再来看下苹果官方的isa与继承链的图:
21
这幅图很好说明了isa的指向关系和继承链。

类的结构分析

我们打开objc的源码,搜下struct objc_class,如图:


21

我们从上图可以看到,objc_class继承objc_object

   // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

上述代码是类的结构,ISA为隐藏属性,superclass父类,class_data_bits_t 存放的是methodlist,ivarlist,property,接下来我们来分析和验证一下。

首先我们断点调试下,如图:


22

接着我们在lldb中执行一下x/4gx RoPerson.class,得到下图

23
0x00000001000083a8是isa在ISA分析的时候已经验证过。
0x000000010036a140是superclass,我们执行下 po 0x000000010036a140 验证,如图所示:
24
NSObject即是RoPerson的superclass。
0x0000000100758910是我们的cache,我们这里先不讲,后续推出。
我们来讲下
0x0002802800000003bits
我们要分析bits,就要平移
isa的长度+superclass的长度+cache的长度
isa的长度=8字节(结构体指针)
superclass的长度=8字节(结构体指针)
cache的长度=16字节

我们分析下cache的长度,cache是cache_t结构体,由于在结构体中,只有成员变量占用结构体大小(方法在方法区,不占用结构体内存),所以我们只需要分析以下代码所占用的内存大小
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4字节
#if __LP64__
            uint16_t                   _flags; // 2字节
#endif
            uint16_t                   _occupied; // 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
    };

从以上代码可以看出,cache的长度占用16字节,所以我们要平移8+8+16=32字节才能找到bits。
所以我们可以执行下p/x 0x00000001000083a8+0x20(0x20即32)这个命令,得到下图:

25
我们再执行下p (class_data_bits_t )0x00000001000083c8得到下图:
26
我们来看下$9的内存情况,我们执行下 p $9->data()命令data()是class_data_bits_t的方法,参考源码,得到下图:
27
我们再取$11,执行命令
p $11,得到以下图:
28
接着我们执行
p $3.properties()*,(重新运行项目,序号从0开始,不影响我们的验证),properties()是class_rw_t结构体中的方法,我们可以得到下图:
29
接着我们要取出list,所以执行p $4.list命令,得到下图:
30

接着我们再来执行p $5.ptr命令,得到下图


31

再执行p *$6,得到下图:


32

我们取一下property_list的内容,执行p $7.get(0)命令,如图所示:


33

取到了,我们的name属性。
由于RoPerson中只有一个name属性,我们加一些代码,我把RoPerson的代码贴出来,如下所示:
.h文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RoPerson : NSObject{
    NSString *subject;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

- (void)sayNB;
+ (void)say666;
@end

NS_ASSUME_NONNULL_END

.m文件

#import "RoPerson.h"

@implementation RoPerson

- (instancetype)init{
    if (self = [super init]) {
        self.name = @"robert";
    }
    return self;
}

- (void)sayNB{
    
}
+ (void)say666{
    
}
@end

重新运行我们的项目,然后断点和lldb调试,如图所示:


34

name和hobby属性都取到了,而subject没有取到(我们这里先不提,后续补上),我们再看下methods,执行一系列lldb命令,如图所示


35
36

这里就是方法列表。(这里没找到+ (void)say666这个方法,后续补上)

结语

以上就是个人对iOS类的探索和分析,还有一些不完整的地方,敬请批评指证,大家相互交流学习。

上一篇下一篇

猜你喜欢

热点阅读