iOS 底层 :isa 与类关联的原理

2021-06-17  本文已影响0人  木槿WEIXIAO

本文的目的主要是理解类与 isa 是如何关联的
在介绍正文之前,首先要理解一个概念:oc 对象的本质是什么

OC 对象的本质

在探索本质之前,先了解一个编译器 clang

Clang

探索对象本质

@interface LGPerson : NSObject
@property(nonatomic,copy)NSString * name;
@end

@implementation LGPerson
@end
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};
struct NSObject_IMPL {
    Class isa;
};

通过以上分析,理解了 oc 对象的本质,但是有一个疑问,为什么 isa 的类型是 Class???

inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
    if (nonpointer) {
        return classForIndex(indexcls);
    }
    return (Class)cls;
#else
    return getClass(authenticated);
#endif
}

inline Class
objc_object::ISA(bool authenticated)
{
    ASSERT(!isTaggedPointer());
    return isa.getDecodedClass(authenticated);
}
image.png

总结

objc_setProperty 源码探索

除了 LGPerson 的底层定义,我们还发现了 name 的 set 和 get 方法定义,其中 set 方法依赖objc_setProperty


image.png

下面就来探索 objc_setProperty 源码

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

总结

通过 objc_setProperty 源码探索,有几下几点说明

cls 与类的关联原理

探索出发点就是initInstanceIsa函数, 探究 isa 与类是如何关联到一起的
在这之前需要了解一个联合体, 为什么 isa 的类型 isa_t 是联合体类型

联合体union

构造数据类型的方式有两种

结构体

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

联合体

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

两者的区别

isa 的类型 isa_t

以下是 isa 指针的类型 isa_t 的定义,从定义可以看出是通过联合体(union)定义的

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

isa类型使用联合体的原因也是基于内存优化考虑的,这里的内存优化是指在 isa 指针中通过char+位域(即二进制中的每一位都可以代表不同的信息)的原理实现,通常来说 isa 指针占用的内存大小是 8 字节,也就是 64 位,足够存储很多信息了,这样可以极高的节省内存
从 isa 的定义中可以看出

#   else
#     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;  //是否对 isa 指针开启了指针优化
        uintptr_t has_assoc         : 1; //是否有关联对象
        uintptr_t has_cxx_dtor      : 1;  //是否有 c++实现
        uintptr_t shiftcls          : 33; //存储类信息
        uintptr_t magic             : 6; //调试器判断对象是真对象还是未初始化空间
        uintptr_t weakly_referenced : 1;  //对象是否被指向或者曾经指向一个ARC 弱变量
        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

原理探索

objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

验证 isa 指针 位域(0-64)

根据前面提到的位域信息,可以在这里验证一下位域是真的存在的,在newisa.bits 处打一个断点


image.png

在执行到这句代码是,通过 lldb 打印p newisa, 然后走到下一行在打印一次 newisa,得到的信息如下图


image.png
通过与前一个newisa 相比,后一个的nonpointer变成了 1, magic变成了 59,

isa 与类的关联

cls 与 isa 关联的原理,就是 isa 中的 shiftcls 位域存储了类信息,其中initInstanceIsa的过程是将calloc 指针与当前类关联起来,有以下几种验证方式

1.通过 initIsa

与bits 赋值结果对比,bits 位域中有两处变化

为什么在shiftcls 赋值时需要强转

因为内存存储时,不能存储字符串,机器码只能识别 0 和 1 这两种数字,所以需要将其转换为uintptr_t数据,这样 shiftcls 中的数据才能被机器识别,其中uintptr_t为 long

为什么需要右移 3 位

因为 shiftcls 处于 isa 中间部分,前面还有 3 个位域,为了不影响前面 3 个位域,需要右移将其抹零

方式 2:通过 isa & ISA_MASK

arm64 中 ISA_MASK 为0x0000000ffffffff8ULL
x86 中 ISA_MASK 为0x00007ffffffffff8ULL


image.png

方式 3 通过 runtime中的函数 object_getClass

方式 4:通过位运算

上一篇下一篇

猜你喜欢

热点阅读