文章目录
- 前言
- 1 类和对象
- 1.1 类
- `1.1.1 cache_t 和 class_data_bits_t`
- 1.2 对象
- 2 isa指针(结构体)
- 复习-OC中的对象
- 实例对象(Instance)
- 类对象(Class)
- 元类对象
- isa的指向
- 方法调用顺序(不涉及消息转发)
- 2.1 union简单了解
- 2.2 isa的初始化
- 2.2.1 objc_object::initIsa
- 2.3 Class
- 2.4 objc_Class的源码部分
- 2.4.1 class_rw_t, class_ro_t
- 区别和联系
- 总结
- 2.4.2 cache_t
- 2.4.5 property_t
- 2.5 category不能添加成员变量
- 2.6 类方法为什么在元类里面?
- OC类的信息存放在哪里
- 最后的点
前言
分析了对象,类及其父类元类的关系之后,类和对象的底层实现是需要我们更多的学习的,在之后的消息转发,包括源码的部分结构体,包括RunLoop等等或多或少都会涉及OC类和对象的底层。
我个人认为深究类和对象的深入底层的代码较为繁琐看不懂也记不住,对于类和对象 isa_t指针是重中之重,跟着这个指针向下一步一步探索,能看懂多少就看造化了。
1 类和对象
1.1 类
在OC中,类和对象的本质是基于C/C++的结构体,编译时期会以结构体的形式被编译到二进制里面。
- 因为在源码直接看不到类的剧具体定义,直接跳转NSObject的实现。
- 通过查看
NSObject
的类定义,可以看到内部有一个Class
isa
的成员变量. 从Apple开放的objc源码来看,可以发现,Class类型是一个结构体指针 - 继续看
objc_class
的实现 发现objc_class
是继承于objc_object
的结构体.而objc_object内部.只存放了isa变量.
objc_class的 Class ISA
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 指向父类的Class结构体
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 指针都指向它的父类的 Class 结构体,直到指向 NSObject 类为止。这就形成了一个类的继承结构,同时也支持多态和动态绑定等面向对象编程的特性
1.1.1 cache_t 和 class_data_bits_t
cache_t
- 在 Objective-C 中,每个类都有一个方法缓存,用于加速方法查找。当程序需要调用一个对象的方法时,它首先在缓存中查找该方法,如果找到了就直接调用,否则就在类的方法列表中进行查找。这个过程会被重复执行,以便于提高方法查找的效率。
cache_t
是一个缓存结构体,它包含了一些关于方法缓存的信息,例如缓存的大小、已经缓存的方法数量、缓存的方法列表等等。在 Objective-C 中,缓存是在运行时创建并维护的,通过使用缓存可以显著提高方法调用的速度。- 因此,定义一个
cache_t
类型的变量cache
可以用于对方法缓存进行操作,例如查询缓存中是否存在指定的方法、向缓存中添加方法等等。
class_data_bits_t
- 这个字段包含了一些标志位和其他信息,用于描述该类对象的一些特性。
1.2 对象
对象的结构体在上一章说过了,在objc834可编译源码看看如何实现
这里是C++的面相对象思想 ,私有和公开。
抛开公开的方法部分,发现对象的结构体里面有一个名为isa
的isa_t
类型的成员变量
struct objc_object {
private:
isa_t isa;
public:
....省略方法部分
对象的本质除了其本身含有的方法列表,它有一个isa
指针又来存储其所属类的信息。
在 Objective-C 中,每个对象都有一个 isa 指针,指向它的类对象。isa 指针实际上是一个指向一个 Class 结构体的指针,这个结构体包含了与类相关的一些信息,包括:
- 类的名字;
- 父类的指针;
- 类的成员变量列表;
- 类的属性列表;
- 类的方法列表;
- 类的协议列表。
2 isa指针(结构体)
- 需要了解元类 理解OC的类,父类,元类的关系
在64位系统之前,isa是一个普通的指针,存放着类对象和元类对象的地址值,在64位系统里面,isa变成了一个共用体结构(union),同时使用位域来存储更多的信息。(这里涉及到共用内存以及位域的概念)网上查找补充的
isa结构体
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; //->存储引用计数
};
};
复习-OC中的对象
要在这里讲一下isa指针的指向问题和方法的调用顺序,再次复习一下OC的对象。
实例对象(Instance)
实例对象,顾名思义就是类的实例对象.当一个类的实例对象在堆中alloc时.内存中存放的是实例对象的isa以及成员变量.至于方法.是通过isa找到对应类对象.再寻找对应的方法进行调用
类对象(Class)
OC的类对象,其实我们上面已经讲过了.类对象存放了isa.成员变量列表.属性列表.方法列表.协议列表.但是值得注意的一点是,类对象中存放的方法为实例方法
元类对象
元类对象.是类对象的元类对象.元类对象的结构与类对象基本一致.但是存放的是类的类方法
类对象存放的是实例方法.元类对象存放的是类方法
isa的指向
对象的isa
===> 类
类的isa
===> 元类(metaClass
)
元类(metaClass
)的isa ===>父类 ===> RootMetaClass
(根元类)
RootMetaClass
(根元类)的isa
===> RootMetaClass
(根元类)
类对象的superClass
===> 父类 ===> rootClass
(根类) ===> nil
元类的superClass
==> 父元类 ===> metaRootClass
(根元类) ===> rootClass
(根类) ===> nil
方法调用顺序(不涉及消息转发)
调用实例对象方法时
- 通过isa找到对象的类,然后查找类中的方法列表,
- 如果找不到,通过superClass找到父类
- 最终到根类.如果都找不到.程序崩溃(unrecognize selector)
调用类方法时
- 通过isa找到元类,查找类方法列表
- 层层向上.到根元类.
- 最终到根类.根元类isa指向根类.都找不到.崩溃
2.1 union简单了解
为了理解isa_t的实现过程,简单了解一下union
在 Objective-C 中,union
是一种数据类型,它可以将多个不同的数据类型共享同一个内存空间。换句话说,它允许程序员在同一个内存地址上存储不同类型的数据。
一个 union
可以包含多个成员变量,每个成员变量可以是不同的数据类型,但是 union 只会为其中一个成员变量分配内存空间,这个空间的大小是所有成员变量中最大的一个。
2.2 isa的初始化
通过isa_t的结构可以得出根据union的定义,cls和bits这两个成员变量是互斥的,这也决定了isa指针初始化的时候有两种初始化方式
- 通过cls初始化,bits无默认值
- 通过bits初始化,cls有默认值、
2.2.1 objc_object::initIsa
isa的初始化会调用两层函数
objc_object::initIsa->objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
表层:objc_object::initIsa
跳转到 objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
#if !SUPPORT_INDEXED_ISA && !ISA_HAS_CXX_DTOR_BIT
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT __attribute__((unused))
#else
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT
#endif
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;
}
// 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;
}
- 首先是assert函数:在 Objective-C 中,
assert
函数用于进行断言检查,它是一个宏定义,通常用于验证程序中的条件是否满足。如果断言的条件为假,即不满足预期,assert
函数会触发一个断言失败的错误,并终止程序的执行。
void assert(int expression);
例如
int x = 5;
assert(x > 0); // 检查 x 是否大于 0,如果为假,则触发断言失败
如果 x 的值不大于 0,assert 函数会触发断言失败,程序会中止执行,并在标准错误流中输出相应的错误信息。
- 接着进行isa初始化,注释写到
isa.magic
是isa_magic_VALUE
的一部分
isa.nonpointer
是isa_MAGIC_VALUE
的一部分
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两个部分。
- 接着has_cxx_dtor的赋值(->是否设置了析构函数,如果没有,释放对象更快)
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
2.3 Class
- class ObjectClass = [[nsobject class]class]; 返回的还是class对象,并不是meta-class对象.
使用Class metaClass = objc_getMetaClass("ClassName");
获取元类对象 - 类对象在内存中有且仅有一个对象 主要包括 isa指针 super Class指针 类的属性信息 类的对象方法信息 类的协议信息 类的成员变量信息
- 元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的
// instacne对象
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
//类对象
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
Class objClass4 = object_getClass(obj1);
Class objClass5 = object_getClass(obj2);
// 元类对象
Class objcMetaClass = object_getClass([NSObject class]);
Class objcMetaClass2 = [[NSObject class] class];
// 获取元类对象
Class metaClass = objc_getMetaClass("NSObject");
NSLog(@"instance - %p %p", obj1, obj2);
NSLog(@"class - %p %p %p %p %p %d", objClass1, objClass2, objClass3, objClass4, objClass5, class_isMetaClass(objClass3));
NSLog(@"mateClass - %p %p %d",objcMetaClass, objcMetaClass2, class_isMetaClass(objcMetaClass));
NSLog(@"%@", metaClass);
}
// class方法得到的都还是类函数
2.4 objc_Class的源码部分
- 关于objc_Class的isa指针的变量已经说过了
// 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类型的结构体。
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
需要知道 bits里面的class_rw_t, class_ro_t
(readwrite, readonly)
2.4.1 class_rw_t, class_ro_t
class_rw_t
和 class_ro_t
是用来描述类信息的两个结构体
- class_rw_t 结构体表示类的可写信息,包含了类的实例变量、属性、方法等信息。
- class_ro_t 结构体表示类的只读信息,包含了类的名称、父类、实例变量、属性、方法等信息。
区别和联系
- 可写信息和只读信息:
class_rw_t
结构体表示类的可写信息,包括类的方法列表、属性列表、协议列表等。这些信息可以在运行时进行修改。class_ro_t
结构体表示类的只读信息,包括类的名称、父类、实例变量、基本方法列表、基本协议列表等。这些信息在类加载时被初始化,之后不可更改。
- 指针引用:
class_rw_t
结构体中的ro
指针指向对应类的class_ro_t
结构体,用于访问类的只读信息。class_ro_t
结构体中的baseMethods
、baseProtocols
、ivars
、weakIvarLayout
、baseProperties
等指针分别指向对应的方法列表、协议列表、实例变量列表、弱引用实例变量布局、属性列表等。
- 功能和用途:
class_rw_t
结构体用于描述类的可写信息,可以用来修改类的方法、属性、协议等,以及添加新的方- 法。class_ro_t
结构体用于描述类的只读信息,包括类的静态属性、方法列表、协议列表等,用于获取类的基本信息。
通过runtime动态修改类的方法时,其实是修改在class_rw_t区域中存储的方法列表
ro编译阶段生成,rw运行的时候生成。从存储的内容角度来讲,ro中有方法、属性、协议和成员变量,而rw中并没有成员变量。rw中的方法属性协议的取值方法中,也是通过取ro或者rwe中的值来获得。ro中的方法、属性、协议都是base,也就是只有本类中的方法属性和协议
总结
编写代码运行后,开始类的方法,成员变量 属性 协议等信息都存放在 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
2.4.2 cache_t
struct cache_t {
struct bucket_t *buckets() const;
mask_t mask() const;
mask_t occupied() const;
}
这里引入了bucket_t(散列表),是cache的内部哈希结构
bucket_t中存储的是SEL和IMP的键值对
2.4.5 property_t
只储存了其name和属性关键字
struct property_t {
const char *name; // 类型的指针,表示属性的名称。
const char *attributes; // 类型的指针,表示属性的特性,包括读写权限、属性类型、内存管理等信息。
};
- 在真正访问的时候,访问的实例变量(该结构体只是属性信息的一个描述,不包含实际的属性值。在实际使用中,需要将属性信息与具体的对象实例关联起来,才能真正地使用属性。)
2.5 category不能添加成员变量
在之前关联对象的源码也学习过,因为因为category是运行时添加的,他只能操作class_rw_t结构,但是class_rw_t结构没有添加成员变量的入口。成员变量是存储在class_ro_t中的,是无法被修改的,所以category就不能添加成员变量。
2.6 类方法为什么在元类里面?
- 单一职责设计原理。
实例对象存储成员变量的值,类对象存放,实例方法、协议、成员变量、属性,元类对象存放类方法,各司其职,互不影响。 - 复用msgSend消息发送机制。
类方法、实例方法是在上层的定义,在底层并不区分类方法实例方法,但在runtime这一层,需要承接上层类方法和实例方法,对接到底层方法调用。使用了msgSend,如果msgSend的时候需要再区分类对象,实例对象,会在内部增加判读逻辑,从而降低了效率,有了元类的存在,问题迎刃而解。
OC类的信息存放在哪里
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法存放在meta-class对象中(元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的)
- 成员变量的具体值存放在
instance
对象中
最后的点
- 对于类和对象 这篇学习从 类 父类 元类开始,到分析 对象的结构 类的结构学习到了isa指针 知道类的本质也是对象,接着了解到了union,知道了isa的初始化两种方法,接着学习objcet_class的源码 学习到了ClassISA的cache和bits,进一步了解了编译和运行时期的过程总结,通过类和元类的学习,知道了类调用方法和对象调用方法的顺序和区别,最后总结了一点小小的问题,至少对类和对象的内部结构有了更深的理解,在之后的源码学习过程和消息转发等机制中打了基础。