iOS进阶之路 (三)OC对象的原理 - isa 结构 & 走位
2020/4/6 23:01:30
本文主要是介绍iOS进阶之路 (三)OC对象的原理 - isa 结构 & 走位,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
学习之前,我们先补充下位域和联合体的知识。
1. 位域
1.1 位域的定义
所谓位域就是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作——这样就可以把几个不同的对象用一个字节的二进制位域来表示。位域是C语言一种数据结构。
使用位域的好处是:
- 有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。这样节省存储空间,而且处理简便。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
- 可以很方便的利用位域把一个变量给按位分解。比如只需要4个大小在0到3的随即数,就可以只rand()一次,然后每个位域取2个二进制位即可,省时省空间。
1.2 位域的使用
在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; } 复制代码
2.联合体
2.1 联合体的定义
联合体(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 复制代码
联合体定义使用时注意点:
- union中可以定义多个成员,union的大小由最大的成员的大小决定。
- union成员共享同一块大小的内存,一次只能使用其中的一个成员。
- 对某一个成员赋值,会覆盖其他成员的值,因为他们共享一块内存。
- union中各个成员存储的起始地址都是相对于基地址的偏移都为0。
2.2 联合体和结构体区别:
- 结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
- 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉
3. isa 的结构
在之前的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; } } } 复制代码
3.1 isa 的初始化
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; } } 复制代码
- TaggedPointer 专门用来存储小的对象(8-10),例如NSNumber和NSDate。
- 创建对象跟着断点发现nonpointer为true
- else流程走SUPPORT_INDEXED_ISA,表示isa_t中存放的 Class信息是Class 的地址,还是一个索引(根据该索引可在类信息表中查找该类结构地址)
3.2 isa_t 的结构
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定义了位域,我们进入位域内查看源码。
3.3 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 复制代码
- nonpointer:是否对isa指针开启指针优化
- 当nonpointer = 0:不优化,纯isa指针,当访问isa指针时,直接通过isa.cls和类进行关联,返回其成员变量cls
- 当nonpointer = 1:优化过的isa指针,指针内容不止是类对象地址,还会使用位域存放类信息、对象的引用计数,此时创建newisa并初始化后赋值给isa指针。 如果没有,则可以更快的释放对象。
- has_assoc:是否有关联对象,0没有,1存在。
- has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
- shiftcls:存储类对象和元类对象的指针的值,在开启指针优化的情况下,在 arm64 架构中用 33 位用来存储类指针
- magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced:对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放
- deallocating:标志对象是否正在释放内存
- has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位(rc = retainCount)
- extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
isa_t联合体有3个成员(Class cls、uintptr_t bits、联合体+位域ISA_BITFIELD),3个成员共同占用8字节的内存空间,通过ISA_BITFIELD里面的位域成员,可以对8字节空间的不同二进制位进行操作,达到节省内存空间的目的。
联合体所有属性共用内存,内存长度等于其最长成员的长度,使代码存储数据高效率的同时,有较强的可读性;而位域可以容纳更多类型
3.4 shiftcls关联类
在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关联类。
3.5 思考
如果我们把联合体中的位域换成基本数据类型来表示,结合内存对齐原则,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 }; 复制代码
4. isa走位
4.1 类在内存中只存在一个
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 复制代码
类在内存中只会存在一个,而实例对象可以存在多个。
4.2 通过对象/类查看isa走位
- 实例对象由类实例化出来。实例对象 和 类 通过isa关联
- 类本质上也是对象,通过元类实例化出来。类对象 和 元类通过isa关联:
- 实例对 - isa - 类 - isa - 元类
我们模仿object_getClass,通过isa & isa_mask,得到对象通过isa关联的类。
- 打印AKPerson类取得isa
- 由AKPerson类进行偏移得到AKPerson元类指针,打印AKPerson元类取得isa
- 由AKPerson元类进行偏移得到NSObject根元类指针,打印NSObject根元类取得isa
- 由NSObject根元类进行偏移得到NSObject根元类本身指针
- 打印NSObject根类取得isa
- 由NSObject根类进行偏移得到NSObject根元类指针
4.3 通过NSObject查看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都是一样的
4.4 类、元类是由系统创建的
1.对象是程序猿根据类实例化的。
2.类是代码编写的,内存中只有一份,是系统创建的。
3.元类是系统编译时,系统编译器创建的,便于方法的编译
4.5isa走位图
isa 走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
继承关系(实现):子类 -> 父类 -> NSObject -> nil。 根元类的父类为NSObject。
5. 参考资料
这篇关于iOS进阶之路 (三)OC对象的原理 - isa 结构 & 走位的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值