美文网首页iOS
iOS-对象的本质,ISA分析

iOS-对象的本质,ISA分析

作者: Summit_yp | 来源:发表于2021-06-15 16:47 被阅读0次

    对象的本质是什么?
    其实在上篇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分析

        本文链接:https://www.haomeiwen.com/subject/kqroeltx.html