iOS-对象的本质,ISA分析

对象的本质是什么?
其实在上篇iOS -内存对齐中已经提过啦,那么已知对象的本质就是结构体,那么我们应该怎么验证这个结论呢?

在探索oc对象本质前,先了解一个编译器:clang

-clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器
-主要是用于底层编译,将一些文件``输出成c++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。

开始操作

1.在main中自定义一个类YPPerson

@interface YPPerson : NSObject
@property (nonatomic, copy) NSString *name;//这个属性方便我们定位相关代码
@end

@implementation YPPerson
@end

2.打开终端,使用clang编译main.m文件,命令:clang -rewrite-objc main.m -o main.cpp
3.在main.cpp中搜索YPPerson,(main.cpp里东西很多,我们只看关键的)

//关键字METACLASS,猜测这是编译器自动生成的元类相关,后续会继续探究,今天就不偏题了。
static struct _class_ro_t _OBJC_METACLASS_RO_$_YPPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1, sizeof(struct _class_t), sizeof(struct _class_t), 
    (unsigned int)0, 
    0, 
    "YPPerson",
    0, 
    0, 
    0, 
    0, 
    0, 
};
//这里才是YPPerson类对象的相关
static struct _class_ro_t _OBJC_CLASS_RO_$_YPPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, __OFFSETOFIVAR__(struct YPPerson, _name), sizeof(struct YPPerson_IMPL), 
    (unsigned int)0, 
    0, 
    "YPPerson",
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_YPPerson,
    0, 
    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_YPPerson,
    0, 
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_YPPerson,
};

//YPPerson的底层编译
struct YPPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};
//NSObject 的底层编译
struct NSObject_IMPL {
    Class isa;
};

通过上述分析,理解了OC对象的本质,但是看到NSObject的定义,会产生一个疑问:为什么isa的类型是Class?(在探究对象alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的)
-其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换,类似于swift中的 as 的强转。源码中isa的强转如下图所示:


image.png

了解了对象的本质,我们再来看看其核心isa的分析吧。

在分析isa之前需要先了解一下结构体联合体以及位域相关知识。

结构体

结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。

缺点:所有属性都分配内存,比较浪费内存,假设有4个int成员,一共分配了16字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费

优点:存储容量较大,包容性强,且成员之间不会相互影响

联合体(共用体)

联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉

缺点:包容性弱

优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间

位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为"位域"或"位段"。
所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

了解了这些基础知识后,我们来看看isa在源码中是怎么定义的吧。
在alloc流程学习中,我们发现isa实际是一个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);
};

提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式

通过cls初始化,bits无默认值

通过bits初始化,cls有默认值

还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示

其中ISA_BITFIELD宏定义为

image.png

当前环境为x86,主要看这块定义

*/
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   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
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif

可以看出在不同架构下,ISA_BITFIELD的定义是不同的,由于现在可编译的源码是运行在mac下的,我们现在主要看x86架构下的定义

#   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

很明显这里用到了前面提到的位域。

接下来我们来仔细看看在一个对象创建过程中isa的初始化流程吧。

找到initIsa方法源码

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

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

这里面涉及到taggedPointer以及nonpointer,

taggedPointer

-Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
-Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
-在内存读取上有着3倍的效率,创建时比以前快106倍。

nonpointer

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

断点发现会走入以下逻辑,并输出了newisa

image.png

当走过setClass后,会发现newisa中已经有了类信息

image.png

让我们来看看setClass都做了些什么吧

image.png

如图当拿到YPPerson的地址后,将其右移三位赋值给了shiftcls,那么为什么要右移三位呢?

在x86架构下

image.png

1、由MACH_VM_MAX_ADDRESS为0x7fffffe00000 知虚拟内存最大寻址空间为47位
2、由于内存对齐的原因,对象内存地址后三位必定为0
基于以上两条,为了节省内存空间,省略后三位的0,shiftcls设计为44位,故需要将类地址右移3位,

参考资料:
1.https://www.jianshu.com/p/7fd6241a7124
2.https://juejin.cn/post/6973582933126823950/#heading-7

你可能感兴趣的:(iOS-对象的本质,ISA分析)