iOS进阶专项分析(三)、isa刨根问底

话不多说,直接开干

一、从源码的角度了解isa及isa底层代码实现


我们都知道Objective-C是一门面向对象的语言,所有的类都继承自NSObject, 既然这样,我们就从NSObject下手。

进入runtime源码, 搜索OC的NSObject {, 找到NSObject的实现部分,

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

我们在NSObject的定义部分找到了isa,并且这个isa是一个Class类型的,
继续点击Class

typedef struct objc_class *Class;

发现Class其实是一个结构体objc_class的指针,打开这个结构体我们可以发现

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; //isa指针
#if !__OBJC2__
    Class _Nullable super_class  OBJC2_UNAVAILABLE;//父类
    const char * _Nonnull name    OBJC2_UNAVAILABLE;//类名
    long version                  OBJC2_UNAVAILABLE;//类的版本信息,默认0
    long info                    OBJC2_UNAVAILABLE;//类的信息,供运行期使用的一些位标识
    long instance_size            OBJC2_UNAVAILABLE;//该类的实例变量大小
    struct objc_ivar_list * _Nullable ivars  OBJC2_UNAVAILABLE;//该类的成员变量的链表         
    struct objc_method_list * _Nullable * _Nullable methodLists  OBJC2_UNAVAILABLE; //方法定义的链表
    struct objc_cache * _Nonnull cache                      OBJC2_UNAVAILABLE; //方法缓存
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE; //协议链表
#endif
} OBJC2_UNAVAILABLE;

点击这个结构体,发现objc_class继承自objc_object

struct 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

    class_rw_t *data() { 
        return bits.data();
    }
    
    ...
    
}

继续深入objc_object,找到了isa_t 类型的isa

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ...
};

继续点击ISA()进入,我们来看一下isa的初始化

inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

重点来了, 摘掉优化部分代码,isa的初始化代码就变成下面这样子

inline Class 
objc_object::ISA() 
{
   return (Class)(isa.bits & ISA_MASK);
}

注意return这段代码,isa在初始化的时候会进行一个&运算,为什么要进行这样的一个运算呢?

其实从64bit开始,isa需要进行一次位运算,才能计算出真实地址,这个位运算的对象就是ISA_MASK

ISA_MASK的值不是唯一的,在arm64和X86架构下是不同的,我把源码中ISA_MASK的部分代码单独剪了出来

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL                                                   # else
#   
# endif

取到一个类的isa,然后和ISA_MASK进行一次位运算,就能够得到类对象的真实地址值。这个下面会做验证。

那么源码分析到此为止,从源码分析,我们知道了isa其实就是一个指向类的结构体指针,也知道了isa是一个经过优化的指针。既然isa是指向类的指针,因为Objective—C的类有很多种,有的类之间也存在继承关系,那么isa在类中间的指向到底如何呢?那么下面我们开始验证isa的指向。

二、控制变量法验证isa的指向

还是新建工程,创建一个继承自NSObjectBMPerson类,并给BMPerson类添加三个属性,然后再创建一个BMStudent类继承自BMPerson,然后去ViewDidLoad中创建对象并给属性赋值,最后打印一下

BMPerson * person = [[BMPerson alloc] init];
person.name = @"张三";
person.age = 18;
person.height = 180;
    
Class class1 = person.class;
Class class2 = [BMPerson class];
Class class3 = object_getClass(person);
    
Class class4 = object_getClass(class1);

NSLog(@"\n实例对象地址:%p\n类地址:class1:%p\n类地址:class2:%p\n类地址:class3:%p\n元类地址:class4:%p", person,class1, class2, class3, class4);


BMStudent * student = [[BMStudent alloc] init];
student.name = @"张三的学生";
student.age = 18;
student.height = 180;
    
Class class5 = student.class;
Class class6 = object_getClass(class5);
NSLog(@"\nBMStudent\n实例对象地址:%p\n类地址:class1:%p\n元类地址:class4:%p", student,class5,class6);


得到结果

2020-06-19 16:55:42.996185+0800 TestProject[21084:1722006] 
BMPerson
实例对象地址:0x600002bdd1a0
类地址:class1:0x1059b8798
类地址:class2:0x1059b8798
类地址:class3:0x1059b8798
元类地址:class4:0x1059b8770


2020-06-19 16:55:42.996416+0800 TestProject[21084:1722006] 
BMStudent
实例对象地址:0x600002b943c0
类地址:class1:0x1059b86f8
元类地址:class4:0x1059b86d0

先来看上面这块log,分析BMPerson这块:
此时BMPerson实例对象的地址是0x600002bdd1a0,class1、class2、class3指向的是同一个地址0x1059b8798,这也说明了类的地址只有一个,也就是说:类对象只有一个,而class4则不同,指向了0x1059b8770元类地址,为什么这么说呢,下面就是验证结果:

1、验证实例对象的isa指向的是类的地址

先拿到BMPerson实例的地址0x600002bdd1a0,然后查看内存

(lldb) x 0x600002bdd1a0
0x600002bdd1a0: 98 87 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00  ................
0x600002bdd1b0: 00 00 00 00 00 00 00 00 18 60 9b 05 01 00 00 00  .........`......

我们知道前八位是isa,小端模式从后往前读八位得到 0x1059B8798,发现这个地址和上面打印的类的地址完全一致!如果感觉地址对比你不够清晰,可以直接po一下,发现这个isa就是指向了 BMPerson类,所以实例对象的isa指向的就是类结论成立。

(lldb) x 0x600002bdd1a0
0x600002bdd1a0: 98 87 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00  ................
0x600002bdd1b0: 00 00 00 00 00 00 00 00 18 60 9b 05 01 00 00 00  .........`......
(lldb) po 0x01059B8798
BMPerson

上面有说,isa是一个经过优化的指针,我们把打印的类的地址和isa_mask进行 &操作,对比一下实际内存空间,发现是完全一致的

BMPerson的实例对象person的内存空间

(lldb) x 0x600002bdd1a0
0x600002bdd1a0: 98 87 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00  ................
0x600002bdd1b0: 00 00 00 00 00 00 00 00 18 60 9b 05 01 00 00 00  .........`......

(lldb) p/x (long)person->isa & 0x00007ffffffffff8ULL
(unsigned long long) $16 = 0x00000001059b8798

这就验证了:

一个类对象的isa,然后和ISA_MASK进行一次位运算,就能够得这个类对象的真实地址值。

2、验证类对象的isa指向的是元类的地址

接下来我们读一下类地址的内存地址0x01059B8798

(lldb) x 0x01059B8798
0x1059b8798: 70 87 9b 05 01 00 00 00 00 ed c1 89 ff 7f 00 00  p...............
0x1059b87a8: 80 f9 5f 00 00 60 00 00 07 00 00 00 2c 80 02 00  .._..`......,...

把前八位也同样读取出来,得到0x01059B8770这个和我们最开始打印得到的元类地址也是完全一致,所以类对象的isa指向的就是元类的结论也成立。

3、验证元类对象的isa指向的是根元类地址

接下来我们对BMPerson的子类BMStudent也做同样的调试,得到的也是同样的结果,student实例的isa指向的就是BMStudent类的地址,BMStudent类的isa指向的就是BMStudent元类的地址。

(lldb) po student


(lldb) x 0x600002b943c0
0x600002b943c0: f8 86 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00  ................
0x600002b943d0: 00 00 00 00 00 00 00 00 58 60 9b 05 01 00 00 00  ........X`......
(lldb) po 0x01059B86F8
BMStudent

(lldb) x 0x01059B86F8
0x1059b86f8: d0 86 9b 05 01 00 00 00 98 87 9b 05 01 00 00 00  ................
0x1059b8708: 00 0c 5e 00 00 60 00 00 07 00 00 00 2c 80 04 00  ..^..`......,...
(lldb) 

此时重点来了,我们分别得到了BMPerson元类的地址和BMStudent元类的地址,
我们看一下这两个类的元类的isa指向的是什么!

(lldb) x 0x1059b8770
0x1059b8770: d8 ec c1 89 ff 7f 00 00 d8 ec c1 89 ff 7f 00 00  ................
0x1059b8780: 80 6a 5f 00 00 60 00 00 07 00 00 00 35 c0 02 00  .j_..`......5...
(lldb) x 0x1059b86d0
0x1059b86d0: d8 ec c1 89 ff 7f 00 00 70 87 9b 05 01 00 00 00  ........p.......
0x1059b86e0: 80 6b 5f 00 00 60 00 00 07 00 00 00 35 c0 01 00  .k_..`......5...

我们发现了共同点!这两个元类的isa指向的是同一块内存空间,都是0x7FFF89C1ECD8, 我们暂且称这个类为未知类,我们再看这个未知类的内存地址!

(lldb) x 0x7FFF89C1ECD8
0x7fff89c1ecd8: d8 ec c1 89 ff 7f 00 00 00 ed c1 89 ff 7f 00 00  ................
0x7fff89c1ece8: 00 60 7f 01 00 60 00 00 0f 00 00 00 31 c0 09 00  .`...`......1...

打印之后发现,这个未知类的isa竟然指向的是自己!其实这个未知类我们现在有另一种称呼,叫做根元类

再po一下这个地址

(lldb) po 0x7FFF89C1ECD8
NSObject

妈耶,这个根元类的类型竟然是NSObject,重点来了!!!类型打印出来虽然是NSObject,但是这个地址代表的并不是NSObject类本身,而是NSObject元类的地址,我通过NSObject创建对象,然后获取这个对象的类,打印一下地址你就明白了

(lldb) po 0x7FFF89C1ECD8
NSObject


(lldb) po NSLog(@"%p", [[[NSObject alloc] init] class]);
2020-06-19 18:56:17.978190+0800 TextProject[21084:1722006] 0x7fff89c1ed00

上面是获取到的根元类的地址,下面才是NSObject类的地址,这俩地址不相同就能证明,根元类其实就是NSObject这个类的元类!

至此,isa的指向图已经很清楚了,如果我上面分析的这么多,其实思路就是按照这张图来分析的


iOS进阶专项分析(三)、isa刨根问底_第1张图片
isa案例图解.png

三、知识点总结


1、**什么是isa

isa是一个一个经过特殊处理优化过的指针,指向对象的类。

实例对象的isa,指向它的类;
类对象的isa,指向这个类对象对应的元类;
元类对象的isa,指向根元类;
根元类的isa指向的是自己;
根元类是NSObject的元类

2、什么是元类

在Objective-C中,每当我们创建一个类,编译时就会自动创建一个元类(元类由系统进行维护),而我们创建的这个类就是这个元类的对象, 只不过这个元类只有一个实例,也就是说我们的 类对象只有一个!

3、为什么元类这么设计?

因为如果没有元类,则类对象的信息和实例对象的信息都会存储在类中,这样对于查找对
应的信息,会更加复杂且混乱(如类方法和实例方法一样的情况下,需要区分查找对
象的类型)

元类和类的类型也必须一致!如果类型不一样,那类对象的信息和实例对象的信息又怎么去找呢?

4、isa走向图

iOS进阶专项分析(三)、isa刨根问底_第2张图片
isa走位图.png

溪浣双鲤的技术摸爬滚打之路

你可能感兴趣的:(iOS进阶专项分析(三)、isa刨根问底)