文章目录
三个问题先放前面
- 一个NSObject对象占用多少内存?
- 对象的isa指针指向哪里?
- OC的类信息存放在哪里?
在了解过后我们应该就会对这三个问题的答案有数了
类与对象
类和对象的本质
对象部分
OC的类和对象本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。
直接干源码
关于对象的源码 我们之间找到objc_object
struct objc_object {
private:
isa_t isa;
public:
....省略方法部分
不看方法部分,我们可以发现其成员变量只有一个isa_t类型的isa
isa_t这个类型只有一个成员变量 存储了对象所属类的信息[Class cls]
ok对象的本质除了其本身含有的方法列表,它有一个isa指针又来存储其所属类的信息。
类部分
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
objc_class继承于objc_object,换句话说,类的本质也是个对象
那么类的isa存储的是什么呢?
再说之前先明白isa是结构体,其中会存储类的地址,其并不是指针
“isa指向”这句话严格来说是不正确的 但是方便理解
在此就要引入元类和根类(一般情况下就是NSObject)的概念了
今年3月有写过 直接拿来用了 很好理解
isa详解
来详细讲解一下isa指针 即isa_t结构体
isa_t类型我们之前ARC的objc_retain部分已经有所了解
不过这里我们和ARC关注的重点不同
每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。
我们还是看一下isa_t这个定义
union isa_t
{
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;//->表示使用优化的isa指针
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;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
uintptr_t extra_rc : 19; //->存储引用计数,实际的引用计数减一,存储的是其对象以外的引用计数
};
};
实际上,在64bit开始,isa需要进行一次位运算即 isa & ISA_MASK,才能计算出指向的类或元类对象的真实地址
isa_t的初始化
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#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
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
isa = newisa;
}
assert()函数
其作用是如果它的条件返回错误,则终止程序执行。
assert的作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
初始化过程
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
这个里面说的很明白 news.bits初始化时候只设置了nonpointer,magic两个部分
可以看一下 在调用完后打印一下newisa
然后给时has_cxx_dtor的赋值,这一位表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。
下一步就是赋值shiftcls
在setClass里面实现 存储着Class\Meta-Class对象的内存地址信息
给extra_rc
的赋值1???
相比于之前的rootRetain部分的实现,逻辑说不过去,索性搞了个新版的源码,果然retainCount部分已经发生了改变 没有加一的实现了 而是在initIsa的时候进行初始化直接赋值一的操作 省略了retainCount部分的加一
class方法
首先Class这里需要注意几点
- class ObjectClass = [[nsobject class]class]; 返回的还是class对象,并不是meta-class对象。
- (Class)class, +(Class)class
返回的就是类对象
//instance实例对象
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
//class对象,类对象
//objectClass1~5都是NSObject的类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
//元类对象(将类对象当作参数传入进去)
Class objectMetaClass = object_getClass([NSObject class]);
Class objectMetaClass2 = [[NSObject class] class];
//判断是不是元类对象
NSLog(@"instance - %p %p", object1, object2);
NSLog(@"class - %p %p %p %p %p %d", objectClass1,objectClass2, objectClass3, objectClass4, objectClass5, class_isMetaClass(objectClass3 ));
NSLog(@"mateClass - %p %p %d",objectMetaClass, objectMetaClass2, class_isMetaClass(objectMetaClass));
输出情况:
instance - 0x100511920 0x10050e840
class - 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0
mateClass - 0x7fff91da20f0 0x7fff91da2118 1
无论多少次class方法得到的都还是类函数。
- 元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的
- 类对象在内存中有且仅有一个对象 主要包括 isa指针 super Class指针 类的属性信息 类的对象方法信息 类的协议信息 类的成员变量信息
看一下objc_class的源码部分
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存 formerly cache pointer and vtable
class_data_bits_t bits; // 用于获取具体的类信息 class_rw_t * plus custom rr/alloc flagsflags
...
bits里面存储了类的方法列表等等的信息
这里的bits是class_data_bits_t类型的,上面objc_object的isa_t类型数据中也有一个uintptr_t类型的bits,但是这是两种结构。
class_rw_t和class_ro_t
bits里面有class_rw_t,class_ro_t两个方法
ro:
rw:
二者相比 首先最直观的可以看到只读文件存储的类的属性、方法、协议前面都有Base 并且多了一个ivars(成员变量)
而读写并没有这个成员变量
所以为后面做个铺垫 通过runtime动态修改类的方法时,其实是修改在class_rw_t区域中存储的方法列表
流程总结
methodizeClass方法、realizeClass方法
- 编写代码运行后,开始类的方法,成员变量 属性 协议等信息都存放在 const class_ro_t中
- 运行过程中,会将信息整合,动态创建 class_rw_t
- 然后会将 class_ro_t中的内容(类的原始信息:方法 属性 成员变量 协议信息) 和 分类的方法 属性 协议 成员变量的信息 存储到 class_rw_t中.并通过数组进行排序,分类方法放在数组的前端,原类信息放在数组的后端.
- 初始运行时 objc-class中的 bits是指向 class_ro_t的,bits中的data取值是从class_ro_t中获得,而后创建 class_rw_t,class_rw_t中的 class_ro_t从初始的 class_ro_t中取值(所以这两个不是一个对象),class_rw_t初始化完成后,修改 objc_class中的bits指针指向class_rw_t
cache_t
struct cache_t {
struct bucket_t *buckets() const;
mask_t mask() const;
mask_t occupied() const;
}
这里引入了bucket_t(散列表)
cache_t 哈希表结构,哈希表内部存储的bucket_t
bucket_t中存储的是SEL和IMP的键值对
property
我们都只知道property会帮属性自动生成set\get方法和一个成员变量
struct property_t {
const char *name;
const char *attributes;
};
我们只储存了其name和属性关键字
总结
第一个问题看这篇就足够了
内存对齐
第二个问题对象的isa指针指向哪里
答:每一个实例变量的isa都指向自己所属的类,每一个类的isa都指向自己所属的元类,同时,父类的元类也是子类的元类的父类,父类的元类也是根元类的子类。而父类的元类和子类的元类都属于根元类。根元类的isa指向自己,同时根元类的父类也是自己的下属类(NSObject元类的父类是NSObject类)。
OC的类信息存放在哪里?
答:对象方法、属性、成员变量、协议信息,存放在class对象中
类方法存放在meta-class对象中(元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的)
成员变量的具体值存放在instance对象中
遇到的问题
不过还有问题
- extra_rc这个存储信息为什么是引用计数减一的结果
- extra_rc的值的改变一会儿加一,一会儿减一 把人搞晕了
- 在自己实际测试时,为什么就算我main函数中什么也不写,在initIsa中打断点也在一直调用?很多次之后才会结束,编译器帮我们做了什么工作?
- 818rootRetainCount函数的实现相比于750的进行了什么优化?
- alloc的创建过程到底是怎么样的?
为什么要考虑这些问题呢?我只需要理解其过程就好了 何必在乎内部的运行呢
导入#import "objc/runtime.h"
之后的object_getClass就是得到其isa指针
superClass的得到的是父类指针。
这个图太重要了