iOS底层探索——对象的本质&isa分析

717 阅读10分钟

前言

我们经常说,OC是一个面向对象的编程语言,对象就是我们整个编写代码的过程中,最为频繁接触到的一个东西,那么什么是对象呢?在上一篇文章iOS探索底层-结构体内存对齐中,我提到过一点,对象的本质就是结构体。那么这个结论对不对呢,我们今天就来探索一下。

探索对象的底层

我们都知道Objcet-C是一种在C的基础上加入面向对象特性扩充而成的编程语言,它的底层实际上是C/C++的代码。在C/C++中是没有对象这个概念的,因此OC中的对象在C/C++中一定会转换成一个C/C++中的存在的东西,我们可以通过这个线索来进行探索。

探索前的准备

我们首先来准备一个对象DMPerson,来开始我们的探索历程。然后在我们的main.m中,初始化它 image.png

转换成C/C++代码

通过clang轻量级编译器的代码还原,我们可以将OC的代码,转换成C/C++代码查看OC的代码的底层结构。

拓展:

Clang是一个由Apple主导,以 C++编写、基于LLVM、 发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。

通过下面的命令,我们可以将main.m文件,转换成C/C++的代码

//模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp 
//真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

image.png

打开main.app文件后,可以看到里面是一大串的C/C++的底层代码,在其中搜索我们的DMPerson类,可以看到如下代码 image.png 通过结构体中的成员变量_dmName_dmAge我们可以确认,这个就是我们要找的DMPerson在C/C++中的底层实现,再仔细看下,没错,他就是一个struct也就是结构体。到此,我们就可以得出我们一开始的结论:

结论一:对象在底层的本质就是结构体 在上面的C/C++代码的分析过程中,我们会很明显的发现一个问题 在我们定义的DMPerson类中,只含有dmNamedmAge两个属性。

但是在C/C++的底层代码中,我们的DMPerson_IMPL结构体中,却含有三个成员变量,除了dmNamedmAge之外,还有一个NSObject_IMPL结构体类型的NSOBject_IVARS成员变量。

从他们的名字我们可以猜想,NSObject_IMPL是不是就是OCNSObject的底层实现,而这种结构体内嵌套结构体的方式,就是类似我们OC继承的实现呢。下面我们来继续探索

首先找到NSObject_IMPL这个结构体的实现

image.png 在这里可以看到,NSObject实际上是objc_object,再来寻找下objc_object的实现

image.png

看到这是不是觉得有点似曾相识,打开OC代码中NSObjcet.h我们可以看到如下

image.png 它就是我们要找的NSObject,我们可以得出两个结论:

结论二: NSObject在C/C++的底层中本质是objc_object结构体。

结论三: 而OC中的继承,在底层中是使用结构体嵌套的方式进行实现的。

isa分析

在上面的代码里,我们可以看到NSObject里面只有一个成员变量,那就是Class类型的isa。那么这isa是什么呢,我们就这个问题,继续探索下去,首先我们先看看他的类型Class在底层中的定义。

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    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;

可以看到,Class实际上就是一个objc_class *类型的结构体指针。

拓展

同样我们常用的id类型,也可以在这里找到定义

typedef struct objc_object *id;

它是一个objc_object *类型的结构体指针,因此可以指向任何实例

接下来我们来看看isa,这个东西实际上我们在之前iOS - 探索底层alloc流程 的文章里有接触到过。在alloc流程的最后一步,就是通过initIsa方法将我们申请的内存地址和我们的Class绑定起来。

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);
    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

在这些代码中间,有个非常重要的东西,就是isa_t,我们再来看看它到底是什么

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

联合体和位域

联合体

在上面的代码中,我们可以看到一个之前没有接触过的结构union,我们称之为联合体,那么他到底是什么,有什么特性呢,我们用下面这个例子来说明,首先看一段代码

struct DMStudent1 {
    char        *name;
    int         age;
    double      height ;
};

union DMStudent2 {
    char        *name;
    int         age;
    double      height ;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
  
        struct DMStudent1   student1;
        student1.name = "mantou";
        student1.age  = 15;
        student1.height = 190.1;

        union DMStudent2    student2;
        student2.name = "mantou";
        student2.age  = 15;
        student2.height = 190.1;
    }
    return 0;
}

逐行打印对应的student1student2所存储的内容可以看到如下结果

image.png 每次对student1进行赋值的时候,所赋予的数据全部都会存储下来。而每次对student2进行赋值以后,我们每次可以正确访问的数据,永远是最后一次赋值的数据,其他的数据都可以理解为是脏数据,没有任何意义。因此我们可以这么说

结构体(sturct)中的所有变量是“共存”的

优点:海纳百川,有容乃大。只要你来,我都给你存下来

缺点:内存空间的分配是粗放的,不管你用不用全都给你分配好位置

联合体(union)中每个变量之间是“互斥”的

优点:就是不够“包容”

缺点:使用内存更为精细灵活,也节省了内存空间

位域

说完联合体,我们再来看看另外一个知识点位域。我们可以看到isa_t这个联合体中含有一个结构体,而结构里里面是一个叫做ISA_BITFIELD的成员,他的定义如下

// arm64位架构为例

#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19

在这里,就运用到了位域这个知识,我们来举个🌰

@interface DMPerson : NSObject
@property (nonatomic, assign) BOOL fat;
@property (nonatomic, assign) BOOL rich;
@property (nonatomic, assign) BOOL handsome;
@end
#import "DMPerson.h"

@implementation DMPerson
{
    struct {
        char fat      : 1;        //是否胖
        char rich     : 1;        //是否有钱
        char handsome : 1;        //是否帅
    }myself;
}

- (void)setFat:(BOOL)fat {
    myself.fat = fat;
}

- (void)setRich:(BOOL)rich {
    myself.rich = rich;
}

- (void)setHandsome:(BOOL)handsome {
    myself.handsome = handsome;
}

- (BOOL)fat {
    BOOL ret = myself.fat;
    return ret;
}

- (BOOL)rich {
    BOOL ret = myself.rich;
    return ret;
}

- (BOOL)handsome {
    BOOL ret = myself.handsome;
    return ret;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DMPerson *p = [[DMPerson alloc] init];
        p.fat = YES;
        p.rich = NO;
        p.handsome = YES;
        NSLog(@"fat : %d,rich : %d,handsome : %d",p.fat, p.rich, p.handsome);
    }
    return 0;
}

我们通过断点,来查看我们的 myself 结构体的值

image.png 再来看看结构体的具体内容

image.png 是不是与我们设置的数据一样,这就是所谓的位域,正常来说,一个这样的结构体需要占用3个字节来表示所存储的数据,但是当使用了位域了以后,我们只需要3位也就是1个字节就能够把内容给存储下来。因此位域的作用,也是为了让内存更加优化

NonpointerIsa&TaggedPoint

说完位域,我们继续回过头来看刚刚的代码

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ……
        对位域的一系列赋值
        ……
        newisa.setClass(cls, this);
    }
    isa = newisa;

我们可以看到,当nonpointer0的时候,直接绑定类和地址的对应关系,我们称这种为TaggedPoint。而当nonpointer1的时候,除了保存类的信息以外,还会保存一些额外的特殊信息,我们称之为NonpointerIsa。

在2013年9月,苹果推出了iPhone5s配备了 64 位架构的处理器。64位CPU下,指针所占用的位数为8个字节64位。一个内存地址实际上用不了64位去存储,一般32位即可存储一个20亿的数(2^31=2147483648,另外1位作为符号位)。所以,苹果把isa根据需要进行了区分,苹果提出了TaggedPointerNonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。

TaggedPointer

对于NSDateNSNumber这样的小对象存储的值,绝大多数情况并不会大于20亿这个量级。如果采用指针、堆内存的方式,那势必会造成内存的浪费和性能损耗。苹果采用将value值直接存储在isa_t中的uintptr_t bits上,并且用一些特殊标识来标明此isaTaggedPoint类型的。这样用isa就存储了值,而不需要在堆上分配内存再去存储值。要知道堆内存的分配、释放及访问,要比栈内存慢很多的。

NonpointerIsa

isa其实并不单单是一个指针,以arm64架构为例,实际上只有33位用于存储对象地址。其余位用来存储一些特殊的值。

uintptr_t nonpointer : 1; //标识是否为nonpointer

uintptr_t has_assoc : 1; //是否有关联对象

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

uintptr_t shiftcls : 33; //对象地址

uintptr_t magic : 6; //⽤于调试器判断当前对象是真的对象还是没有初始化的空间

uintptr_t weakly_referenced : 1; //志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放

uintptr_t deallocating : 1; //是否正在释放

uintptr_t has_sidetable_rc : 1; //实际使用过程中,uintptr_t的19位已经足够储存,完全用不到这个特殊位置

uintptr_t extra_rc : 19 //当表示该对象的引⽤计数值,实际上是引⽤计数值减 1

isa还原类信息

通过掩码来还原类信息

在查看ISA_BITFIELD定义的时候,我们还看到了一个有意思的东西,就是ISA_MASK。那么这个ISA_MASK是什么东西呢?我们称之为掩码。我们知道NonpointerIsa中除了含有类信息,还含有一些别的特殊信息,掩码就是为我们屏蔽其他的特殊信息,直接找到类信息的直接方法,你可以理解为下图这样

image.png 同样我们举个🌰

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        DMPerson *p = [DMPerson alloc];
        NSLog(@"%@",p);
    }
    return 0;
}

还是上面的DMPerson这个类,我们对他进行alloc方法的调用,然后打上断点,打印出相信的信息

image.png 我们发现,当我们获取到的isa的值与掩码按位与之后,出来的结果就是我们类的信息。

通过位运算来还原类信息

由于测试机是x86_64的架构,因此我先把x86_64架构的NonpointerIsa结构贴出来

#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8

我们分析一下,我们需要寻找的类信息,在shiftcls这个位置存储,在他的前面有3位是特殊的存储位,而在他后面有6+1+1+1+8=17位特殊存储位。我们想到得到shiftcls位置存储的类信息,那么需要把其他的特殊信息位给剔除掉,这样就很简单了

image.png 用图来说明的话,大概就是下面这样

image.png

总结

我们通过clang将OC代码转换成底层的C/C++的代码,从而确认了:

  1. 对象在底层的本质就是结构体
  2. NSObject在C/C++的底层中本质是objc_object结构体。
  3. OC中的继承,在底层中是使用结构体嵌套的方式进行实现的。

接着我们又从底层代码中,找到了isa的定义,从而引申出了联合体(union)位域的概念;

接着我们了解到了NonpointerIsaTaggedPoint的不同;

最后我们通过两种不同的方式来逆向还原了从isa到类的过程。