isa是Class设计的精髓,没有它就没有消息机制,没有引用计数机制,无法管理弱引用和联对象。isa通过指向类对象或者元类对象使对象的内存设计更为分散又紧密相连。所以了解isa对夯实自己的OC基础非常重要,而且以此展开的问题一直是面试热点。
写在前面的话
OC 是一门复杂的高级语言,正是因为巧妙复杂的结构设计和内存模型,巧妙的运用isa和superClass才赋予这门语言独特的属性和特点。
实例对象的isa 指针类对象,类对象的isa指针指向metaClass,metaClass的isa指针指向基类NSObject.
实例对象没有superClass指针,类对象的superClass指向父类对象,一直到基类的类对象[NSObject class], NSObject的类对象指向nil。
metaClass对象的superClass指向父类的metaClass对象,一直到基类的metaClass对象, NSObject的metaClass对象指向类对象[NSObject class]。
面试
(文末回答,也请评论你遇到的面试问题,共同进步。)
- isa的结构?
- Class的结构?
- isa指针的作用
- IMP和SEL以及具体执行的操作
- OC对象所占的内存大小怎么计算
看清OC对象和实例对象的本质
OC是一门面向对象的语言,万物皆对象,比如实例对象,类对象,元类对象,block对象,每个对象都有一个isa指针。NSObject是所有对象的基类(NSPoxy
除外),所以我们从NSObject 慢慢梳理:
实例对象实际上指向的是一个objc_object
指针类型,objc_object
结构体内部存储着一个isa指针指向类对象:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
NSObject类对象内部只有一个Class isa指针,Class isa是一个objc_class 类型的结构体,这个结构体存储着类的基本信息:
@interface NSObject {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
}
精简一下这个objc_class结构体,最终如下所示:
了解完整体结构,然后阐述objc_class中每一个变量的作用:
Class isa
Class 中的isa指针指向metaClass对象,执行类方法时通过这个指针找到存储在metaClass中的方法信息,同样是首先在缓存中查找再去方法列表中查找。
iOS中的每个对象都有一个isa指针,正是通过isa指针才把一个对象的内存模型巧妙的分散又紧密的联系在一起,isa在64位之后不再单单存储一个地址值了,苹果对它的结构进行了优化,通过union 共用体采用位域的原理可以存储更多的信息;union中包含了一个struct,struct的存在仅仅是增加可读性,并没有实际内存意义,所有的信息都存储在 uintptr_t bits的这个8个字节的内存中。转化成二进制一共64位,其中只有uintptr_t shiftcls 这33位用来存储对象内存地址,其他31位分别存储不同的信息。
nonpointer
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址。
1,代表优化过,使用位域存储更多的信息,64位之后默认是1。has_assoc
是否有设置过关联对象,如果没有,释放时会更快,释放对象时要通过对象的地址找到存储在全局对象中的关联对象释放该对象。has_cxx_dtor
是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快。shiftcls
占据33位,存储着Class、Meta-Class对象的内存地址信息。magic
用于在调试时分辨对象是否未完成初始化。weakly_referenced
是否有被 弱引用指向过,如果没有,释放时会更快。deallocating
对象是否正在释放。has_sidetable_rc
引用计数器是否过大无法存储在isa中如果为1,那么引用计数会存储在一个叫SideTable的类的属性中。extra_rc
占据19位,里面存储的值是引用计数器减1后的值。
Class superClass
指向父类的类对象,执行方法时,如果缓存中和方法列表中都没有命中的话会通过该指针找到父类的类对象,在父类的类对象中同样是先去缓存中查找,再去方法列表中查找,找到之后执行并且缓存到子类的方法列表中。
cache_t cache
cache_t结构体中有一个哈希表也叫散列表,用来存储对象调用过的方法,这是一种牺牲空间换时间的数据结构。
设置方法缓存流程如下:
1.以SEL为key & mask = 下标;
2.通过下标查看散列表中对应位置是否有值,如果没有把bucket_t(包含SEL和IMP的结构体)存入该位置;如果有值说明哈希冲突,去下标位置减1的位置查看是否有值,如果没有值直接存入;如果有值继续减1,直至等于遍历一遍。
3.当_mask + 1 = _occupied的时候散列表会扩容_occupied * 2,扩容的时候会将原来的缓存清除掉。
获取方法缓存流程如下:
1.以SEL为key & mask = 下标;
2.通过下标取出散列表中对应的bucket_t;
3.判断bucket_t中的key是否和SEL相同,相同取出IMP方法实现;否则下标减1一次遍历直到取出切key和传入的SEL相同。
class_data_bits_t bits
bits & FAST_DATA_MASK得到class_rw_t的内存地址
class_rw_t和class_ro_t中存储着类的基本信息,class_ro_t只读,class_rw_t可读可写。class_ro_t中存储着类的一些初始化信息,运行时会把这些初始信息copy到class_rw_t中,如若有分类同时也会把分类的信息copy到class_rw_t中。值得注意的是成员变量信息只存储在class_ro_t中,但是Category_t内部有储存方法和协议以及属性的列表,唯独没有储存成员变量的列表,这也是Categoty无法添加成员变量的根本原因。class_ro_t中的method_list_t是一个一维数组存放着缓存的方法,class_rw_t中的method_list_t是一个二维数组每一个元素可能是类的原始方法列表,也可能是分类的方法列表,列表中存放的是method_t。
面试答案
- isa的结构?
isa 在64位之前是以存储的是一个对象地址,64位之后通过union存储了更多的信息。包括对象地址和引用计数,是否弱引用,会否设置关联对象。
- Class的结构?
包括isa指针,指向原类对象;superClass指针指向父类的类对象;方法缓存结构体引用,结构体中包含缓存方法的一个哈希表缓存;class_data_bits_t的一个结构体通过MASK值的位运算可以得到class_rw_t里面包含class_rw_t,后者包含类的初始信息(成员变量,方法列表,属性列表,协议列表)。运行时会把这些初始信息copy到class_rw_t,若有分类会把分类的数据信息同样添加到class_rw_t中。
- isa指针的作用?
1、isa通过指向类对象或者元类对象使对象的内存设计更为分散又紧密相连,为runtime动态调整对象的内存信息提供便捷。
2、内存管理时通过isa得知对象的引用计数情况,以及关联对象和若引用情况。
- IMP和SEL以及具体执行的操作?
IMP是函数的具体实现,有typeddef 定义,
SEL是函数名是一个C语言的字符串。
两者合起来再加上方法的签名构成了struct method_t
- OC对象所占的内存大小怎么计算?
如下所示的Person 对象,一个isa指针占8个字节,一个int 变量占4个字节, 但是系统还是会分配16个字节给person对象,因为当对象申请的内存小于16个字节时,系统默认会分配16个字节。
@interface Person()
{
int _age;
}
还有一个概念叫内存对齐原则,结构体的大小必须是最大成员大小的倍数;
@interface Student()
{
int _age; //4
Double _hight; // 8
}
isa 8 + int 4 + double 8 = 20, 但实际上系统会根据内存对齐分配24个字节,必须是8的倍数。
扩展:
引用计数器管理、方法缓存原理、Categoty原理、关联对象原理、OC消息机制
(系列文章已通过蓝色文本标注)
如有错误或者新的见解欢迎在评论区约谈...