[iOS]OC底层探索——对象原理之ISA

226 阅读5分钟

前言

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

一个最基础的NSObject对象大小为8字节,而这个8字节就是我们时常听闻的ISA,本文将记录探索ISA究竟是什么的过程。

获取main.cpp

OC代码相对信息不全,且有很多代码并不好找,将main.m文件编译获得main.cpp文件可以帮助我们获得更多的信息进行分析。

具体操作即到main.m目录下使用如下指令。

20220218-111913.png

main.cpp分析 类对象的本质

示例代码,定义一对有继承关系的类

@interface Father : NSObject
@property (nonatomic, assign) int age;
@end

@interface Son : Father
@property (nonatomic, strong) NSString *sonName;
@end

获得arm64架构下对应的cpp文件,由于文件过大,我们直接进行关键代码分析:

  1. 找到FatherSon的定义,发现cpp文件里是用struct对类进行定义,其中NSObject结构体内的唯一成员就是我们关心的Class isa
struct NSObject_IMPL {
    Class isa;
};
// Father继承了NSObject
struct Father_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
};
// Son继承了Father
struct Son_IMPL {
    struct Father_IMPL Father_IVARS;
    NSString *_sonName;
};
  1. 找到Class的定义,发现就是objc_class *类型的别名,即ISA本质上就是一个结构体objc_class的指针。而结构体objc_class类型在cpp文件中仅有声明,并没有具体定义,其定义需要从objc源码中去进一步探究。
typedef struct objc_class *Class;

其实我们多观察cpp文件能看到不少我们之前一知半解的地方,比如id类型之所以能代表通用类型且不用加*号,就是因为id就是objc_object *的别名。

typedef struct objc_object *id;

又或者说我们之所以能通过变量名字前加_访问属性,是因为属性实质上就是OC类对应结构体内的成员变量,其变量名默认_+属性名,感兴趣的话还可以找一下属性对应的getter setter方法也在cpp文件中有对应的定义。

结论

OC中类的本质是结构体,因为OC中所有类都继承自NSObject,而NSObject的结构体定义中有且仅有成员Class isa;(Classobjc_class *),所以我们通常说isa是每一个对象用来标识自身类型的。

objc源码分析 objc_class是什么

  1. objc源码中找到objc_class定义,我们看到它继承自objc_object并且有三个成员变量。
struct objc_class : objc_object {
    ...
    // Class ISA;
    Class superclass; // 指向父类的isa对象
    cache_t cache; // formerly cache pointer and vtable
    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
    ...
}
  1. 找到class_object定义,发现其只有一个成员变量isa_t isa;,这里的isa和上文中的Class isa;的关系会在后面进一步探究。
struct objc_object {
private:
    isa_t isa;
public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA(bool authenticated = false);

    // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
    Class rawISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ...
};

结论

上文说OC类的本质是一个结构体,其内部一定有一个Class isa,而通过objc源码,我们对OC类又有了新的理解:OC中所有的类最终即objc_object,而OC又定义了其的子类型objc_class来存储父类,方法缓存等信息以更好的使用OC类。

isa_t

上文中我们最后发现objc_object中存在唯一成员变量isa_t isa,它与NSObject中的唯一成员变量Class isa同名但不同类型,那么他们究竟是和关系呢?

在源码中查看isa_t的定义,发现它是一个位域union,有两个成员变量为uintptr_t bitsClass cls,而我们知道Class就是一个指针,所以这两个成员变量大小相同,本质上就是同一段内存。

对于位域联合体不了解的建议自行补习一下。

同时,之前的问题也有了答案,isa_t是包括Class类型的联合体,两者本质上是同一段内存

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    
    uintptr_t bits;
private:
    // !! 重点 !!
    Class cls;
public:
// 位域定义
#if defined(ISA_BITFIELD)  
    struct {
        // !! 重点 后面会分析 !!
        ISA_BITFIELD;  // defined in isa.h
    };
    ...
#endif
...
};

那么新的问题出来了,为什么OC要定义联合体isa_t来兼容Class呢,uintptr_t bits又是干什么用的呢?

这就涉及一个概念nonPointerIsa,而bits就可以理解为nonPointerIsa

nonPointerIsa

nonPointerIsa即非纯指针isa,与之对应的Class isa就是纯指针。具体点解释,nonPointerIsa表示该isa变量内存除了存储指针内容外,还存储了其他信息,指针信息只占用其一部分内存来表示。

nonPointerIsa其实我们并不陌生,回顾之前遇到它是在探究alloc的时候,代码如下:

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ...
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    ...

    id obj;
    ...


    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor); // objc_object::initIsa(cls, true, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls); // objc_object::initIsa(clas, false, false)
    }
    ...
}

inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor);

现在再看这段代码,我们能发现在初始化ISA的时候,fast变量即标识类的ISA类型是nonpointer非纯指针类型的。我们具体看下面initIsa内容会发现,对于纯指针类型的初始化,我们将cls简单复制即可,针对nonpointer我们还会进行一些其他的赋值操作。

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{     
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } 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);
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

接下来我们具体看nonPointerIsa中除了指针还存储了什么信息,这部分内容其实就定义在了isa_t定义中的位域信息中:

# if __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;  // 是否有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)

ISA_BITFIELD即之前isa_t定义中用到的位域信息,其中各位的存储信息都通过注释进行解释了。

通过ISA推导Class类型

接下来我们本地调试验证一下ISA中存储的信息是否如我们探究的。

image.png 如图所示,我们将ISA & ISA_MASK后即Father的类指针,能通过输出识别。