iOS 底层原理探索 之 isa - 类的底层原理结构(上)

1,295 阅读7分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现

以上内容的总结专栏


细枝末节整理


前言

在之前的 iOS 底层原理探索 之 对象的本质 & isa的底层实现 文章中,我们探索了对象的本质,对象在内存中的存储结构,以及 isa 的底层实现。今天,我们就接着从头一个对象的 isa 的指向来看看类的底层原理结构是什么。

  • 对象的isa

首先我们在调试台,从一个对象的isa开始:

image.png

入上图所示,我们先拿到p1的内存结构,第一个地址存储的是它的isa,与上掩码之后,我们拿到了它的类指针的值。 其实,一直都很好奇 这个类指针的内存结构到底是什么样子的,那么我们就接着查看下它的内存信息,发现,p1的类也有自己的内存空间布局, 当我们拿着p1的类的 isa 再一次与上 掩码的时候,po下结果发现,也是p1的类 SMPerson。

p1的 类指针式 0x0000000100008360

p1 的 类指针的类指针式 0x0000000100008338

我们通过打印 SMPerson.class 的指针地址,可以确定 是 0x0000000100008360 ,那么 0x0000000100008338 这个地址存储的是什么呢? 会不会是NSObject呢?

p/x NSObject.class
(Class) $7 = 0x000000010036a140 NSObject

打印地址后发现并不是。 那么,我们暂且说他是一个新的东西吧。

接着我们分析下 我们的对象有一个isa 指向了 它的类, 类也有一个 isa 指向了 元类。 接着我们用烂苹果分析下边以后的可执行文件的内容:

image.png

可以看到除了有 _OBJC_CLASS_&_SMPerson 之外 还有一个 _OBJC_METACLASS_&_SMPerson,然而,在编程的过程中我们并没有去写有关的代码,这说明系统或者编译器帮我生成好了一个额外的 MATEClass。 既然发现了元类,那么,我们有必要再看看元类的isa指向类哪里。

控制台分析如下, isa 的走位指向: image.png

有些读者看的不是太直观,那么我们画个图更加直观一点:

未命名文件 (2).png

  • isa总结

  1. 对象的isa指向它的类;
  2. 类的isa指向元类;
  3. 元类的isa指向根元类;
  4. 根元类的isa指向它自己。

总结了 isa 的指向, 我们知道,类是有继承关系的,那么,类的继承关系又是如何的一个走向,我们继续探索类的继承关系的走向。

void smTestNSObject(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);
    
    // SMPerson元类
    Class pMetaClass = object_getClass(SMPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
    
    // SMTeacher -> SMPerson -> NSObject
    // 元类也有一条继承链
    Class tMetaClass = object_getClass(SMTeacher.class);
    Class tsuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"%@ - %p",tsuperClass,tsuperClass);
    
    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);
    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);

}

打印输出如下:

image.png

用一张图片来总结下调试台打印的信息就是:

未命名文件 (3).png

  • 类的继承关系总结

  1. 子类继承自父类
  2. 父类继承自NSObject
  3. 子元类继承自它的父元类
  4. 父元类继承自根元类
  5. 根元类继承自NSObject类
  6. NSObject类类继承自 nil

最后,整合下 类的继承关系isa 指向的 关系,就是我们非常熟悉苹果给我们总结的的下图:

isa流程图.png

类的底层原理结构

准备

内存偏移

    1. 普通指针
   int a = 10;
   int b = 10;
   NSLog(@"%d -- %p",a,&a);
   NSLog(@"%d -- %p",b,&b);
   

10 -- 0x7ffeefbff3f4
10 -- 0x7ffeefbff3f8

未命名文件 (8).png

    1. 对象指针
   SMPerson *p1 = [SMPerson alloc];
   SMPerson *p2 = [SMPerson alloc];
   NSLog(@"%@ -- %p",p1,&p1);
   NSLog(@"%@ -- %p",p2,&p2);
   
   
<SMPerson: 0x101442430> -- 0x7ffeefbff3d8
<SMPerson: 0x10144ccc0> -- 0x7ffeefbff3e0

未命名文件 (9).png

    1. 数组指针
    int c[4] = {1,2,3,4};
    int *d   = c;
    NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
    NSLog(@"%p - %p - %p",d,d+1,d+2);
    
    
0x7ffeefbff400 - 0x7ffeefbff400 - 0x7ffeefbff404
0x7ffeefbff400 - 0x7ffeefbff404 - 0x7ffeefbff408

我们可以发现,在数组中,数组的首地址存储的就是数组中的第零个元素的地址,接着是第一个元素的地址,元素之间的地址相差4字节,也就是元素(int)类型的大小。 如果我们将数组赋值给指针d,那么,d就是c的地址指针, d+1 就是按照现有的数据结构,向后平移一个地址。 也就是内存是可以平移,平移以后 取地址指针里面的值。

未命名文件 (10).png

类的内存结构

我们都知道,对象在底层的内存结构中,在首地址存储的是类的isa,接下来是对应的属性,成员变量的值。那么,对于类而言,在内存中的存储结构优势什么样子的呢?

我们通过对于objc底层源码的分析可以知道,类在底层中的数据结构如下:

image.png

  • Class ISA,
  • Class superclass,
  • cache_t cache ,
  • class_data_bits_t bits

探索到这里,我们整理下类的信息:

myclass.001.jpeg

今天我们重点探索下 类中的 bits 的数据结构。

bits

今天我们主要探究下 这个 class_data_bits_t 类型的 bits

那么,根据我们对于内存平移的总结,我们只需要拿到类的首地址,以及知道 cache_t 所占用的内存大小,那么加上 isasuperclass 的 8 + 8 字节地址,就可以取到 bits 的内存中的值。 那么,我们看下这个cache_t结构中都是包含什么。

cache_t 的底层实现如下(已将函数和static全局区的内容去掉):

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
    };

可以看到 是有 uintptr_t 8字节 和 联合体中 8 字节 大小组成。也就是总共 16字节。

所以,我们在类的数据结构中,要通过内存平移的方式来获取到bits的值的话,需要平移 8 + 8 + 16 = 32字节的长度。

image.png

这样,我们就拿到了,类中 bits 的地址。

bits 的结构内容是:

struct class_data_bits_t {
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}

class_rw_t的结构如下:

image.png image.png image.png

整理下bits的结构 bits结构.001.jpeg

接下来我们就探索下,类的底层存储的信息,

image.png

properties

image.png

methods

在我们找到methods之后,我们找到了method_array_t,继续我们找到了method_t结构体,内容如下:

image.png

这样,我们需要的方法的信息都在 big 这个结构体中, 那么,接着找下去,发现 big 方法会返回 我们想要的这个结构体的信息。所以在获取方法信息的时候,我们需要 使用 这个 big

image.png

image.png

image.png

在 属性 和 方法 数据中,我们并没有发现 类方法 和 成员变量并没有找到,那么这两部分存储到哪里了呢?

ivars

我们继续探索类的底层原理结构,发现在 结构体 class_ro_t ro 中。

image.png image.png

猜想

类方法

那么我们并没有发现类方法,因为对象方法都是存储在类中的,那么类方法是不是都存储在元类中呢?接下来我们验证下我们的猜想:

image.png

果然在元类的信息中,我们找到了类方法。

扩展

protocol

protocol也是存储在 bits 中的,所以我们来探究下 protocol。 首先,我们定义了一个 代理 来让 SMPerson 实现。 image.png

接着,我们在 class_rw_t 中找到了 protocolsprotocols是一个 protocol_array_t 类型 ,通过 protocol_array_t 我们有找到了 protocol_ref_t

image.png

最终 protocol_ref_t 是一个 uintptr_t 。 接下来重点来了, 通过后面的注视 我猜想,是需要将 protocol_ref_t 强转成 protocol_t *, 那么,我们接下来验证下这个想法:

然后我查看类的protocol信息

image.png