OC 底层原理 03:OC对象本质与 isa

一.对象的本质

在探索oc对象本质前,我们需要先了解一下 clang 编译器。 clang是一个由Apple主导编写,基于LLVMC/C++/OC的编译器。主要是用于底层编译,将一些 OC原文件输出c++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。

1.1 使用 clang 生成 cpp 文件

  • main中自定义一个类YJPerson,有一个属性name

1656489636938.jpg

  • 通过终端,利用clangmain.m 编译成 `main.cpp
// 将 main.m 编译成 
main.cpp clang -rewrite-objc main.m -o main.cpp

这样我们就生成了 main.cpp 文件

1.2 main.cpp 解读

  • 打开编译好的main.cpp,找到YJPerson的定义,发现YJPerson在底层会被编译成 struct 结构体
1.2.1 YJPersion
// YJPersion 结构体声明
typedef struct objc_object YJPerson;
typedef struct {} _objc_exc_YJPerson;

// YJPersion 结构体实现
struct YJPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};

代码解读:

  1. 定义了一个别名YJPerson,该别名指向struct objc_object类型
  2. 在结构体实现YJPerson_IMPL中,有一个成员变量NSObject_IVARS,也就是isa;另一个成员变量是_name,也就是YJPerson的属性,和OC层面定义是一致的。
1.2.2 NSObject
// NSObject结构体声明
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;

// NSObject实现-对象
struct NSObject_IMPL {
    Class isa;
};

代码解读:

  1. 定义别名NSObject,同样指向struct objc_object类型
  2. NSObject_IMPL结构体实现中,有一个Class类型的成员变量isa
1.2.3 底层结构关系

Class 的定义:

typedef struct objc_class *Class;

objc_object 定义-根类定义:

struct objc_object {
     Class _Nonnull isa __attribute__ ((deprecated));
};

id 的定义指向 objc_object的指针:

typedef struct objc_object *id;

SEL 方法编号,方法选择器指针

typedef struct objc_selector *SEL;

解读代码:

  1. OC层面NSObject,在底层对应objc_object结构体
  2. 子类的isa均继承自NSObject,也就是来自objc_object结构体;
  3. OCNSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类
  4. isa的类型为Class,被定义为指向objc_class的指针
  5. 在开发中可以用id来表示任意对象,根本原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针
  6. SEL方法选择器指针,方法编号。

结构关系总结: 1656570861929.jpg

1.2.4 get\set方法

YJPerson类的属性,自动添加get\set方法。见下面代码:

static NSString * _I_YJPerson_name(YJPerson * self, SEL _cmd) {
    return (*(NSString **)((char *)self + OBJC_IVAR_$_YJPerson$_name)); 
}

extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long id, bool bool);

static void _I_YJPerson_setName_(YJPerson * self, SEL _cmd, NSString *name) {
    objc_setProperty (self, _cmd, __OFFSETOFIVAR__(structYJPerson, _name), (id)name, 0, 1); 
}

通过以上代码可以发现,无论是get方法还是set方法,都会有两个隐藏参数self_cmd,也就是方法接收者方法编号。在获取属性时,采用指针偏移的方式,获取成员变量所在地址,转换后返回对应的类型。

objc_setProperty:

objc_setProperty,在对实例变量进行设置时,会自动调用objc_setProperty方法。该方法可以理解为set方法的底层适配器,通过统一的封装,实现set方法的统一入口。

runtime源码中,搜索objc_setProperty,可以找到最终实现方法,见下段代码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { 
    if (offset == 0) { 
        object_setClass(self, newValue); 
        return; 
    }
    id oldValue;
    id *slot = (id*) ((char*)self + offset); 
    
    if (copy) { 
        newValue = [newValue copyWithZone:nil]; 
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil]; 
    } else { 
        if (*slot == newValue) return; 
        newValue = objc_retain(newValue); // retain新值 
    } 
    
    if (!atomic) { 
        oldValue = *slot; 
        *slot = newValue;
    } else { 
        spinlock_t& slotlock = PropertyLocks[slot]; 
        slotlock.lock();
        oldValue = *slot; 
        *slot = newValue; 
        slotlock.unlock(); 
    } 
    // 释放旧值
    objc_release(oldValue); 
}

本质是通过指针平移找到成员变量位置,然后进行新值的retain,旧值的release。

1.3 对象本质总结

通过工具clang,编译生成的cpp文件,我们可以发现,对象实质是一个结构体。在OC层,NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类NSObject仅有一个实例变量Class isaClass实质上是指向objc_class的指针objc_class的定义见下面的代码:

struct objc_class : objc_object { 
    // Class ISA; 
    Class superclass; 
    cache_t cache; // formerly cache pointer and vtable 
    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 
    
    class_rw_t *data() const { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData); 
    } 
    …… 省略 
}

`objc_class`继承自`objc_object`,所以万物皆对象!

二. 结构体/联合体/位域

2.1 结构体

结构体(struct)是一种存储结构,特点是所有成员变量是“共存”的,优点是“有容乃⼤”,信息更全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全部分配,不够精细;

2.2 联合体(又称:共用体)

联合体(union)也是一种存储结构,特点是各成员变量共用同一块内存。因为共用同一块内存,所以给其中一个成员变量赋值后,其它成员变量会同步该成员变量的值,如下图: 1656599852463.jpg

赋值 age2,age1 和 age3 也同步了age2 的值 1656600004195.jpg

2.3 位域(又称:位段/位字段)

  • 位域是一种常用于结构体中的存储技术,一般情况下结构体信息的存取以字节为单位,实际上有时存储信息只用一个字节中的几个byte位即可,这种情况下可以使用位域存储技术。

三. isa 探索

3.1 isa_t联合体

通过上面的说明,认识到了联合体与结构体的区别,同时了解到位域在节省内存方面的优势。而isa,就是采用联合体结合位域,对数据进行了封装。见下面源码:

// isa 联合体
union isa_t {
    // 构造方法
    isa_t() { }
    // 带参数构造方法
    isa_t(uintptr_t value) : bits(value) { }

    // bits
    uintptr_t bits;
    // class
    Class cls;

#if defined(ISA_BITFIELD)
    // 位域
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
    ... 无关内容,省略...
#endif
};

isa_t是一个联合体,有三个成员: uintptr_t bits;Class cls;结构体位域:struct { ISA_BITFIELD; };,三个成员共用一份内存,占用8个字节内存空间。

  • Class cls; 非nonpointer isa时,没有对指针进行优化,直接指向类,typedef struct objc_class *Class;
  • uintptr_t bits; nonpointer isa时,使用了结构体位域,针对arm64架构x86架构提供了不同的位域设置规则

ISA_BITFIELD 定义:

#if SUPPORT_PACKED_ISA

# if __arm64__

// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
// arm64 模拟器
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                            
        uintptr_t nonpointer        : 1;                             
        uintptr_t has_assoc         : 1;                             
        uintptr_t weakly_referenced : 1;                             
        uintptr_t shiftcls_and_sig  : 52;                            
        uintptr_t has_sidetable_rc  : 1;                             
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else // arm64 真机
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     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          : 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
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif
# 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解读:

  • nonpointer表示自定义的类,占1

    • 0纯isa指针 
    • 1:不只是类对象地址,isa中包含了类信息、对象的引用计数
  • has_assoc表示关联对象标志位,占1

    • 0没有关联对象
    • 1存在关联对象
  • has_cxx_dtor 表示该对象是否有C++/OC的析构器(类似于dealloc),占1

    • 如果析构函数,则需要做析构逻辑
    • 如果没有,则可以更快的释放对象
  • shiftcls表示存储类的指针的值(类的地址), 即类信息

    • arm64中占 33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针
    • x86_64中占 44
  • magic 用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6

  • weakly_refrenced是 指对象是否被指向 或者 曾经指向一个ARC的弱变量

    • 没有弱引用的对象可以更快释放
  • deallocating 标志对象是是否正在释放内存

  • has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位

  • extra_rc(额外的引用计数) ,表示该对象的引用计数值,实际上是引用计数值减1

    • 如果对象的引用计数为10,那么extra_rc为9(这个仅为举例说明),实际上iPhone 真机上的 extra_rc 是使用 19位来存储引用计数的

3.2 nonpointer isa初始化

在对象进行初始化过程中,_class_createInstanceFromZone中三个重要的初始化流程:

  1. cls->instanceSize,计算要开辟的内存大小,16字节对齐原则
  2. obj = (id)calloc(1, size);,内存空间开辟;
  3. obj->initInstanceIsaisa初始化过程。
  • 重点nonpointer isa的初始化流程!

设置断点,运行程序,过滤出我们所需要研究的GFPerson类的初始化流程。见下图所示: 1656602058135.jpg

ISA_MAGIC_VALUE 的定义(这里用的是电脑,所以是 x86_64): 1656602444475.jpg

继续运行代码,bits赋值ISA_MAGIC_VALUE,赋值后,联合体 newisa 各成员的值见下图:

1656603085602.jpg

联合体共用一块内存,所以 bitscls 值一样,那么结构体位域呢?

1656603576736.jpg

1656603870368.jpg

继续运行代码,走到了 setClass:

1656604013619.jpg

进入 setClass:

1656604137537.jpg setCalls 中将类的地址右移3位 赋值给了 shiftcls,为何要右移三位呢?因为shiftcls前面还有3位存储着nonpointerhas_assochas_cxx_dtor

1656605169273.jpg

到这里 isa初始化流程结束。

3.3 通过person实例反推isa指向

1656605715611.jpg

3.4 获取对象所属类

平常获取对象的类会直接调用class方法,那么class方法内部实现是怎样的?见下面源码:

- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil
}

inline Class
objc_object::getIsa() 
{
    // 当前不是taggedPointer,而是nonpointor isa, 直接返回ISA()
    if (fastpath(!isTaggedPointer())) return ISA(/*authenticated*/true);
    
    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

// ISA——返回:return (Class)(isa.bits & ISA_MASK);
inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

通过解读上面的代码,发现获取对象的类,最终是实现代码是(Class)(isa.bits & ISA_MASK);,是通过对象isa & ISA_MASKISA_MASK是什么呢?见下图:

1656606804450.jpg

在计算器中可以发现,该掩码低3位高17位全部是0,通过对象isa & ISA_MASK运算,会将对象isa低3位高17位全部抹零,等价于上面的右移3位左移20位,再右移17位操作流程。 验证一下:

1656607181880.jpg 两种方式结果 一模模一样样

猜你喜欢

转载自juejin.im/post/7115073503035916296