学习之前,我们先补充下位域和联合体的知识。
所谓位域就是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作——这样就可以把几个不同的对象用一个字节的二进制位域来表示。位域是C语言一种数据结构。
使用位域的好处是:
在C语言中,位域的声明和结构体(struct)类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际储存在一个或多个整型变量中。
在声明时,位域成员必须是整形或枚举类型(通常是无符号类型),且在成员名的后面是一个冒号和一个整数,整数规定了成员所占用的位数。
位域不能是静态类型。不能使用&对位域做取地址运算,因此不存在位域的指针,编译器通常不支持位域的引用(reference)。
// 结构体 struct Struct { // (数据类型 元素); char a; // 1字节 0 补1 2 3 int b; // 4字节 4 5 6 7 } Str; // 位域 struct BitArea { // (数据类型 位域名: 位域长度); char a: 1; int b: 3; } Bit; int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Struct:%lu——BitArea:%lu", sizeof(Str), sizeof(Bit)); } return 0; } 复制代码
联合体(union,又叫共用体):使几种不同类型的变量存放到同一段内存单元中。即使用覆盖技术,几个变量互相覆盖重叠。
union MyValue{ // (数据类型 元素) int x; int y; double z; }; void main(){ union MyValue d1; d1.x = 90; d1.y = 100; d1.z = 23.8; // 最后一次赋值有效 printf("%d,%d,%lf\n",d1.x,d1.y,d1.z); } // 输出结果: -858993459,-858993459,23.800000 复制代码
联合体定义使用时注意点:
在之前的iOS探索alloc流程中,我们提了一句obj->initInstanceIsa(cls, hasCxxDtor)在内部调用initIsa(cls, true, hasCxxDtor)初始化isa,今天就分析下isa。
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { if (fastpath(!cls->ISA()->hasCustomAWZ())) { if (fastpath(cls->canAllocFast())) { // No ctors, raw isa, etc. Go straight to the metal. bool dtor = cls->hasCxxDtor(); id obj = (id)calloc(1, cls->bits.fastInstanceSize()); if (slowpath(!obj)) return callBadAllocHandler(cls); obj->initInstanceIsa(cls, dtor); return obj; } else { // Has ctor or raw isa or something. Use the slower path. id obj = class_createInstance(cls, 0); if (slowpath(!obj)) return callBadAllocHandler(cls); return obj; } } } 复制代码
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { assert(!isTaggedPointer()); if (!nonpointer) { isa.cls = cls; } else { assert(!DisableNonpointerIsa); assert(!cls->instancesRequireRawIsa()); isa_t newisa(0); #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 newisa.has_cxx_dtor = hasCxxDtor; newisa.shiftcls = (uintptr_t)cls >> 3; #endif // 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; } } 复制代码
union isa_t { isa_t() { } // 初始化方法1 isa_t(uintptr_t value) : bits(value) { } // 初始化方法2 Class cls; // 成员1 uintptr_t bits; // 成员2 #if defined(ISA_BITFIELD) struct { // 成员3 ISA_BITFIELD; // defined in isa.h // 位域宏定义 }; #endif }; 复制代码
通过源码我们发现isa它一个联合体,8个字节,它的特性就是共用内存,或者说是互斥(比如说如果cls赋值了,再对bits进行赋值时会覆盖掉cls)。在isa_t联合体内使用宏ISA_BITFIELD定义了位域,我们进入位域内查看源码。
# if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # 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 deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) # elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # define ISA_MAGIC_MASK 0x001f800000000001ULL # define ISA_MAGIC_VALUE 0x001d800000000001ULL # 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 deallocating : 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 复制代码
isa_t联合体有3个成员(Class cls、uintptr_t bits、联合体+位域ISA_BITFIELD),3个成员共同占用8字节的内存空间,通过ISA_BITFIELD里面的位域成员,可以对8字节空间的不同二进制位进行操作,达到节省内存空间的目的。
联合体所有属性共用内存,内存长度等于其最长成员的长度,使代码存储数据高效率的同时,有较强的可读性;而位域可以容纳更多类型
在shiftcls中存储着类对象和元类对象的内存地址信息,我们重点看一下newisa.indexcls = (uintptr_t)cls->classArrayIndex()和uintptr_t shiftcls : 33这两行源码。
上篇文章中我们提到,在Person实例对象里面可能因为内存优化,属性的位置可能发生变换(比如ch1和ch2)。但是对象内存的第一个属性必然是isa。因为isa来自于NSObject类,是继承过来的,根本还没有编辑属性列表(关于ro/rw我们后续章节会提到)。
我们就测试下,person的第一个属性是不是isa。
int main(int argc, const char * argv[]) { @autoreleasepool { Person *person = [Person alloc]; objc_getClass(); } return 0; } Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } inline Class objc_object::getIsa() { if (!isTaggedPointer()) return ISA(); uintptr_t ptr = (uintptr_t)this; if (isExtTaggedPointer()) { uintptr_t slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK; return objc_tag_ext_classes[slot]; } else { uintptr_t slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK; return objc_tag_classes[slot]; } } 复制代码
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 } # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # endif 复制代码
打印出isa & mask的值,与class第一段地址比较。
实例对象首地址一定 是isa。实例对象通过isa & isa_mask关联类。
如果我们把联合体中的位域换成基本数据类型来表示,结合内存对齐原则,ISA_BITFIELD占用24个字节。 通过位域,每一个继承自NSObject的对象都至少减少了16字节的内存空间。
// 在arm64下将位域换成基本数据类型 struct isa_t_bitFields { unsigned char nonpointer; // 1字节 0 unsigned char has_assoc; // 1字节 1 unsigned char has_cxx_dtor; // 1字节 2 补3 4 5 6 7 unsigned long shiftcls; // 8字节 8 9 10 11 12 13 14 15 unsigned char magic; // 1字节 16 unsigned char weakly_referenced; // 1字节 17 unsigned char deallocating; // 1字节 18 unsigned char has_sidetable_rc; // 1字节 19 unsigned int extra_rc; // 4字节 20 21 22 23 }; 复制代码
Class class1 = [Person class]; Class class2 = [Person alloc].class; Class class3 = object_getClass([Person alloc]); Class class4 = [Person alloc].class; NSLog(@"\n%p\n%p\n%p\n%p", class1, class2, class3, class4); 复制代码
0x1000020f0 0x1000020f0 0x1000020f0 0x1000020f0 复制代码
类在内存中只会存在一个,而实例对象可以存在多个。
我们模仿object_getClass,通过isa & isa_mask,得到对象通过isa关联的类。
int main(int argc, const char * argv[]) { @autoreleasepool { // NSObject实例对象 NSObject *object1 = [NSObject alloc]; // NSObject类 Class class = object_getClass(object1); // NSObject元类 Class metaClass = object_getClass(class); // NSObject根元类 Class rootMetaClass = object_getClass(metaClass); // NSObject根根元类 Class rootRootMetaClass = object_getClass(rootMetaClass); NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1, class, metaClass, rootMetaClass, rootRootMetaClass); } return 0; } 复制代码
0x103239120 实例对象 0x7fff9498b118 类 0x7fff9498b0f0 元类 0x7fff9498b0f0 根元类 0x7fff9498b0f0 根根元类 复制代码
1.实例对象-> 类对象 -> 元类 -> 根元类 -> 根元类(本身)
2.NSObject(根类) -> 根元类 -> 根元类(本身)
3.指向根元类的isa都是一样的
1.对象是程序猿根据类实例化的。
2.类是代码编写的,内存中只有一份,是系统创建的。
3.元类是系统编译时,系统编译器创建的,便于方法的编译
isa 走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
继承关系(实现):子类 -> 父类 -> NSObject -> nil。 根元类的父类为NSObject。