likes
comments
collection
share

OC底层(isa、类对象、元类对象篇)

作者站长头像
站长
· 阅读数 38

上回书说到了OC的对象及其alloc和init。验证了OC对象底层是结构体,然后在alloc的方法调用栈的最后一个关键方法creatinstance中,有一个创建isa指针的方法。这篇,我们就先聊一聊isa指针。

为什么万物继承自NSObject?

我们知道,OC中的绝大部分对象,都是继承自NSObject(目前我已知的只有一个 NSProxy 类不继承自NSObject,它跟NSObject一样,是基类)。按住command键,跳进NSObject的头文件,就能看到下面的代码:

@interface NSObject <NSObject> {
    #pragma clang diagnostic push 
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
}

这就是NSObject的声明了,它只有一个isa指针,万物继承自NSObject,就有了自己的isa指针了。那这个isa到底有什么用呢?它究竟是个啥东西?

位域

在说isa之前,我们先了解一下共用体和位域这种技术。

思考这样一个问题,一类有3个bool类型的属性,该如何存储与读取。我们第一时间想到的就是定义成property。然后通过set和get方法完成这个操作。我们加点要求,使用极致的最小空间来实现这个需求,例如,一个字节,能不能搞定这个问题?答案是,当然可以。理论来说,一个bool值只需要一位,3个bool值也只需要3位。咱们直接看代码吧,talk is cheap ,show me the code。

第一种实现位存储的方法,是通过利用掩码的操作来实现,原理如下:

 与运算 & 可以用来取出特定的位,
 需要取第几位,第几位设为1,其他位为0,例,取第二位的值,得出0000 0010,这就是掩码
// 0000 0111  三个属性值分别存储在第一位、第二位、第三位
//&0000 0010  取出第二位
//----------
// 0000 0010  取出的第二位是1

 设置值的时候,如果值为YES,可以直接与掩码直接进行或操作,
 例,设置第二位的值为YES
// 0010 1000  三个属性值分别存储在第二位、第四位、第六位
//|0000 0010  直接与掩码或运算
//----------
// 0010 1010  成功将第二位的值改为1,如果原来值为1,或运算后仍然为1

设置值得时候将掩码按位取反,然后再进行&与操作,则会将,
例,设置第二位的值为NO
// 0010 1010  三个属性值分别存储在第二位、第四位、第六位
//&1111 1101  直接与掩码或运算
//----------
// 0010 1000  成功将第二位的值改为0,如果原来值为0,或运算后仍然为0

例如,Person类有个tall、rich、handsome三个bool值,使用一个字节_tallRichHansome。我们通过自定义set和get方法来实现。handsome设置在第二位,则HandsomeMask为0000 0010。

- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHansome |= HandsomeMask;
    } else {
        _tallRichHansome &= ~HandsomeMask;
    }
}

- (BOOL)isHandsome
{
    return !!(_tallRichHansome & HandsomeMask);
}

第一种方法实现了,不过有很多的弊端,比如扩展新的’属性‘值的时候,要定义新的掩码,定义新的set和get方法,代码增加了,可读性也会变差。此时我们引入C语言中的位域技术,位域技术,就是多个字段公用一块内存,这种方法不用定义掩码,代码和解释如下:

@interface Person
{
    // 位域,变量名:1  表明此变量的名称和占用的位数
    struct {
        //忽略下面的char,它们是共享一块内存的,
        //:1代表就占1位,且从低位开始计算,
        char tall : 1;      //tall 共1位,占用一个字节的第1位
        char rich : 1;      //rich 共1位,占用一个字节的第2位
        char handsome : 1;  //handsome 共1位,占用一个字节的第3位
    } _tallRichHandsome;    //tallRichHandsome的结构体
}
@end

存取值,做出相应的改变,仍以handsome为例。

- (void)setHandsome:(BOOL)handsome
{
    //此时方便了很多,可以直接使用.来进行赋值了
    _tallRichHandsome.handsome = handsome;
}

- (BOOL)isHandsome
{
     //赋值的时候,要进行两次的取反。因为handsome是一位,而返回值要求为BOOL,占用8位
     //如果此时handsome为1,另外7位会自动补为1,变为1111111,
     //如果用一个字节存储255和-1,都是11111111,所以此时打印isHandsome,会是-1
     //两次取反,取反没有符号一说,只要非0就是1,取值就对了
     //如果属性值占用两位,则不会出现上述问题,也就不用两次取反了,感兴趣的可自行验证
    return !!_tallRichHandsome.handsome;
}

最后一种方案,就是共用体。这种方案,结合了第一种方案的位运算的速度优势,与第二种方案点语法的方便。共用体的一个特征跟位域类似,就是所有设置的值公用一块儿内存,也需要使用掩码来进行相应的位运算来进行值的存储也读取。我们来看一下具体的定义:

{
    union { //union(共用体)类型的结构体
        char bits;//里面有个变量bits,将来值存在这个里,带位域功能
        struct {//仅仅为了可读性,没有实际意义。删掉这个struct也可以
            char tall : 1;//仅仅为了可读性,没有实际意义。
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}

存取值的代码也做了相应的改变

- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome.bits |= HandsomeMask;
    } else {
        _tallRichHandsome.bits &= ~HandsomeMask;
    }
}

- (BOOL)isHandsome
{
    return !!(_tallRichHandsome.bits & HandsomeMask);
}

了解了union和位域的技术,接下来就是我们的主角,isa。在armv7、armv7s时代,由于是32位操作系统,苹果并没有对指针进行优化,实例对象的isa是直接指向类对象的。进入到arm64架构时代后isa占用8个字节,共计64位,如果只是单纯存一个指针太浪费,所以,苹果通过共用体技术,充分的让这64位存储了非常多的信息,不用再额外的开辟空间来存储,减少了内存开支,减少了内存的操作。下面我们具体看一下isa的结构。

isa的定义

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct{
        ISA_BITFIELD;  // defined in isa.h  说明在isa.h中
    };
#endif
};

里面有个ISA_BITFIELD,再看这个是怎么定义的:(这个define在不同的架构下是不同的,如果要验证掩码的正确性,要使用跟架构一致的掩码,所有的define都在上一篇objc4-787.2.tar.gz这份源码的isa.h文件中)

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;          //表明这个属性占一位,且从低位开始\
      uintptr_t has_assoc         : 1;          // 同上                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

isa union共用体中各个位的含义

  • nonpointer 指示位,表示是否对isa指针开启指针优化,值为0表示纯isa指针 1表示不止是类对象的地址,isa中还包含了类信息、对象的引用计数等

  • has_assoc: 关联对象标志位,0没有,1存在(KVO的实现原理)

  • has_cxx_dtor :该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象

  • shiftcls: 实际存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针,也是基于此,不同的架构中,对于类指针的读取需要不同的操作,在32位时期,由于,由于未进行isa优化,isa是直接指向类对象的,在64位之后,需要用isa指针与上掩码(ISA_MASK)才能取到指针,这个指针指向类对象。

  • magic :用于调试器判断当前对象是真的对象还是没有初始化的空间

  • weakly_refrenced: 对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象,可以更快的释放

  • dellocating :标志对象是否正在释放对象。ing表示进行时

  • has_sidetable_rc:当对象引用计数大于0时,则需要借用该变量存储进位。weak的实现原理,就是使用sidetables。后续讲到内存管理的时候会对此进行详细的讲解。

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1.例如,如果对象的引用计数为10,那么extra_rc为9,如果引用计数大于10,则需要使用到上面的has_sidetable_rc。

用64位存储了这么多的信息,相当的高效!!我当初知道这个点的时候,叹为观止!苹果真的把这种细节这种设计做到了极致。苹果的工程师们真他娘的都是人才,各个身怀绝技(也可能我作为井底之蛙,没有了解过这个技术)。

好,介绍过isa之后,接下来就该是实例对象、类对象、和元类对象了。

在arm64架构之后,isa通过shiftcls来指向类对象的。类对象也是个对象,也有isa指针,它的isa中的shiftcls是指向元类对象的。元类对象也有isa,它的shiftcls都指向根类的元类对象。 网上有一张非常经典的图来展示了对象和元类对象的关系(这幅图将贯穿大部分关于底层原理的博客内容)来看一下:

实例对象、类对象、元类对象的关系图

OC底层(isa、类对象、元类对象篇)

这个图用语言来描述一下,就是

  • 实例对象的isa指向的类对象,类对象的isa指向元类对象。元类对象的isa均指向基类的元类对象(基类的元类对象的isa指向自己)。需要特别注意,arm64架构后,isa不直接指向,需要先用掩码取出shift_cls指针,这个是实际的指向关系的指针。
  • 类对象和元类对象中有superclass指针,普通类对象的superclass指针指向父类的类对象,基类类对象的superclass指向nil。基础对象的元类对象的superclass指针指向父类的元类对象,基类的superclass指针指向父类的类对象。

写完这些我都不认识这个类字了。谨记这幅图,就能非常好的理解实例对象、类对象、元类对象之间的关系了。这个图画的非常好,在后面的runtime的消息转发流程中还会用到这幅图以及这些对象之间的关系。

我们为什么需要类对象和元类对象

那为啥要有类对象和元类对象呢?类对象和元类对象中都放着什么东西呢?

先说为什么。我们都知道,我们创建的类,有很多的属性、协议和方法,方法又分为实例方法和类方法。而这些方法的实现都是统一的,再调用的过程中,只是参数的值不同,所以这些方法存一份儿就够了,没必要在每个对象中都存一份。所以就有了类对象和元类对象。可以想一下平时写的方法调用的代码,假定Person类有一个实例方法叫-(void)instanceFunction,有个类方法+(void)classFunction。我们在调用的时候是这么调用的:

- (void)callFunctions{
    [person instanceFunction];     //使用实例对象来调用实例方法
    [Person classFunction];        //使用类名(其实就是类对象)调用类方法
}

可以很明显的看出,方法调用者的不同。而这个不同就是由类对象、元类对象、isa来实现的。类对象中,存储了这个类的属性、协议和实例方法。元类对象中存储了这个类的类方法。在方法调用时,实例方法通过这个实例对象的isa找到这个类的对象,然后在类对象中查找这个方法。类方法通过类对象的isa找到这个类的元类对象,在元类对象中查找这个方法。通过这样的描述,结合上面的图,就能很好的理解isa指针、类对象和元类对象了。

好,简单的来总结一下今天的内容。主要是理清实例对象、类对象、元类对象之间的关系,要记住那副图,保证自己能完整的复述出他们之间的关系。isa就相当于纽带,将他们串联了起来。在最后提到了,类对象和元类对象是存储属性、协议和方法的,我们会在runtime中,具体的讲解这些信息是如何存储的。

那这篇文章就暂时收尾了,如果有错误,请指正。

PS:感觉共用体和位域那块跟本文章的结合还不是特别好,后续可能会考虑拆分出去。

转载自:https://juejin.cn/post/7002156875588304927
评论
请登录