OC底层(isa、类对象、元类对象篇)
上回书说到了OC的对象及其alloc和init。验证了OC对象底层是结构体,然后在alloc的方法调用栈的最后一个关键方法creatinstance中,有一个创建isa指针的方法。这篇,我们就先聊一聊isa指针。
为什么万物继承自NSObject?
我们知道,OC中的绝大部分对象,都是继承自NSObject(目前我已知的只有一个 NSProxy 类不继承自NSObject,它跟NSObject一样,是基类)。按住command键,跳进NSObject的头文件,就能看到下面的代码:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
这就是NSObject的声明了,它只有一个isa指针,万物继承自NSObject,就有了自己的isa指针了。那这个isa到底有什么用呢?它究竟是个啥东西?
位域
在说isa之前,我们先了解一下共用体和位域这种技术。
思考这样一个问题,一类有3个bool类型的属性,该如何存储与读取。我们第一时间想到的就是定义成property。然后通过set和get方法完成这个操作。我们加点要求,使用极致的最小空间来实现这个需求,例如,一个字节,能不能搞定这个问题?答案是,当然可以。理论来说,一个bool值只需要一位,3个bool值也只需要3位。咱们直接看代码吧,talk is cheap ,show me the code。
第一种实现位存储的方法,是通过利用掩码的操作来实现,原理如下:
与运算 & 可以用来取出特定的位,
需要取第几位,第几位设为1,其他位为0,例,取第二位的值,得出0000 0010,这就是掩码
// 0000 0111 三个属性值分别存储在第一位、第二位、第三位
//&0000 0010 取出第二位
//----------
// 0000 0010 取出的第二位是1
设置值的时候,如果值为YES,可以直接与掩码直接进行或操作,
例,设置第二位的值为YES
// 0010 1000 三个属性值分别存储在第二位、第四位、第六位
//|0000 0010 直接与掩码或运算
//----------
// 0010 1010 成功将第二位的值改为1,如果原来值为1,或运算后仍然为1
设置值得时候将掩码按位取反,然后再进行&与操作,则会将,
例,设置第二位的值为NO
// 0010 1010 三个属性值分别存储在第二位、第四位、第六位
//&1111 1101 直接与掩码或运算
//----------
// 0010 1000 成功将第二位的值改为0,如果原来值为0,或运算后仍然为0
例如,Person类有个tall、rich、handsome三个bool值,使用一个字节_tallRichHansome。我们通过自定义set和get方法来实现。handsome设置在第二位,则HandsomeMask为0000 0010。
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHansome |= HandsomeMask;
} else {
_tallRichHansome &= ~HandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHansome & HandsomeMask);
}
第一种方法实现了,不过有很多的弊端,比如扩展新的’属性‘值的时候,要定义新的掩码,定义新的set和get方法,代码增加了,可读性也会变差。此时我们引入C语言中的位域技术,位域技术,就是多个字段公用一块内存,这种方法不用定义掩码,代码和解释如下:
@interface Person
{
// 位域,变量名:1 表明此变量的名称和占用的位数
struct {
//忽略下面的char,它们是共享一块内存的,
//:1代表就占1位,且从低位开始计算,
char tall : 1; //tall 共1位,占用一个字节的第1位
char rich : 1; //rich 共1位,占用一个字节的第2位
char handsome : 1; //handsome 共1位,占用一个字节的第3位
} _tallRichHandsome; //tallRichHandsome的结构体
}
@end
存取值,做出相应的改变,仍以handsome为例。
- (void)setHandsome:(BOOL)handsome
{
//此时方便了很多,可以直接使用.来进行赋值了
_tallRichHandsome.handsome = handsome;
}
- (BOOL)isHandsome
{
//赋值的时候,要进行两次的取反。因为handsome是一位,而返回值要求为BOOL,占用8位
//如果此时handsome为1,另外7位会自动补为1,变为1111111,
//如果用一个字节存储255和-1,都是11111111,所以此时打印isHandsome,会是-1
//两次取反,取反没有符号一说,只要非0就是1,取值就对了
//如果属性值占用两位,则不会出现上述问题,也就不用两次取反了,感兴趣的可自行验证
return !!_tallRichHandsome.handsome;
}
最后一种方案,就是共用体。这种方案,结合了第一种方案的位运算的速度优势,与第二种方案点语法的方便。共用体的一个特征跟位域类似,就是所有设置的值公用一块儿内存,也需要使用掩码来进行相应的位运算来进行值的存储也读取。我们来看一下具体的定义:
{
union { //union(共用体)类型的结构体
char bits;//里面有个变量bits,将来值存在这个里,带位域功能
struct {//仅仅为了可读性,没有实际意义。删掉这个struct也可以
char tall : 1;//仅仅为了可读性,没有实际意义。
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
存取值的代码也做了相应的改变
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
} else {
_tallRichHandsome.bits &= ~HandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHandsome.bits & HandsomeMask);
}
了解了union和位域的技术,接下来就是我们的主角,isa。在armv7、armv7s时代,由于是32位操作系统,苹果并没有对指针进行优化,实例对象的isa是直接指向类对象的。进入到arm64架构时代后isa占用8个字节,共计64位,如果只是单纯存一个指针太浪费,所以,苹果通过共用体技术,充分的让这64位存储了非常多的信息,不用再额外的开辟空间来存储,减少了内存开支,减少了内存的操作。下面我们具体看一下isa的结构。
isa的定义
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct{
ISA_BITFIELD; // defined in isa.h 说明在isa.h中
};
#endif
};
里面有个ISA_BITFIELD,再看这个是怎么定义的:(这个define在不同的架构下是不同的,如果要验证掩码的正确性,要使用跟架构一致的掩码,所有的define都在上一篇objc4-787.2.tar.gz这份源码的isa.h文件中)
# 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)
isa union共用体中各个位的含义
-
nonpointer 指示位,表示是否对isa指针开启指针优化,值为0表示纯isa指针 1表示不止是类对象的地址,isa中还包含了类信息、对象的引用计数等
-
has_assoc: 关联对象标志位,0没有,1存在(KVO的实现原理)
-
has_cxx_dtor :该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
-
shiftcls: 实际存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针,也是基于此,不同的架构中,对于类指针的读取需要不同的操作,在32位时期,由于,由于未进行isa优化,isa是直接指向类对象的,在64位之后,需要用isa指针与上掩码(ISA_MASK)才能取到指针,这个指针指向类对象。
-
magic :用于调试器判断当前对象是真的对象还是没有初始化的空间
-
weakly_refrenced: 对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象,可以更快的释放
-
dellocating :标志对象是否正在释放对象。ing表示进行时
-
has_sidetable_rc:当对象引用计数大于0时,则需要借用该变量存储进位。weak的实现原理,就是使用sidetables。后续讲到内存管理的时候会对此进行详细的讲解。
-
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1.例如,如果对象的引用计数为10,那么extra_rc为9,如果引用计数大于10,则需要使用到上面的has_sidetable_rc。
用64位存储了这么多的信息,相当的高效!!我当初知道这个点的时候,叹为观止!苹果真的把这种细节这种设计做到了极致。苹果的工程师们真他娘的都是人才,各个身怀绝技(也可能我作为井底之蛙,没有了解过这个技术)。
好,介绍过isa之后,接下来就该是实例对象、类对象、和元类对象了。
在arm64架构之后,isa通过shiftcls来指向类对象的。类对象也是个对象,也有isa指针,它的isa中的shiftcls是指向元类对象的。元类对象也有isa,它的shiftcls都指向根类的元类对象。 网上有一张非常经典的图来展示了对象和元类对象的关系(这幅图将贯穿大部分关于底层原理的博客内容)来看一下:
实例对象、类对象、元类对象的关系图
这个图用语言来描述一下,就是
- 实例对象的isa指向的类对象,类对象的isa指向元类对象。元类对象的isa均指向基类的元类对象(基类的元类对象的isa指向自己)。需要特别注意,arm64架构后,isa不直接指向,需要先用掩码取出shift_cls指针,这个是实际的指向关系的指针。
- 类对象和元类对象中有superclass指针,普通类对象的superclass指针指向父类的类对象,基类类对象的superclass指向nil。基础对象的元类对象的superclass指针指向父类的元类对象,基类的superclass指针指向父类的类对象。
写完这些我都不认识这个类字了。谨记这幅图,就能非常好的理解实例对象、类对象、元类对象之间的关系了。这个图画的非常好,在后面的runtime的消息转发流程中还会用到这幅图以及这些对象之间的关系。
我们为什么需要类对象和元类对象
那为啥要有类对象和元类对象呢?类对象和元类对象中都放着什么东西呢?
先说为什么。我们都知道,我们创建的类,有很多的属性、协议和方法,方法又分为实例方法和类方法。而这些方法的实现都是统一的,再调用的过程中,只是参数的值不同,所以这些方法存一份儿就够了,没必要在每个对象中都存一份。所以就有了类对象和元类对象。可以想一下平时写的方法调用的代码,假定Person类有一个实例方法叫-(void)instanceFunction,有个类方法+(void)classFunction。我们在调用的时候是这么调用的:
- (void)callFunctions{
[person instanceFunction]; //使用实例对象来调用实例方法
[Person classFunction]; //使用类名(其实就是类对象)调用类方法
}
可以很明显的看出,方法调用者的不同。而这个不同就是由类对象、元类对象、isa来实现的。类对象中,存储了这个类的属性、协议和实例方法。元类对象中存储了这个类的类方法。在方法调用时,实例方法通过这个实例对象的isa找到这个类的对象,然后在类对象中查找这个方法。类方法通过类对象的isa找到这个类的元类对象,在元类对象中查找这个方法。通过这样的描述,结合上面的图,就能很好的理解isa指针、类对象和元类对象了。
好,简单的来总结一下今天的内容。主要是理清实例对象、类对象、元类对象之间的关系,要记住那副图,保证自己能完整的复述出他们之间的关系。isa就相当于纽带,将他们串联了起来。在最后提到了,类对象和元类对象是存储属性、协议和方法的,我们会在runtime中,具体的讲解这些信息是如何存储的。
那这篇文章就暂时收尾了,如果有错误,请指正。
PS:感觉共用体和位域那块跟本文章的结合还不是特别好,后续可能会考虑拆分出去。
转载自:https://juejin.cn/post/7002156875588304927