学习之前,我们先补充下位域和联合体的知识。
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.5 isa走位图
isa 走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
继承关系(实现):子类 -> 父类 -> NSObject -> nil。 根元类的父类为NSObject。