Runtime:OC对象、类、元类的本质

零、Runtime是什么
一、OC对象的本质
二、OC类的本质
三、OC元类的本质
四、Runtime关于对象、类、元类的常用API
零、Runtime是什么
Runtime即运行时,它是一个库,这个库是用C、C++、汇编语言编写的,提供的API基本都是C语言的。正是由于这个库的存在,才使得OC具备了面向对象的能力,也使得OC成为了一门动态语言,比如:
- OC对象其实是基于C/C++结构体实现的,OC方法其实是基于C/C++函数实现的,OC对象调用方法其实就是通过C/C++结构体里的指针找到具体的C/C++函数来执行,而这一切都是在Runtime库里实现的。
- 我们编写的代码一般都要经过编译链接过程(
build
,cmd+b
)来生成可执行文件,然后再运行这个可执行文件。那么对于静态语言来说,你编译链接成什么,程序在运行时肯定就执行什么,而对于动态语言来说,你编译链接成什么,程序在运行时可不一定就执行什么,也就是说在运行时我们还是能决定到底要执行什么,比如我们可以在运行时动态地为某个方法添加实现,或者把消息转发给别的对象等等,这一切也都是Runtime库在支撑的,这就使得我们编写代码可以更加灵活。

2006年苹果发布了OC 2.0,其中对Runtime的很多API做了改进,并把OC 1.0中Runtime的很多API标记为“不可用”、“无效”、“将来会被废弃”等。
但是两套API的核心实现思路还是一样的,而旧API比较简单,所以我们会分析旧API,然后看看新API作了哪些变化,这里有最新的Runtime源码。
一、OC对象的本质
1、OC 1.0
通过查看Runtime的源码(objc.h
文件),我们得到OC对象的定义如下(伪代码):
typedef struct objc_object *id; // id类型的本质就是一个objc_object类型的结构体指针,所以它可以指向任意一个OC对象
struct objc_object {
Class isa; // 一个Class类型的结构体指针,存储着一个地址,指向该对象所属的类
// 自定义的成员变量,存储着该对象这些成员变量具体的值
NSSring *_name; // “张三”
NSSring *_sex; // “男”
int _age; // 33
};
可见OC对象的本质就是一个objc_object
类型的结构体,该结构体内部只有一个固定的成员变量isa
,它是一个Class
类型的结构体指针,存储着一个地址,指向该对象所属的类。当然OC对象内部还可能有很多我们自定义的成员变量,存储着该对象这些成员变量具体的值。
2、OC 2.0
通过查看Runtime的源码(objc-private.h
文件),我们得到OC对象的定义如下(伪代码):
typedef struct objc_object *id;
struct objc_object {
isa_t isa; // 一个isa_t类型的共用体
// 自定义的成员变量,存储着该对象这些成员变量具体的值
NSSring *_name; // “张三”
NSSring *_sex; // “男”
int _age; // 33
}
union isa_t {
Class cls;
unsigned long bits; // 8个字节,64位
struct { // 其实所有的数据都存储在成员变量bits里面,因为外界只访问它,而这个结构体则仅仅是用位域来增加代码的可读性,让我们看到bits里面相应的位上存储着谁的数据
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
unsigned long nonpointer : 1;
unsigned long has_assoc : 1;
unsigned long has_cxx_dtor : 1;
unsigned long shiftcls : 33; // 对象所属类的地址信息
unsigned long magic : 6;
unsigned long weakly_referenced : 1;
unsigned long deallocating : 1;
unsigned long has_sidetable_rc : 1;
unsigned long extra_rc : 19;
# endif
};
};
可见OC对象的本质还是一个objc_object
类型的结构体,该结构体内部也还是只有一个固定的成员变量isa
,只不过64位操作系统以后,对isa
做了内存优化,它不再直接是一个指针,而是一个isa_t
类型的共用体,它同样占8个字节,但其中只有33位用来存储对象所属类的地址信息,还有19位用来存储(对象的引用计数 - 1),其它位上则存储着各种各样的标记信息。
nonpointer
:占1位,标记isa
是否经过内存优化。如果值为0,代表isa
没经过内存优化,它就是一个普通的isa
指针,64位全都用来存储该对象所属类的地址;如果值为1,代表isa
经过了内存优化,只有33位用来存储对象所属类的地址信息,其它位则另有用途。has_assoc
:占1位,标记当前对象是否有关联对象,如果没有,对象销毁时会更快。has_cxx_dtor
:占1位,标记当前对象是否使用过C++析构函数,如果没有,对象销毁时会更快。shiftcls
:占33位,存储着当前对象所属类的地址信息。-
magic
:占1位,用来标记在调试时当前对象是否未完成初始化。 weakly_referenced
:占1位,标记弱引用表里是否有当前对象的弱指针数组——即是否被弱指针指向着、是否有弱引用,如果没有,对象销毁时会更快。-
deallocating
:占1位,标记当前对象是否正在释放。 has_sidetable_rc
:占1位,标记引用计数表里是否有当前对象的引用计数,如果没有,对象销毁时会更快。extra_rc
:占19位,存储着(对象的引用计数 - 1),存值范围为0~255。
共用体也是C语言的一种数据类型,和结构体差不多,都可以定义很多的成员变量,但两者的主要区别就在于内存的使用。
一个结构体占用的内存等于它所有成员变量占用内存之和,而且要遵守内存对齐规则,而一个共用体占用的内存等于它最宽成员变量占用的内存。结构体里所有的成员变量各自有各自的内存,而共用体里所有的成员变量共用这一块内存。所以共用体可以更加节省内存,但是我们要把数据处理好,否则很容易出现数据覆盖。
3、对象内部的其它成员变量
我们通常说的OC对象一般是指狭义的OC对象——即实例对象,而广义的OC对象则包括实例对象、类、元类。每个类可以创建出N多个实例对象,一个实例对象占用一份内存,它们的成员变量除了isa
存储的值一样外,其它成员变量都存储着该对象这些成员变量具体的值。
INEPerson *person1 = [[INEPerson alloc] init];
person1.name = @"张三";
person1.sex = @"男";
person1.age = 33; // person1的age内存中存储的是数值33
INEPerson *person2 = [[INEPerson alloc] init];
person2.name = @"李四";
person2.sex = @"男";
person2.age = 44; // person2的age内存中存储的是数值44
NSLog(@"%p", person1);// 0x10054bd70
NSLog(@"%p", person2);// 0x10054bb80
二、OC类的本质
1、OC 1.0
通过查看Runtime的源码(runtime.h
文件),我们得到OC类的定义如下(伪代码):
typedef struct objc_class *Class; // Class类型的本质就是一个objc_class类型的结构体指针,所以它可以指向任意一个OC类
struct objc_class {
Class isa;
Class super_class;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
const ivar_list_t *ivars;
cache_t cache;
const char *name;
long instance_size;
long version;
long info;
};
可见OC类的本质就是一个objc_class
类型的结构体,该结构体内部有若干个成员变量,其中有几个是我们重点关注的:
-
isa指针
:存储着一个地址,指向该类所属的类,即元类。(面向对象编程中,我们常说“万事万物皆对象”,所以类也是一个对象) superclass指针
:存储着一个地址,指向该类的父类。methods
:数组指针,存储着该类所有的实例方法信息。properties
:数组指针,存储着该类所有的属性信息。protocols
:数组指针,存储着该类所有遵守过的协议信息。ivars
:数组指针,存储着该类所有的成员变量信息。cache
:结构体,存储着该类所有的方法缓存信息。
2、OC 2.0
通过查看Runtime的源码(objc-runtime-new.h
文件),我们得到OC类的定义如下(伪代码):
typedef struct objc_class *Class;
struct objc_class : objc_object {
// isa_t isa; // objc_class继承自objc_object,所以不考虑内存对齐的前提下,可以直接把isa成员变量搬过来
Class superclass;
class_data_bits_t bits; // 存储着该类的具体信息,按位与掩码FAST_DATA_MASK便可得到class_rw_t
cache_t cache; // 存储着该类所有的方法缓存信息
}
// class_rw_t结构体就是该类的可读可写信息(rw即readwrite)
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // 该类的只读信息
method_array_t methods; // 存储着该类所有的实例方法信息,包括分类的
property_array_t properties; // 存储着该类所有的属性,包括分类的
protocol_array_t protocols; // 存储着该类所有遵守过的协议,包括分类的
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
// class_ro_t结构体就是该类的只读信息(ro即readonly)
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList; // 存储着该类原来的实例方法信息
protocol_list_t * baseProtocols; // 存储着该类原来遵守过的协议信息
const ivar_list_t * ivars; // 存储着该类原来的成员变量信息
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; // 存储着该类原来的属性信息
}
可见OC类的本质还是一个objc_class
类型的结构体,只不过它的内部结构套了好几层,但我们重点关注的那几个成员变量都还是可以顺利找到的。
其实在编译时,bits
成员变量按位与掩码得到的是class_ro_t
结构体,该结构体内部存储着我们在类本身定义的方法、属性、协议、成员变量。而在运行时系统才生成了一个class_rw_t
结构体,并把类本身的方法、属性、协议和分类里的方法、属性、协议合并到class_rw_t
结构体的中,同时设置class_rw_t
结构体为可读可写,class_ro_t
结构体为只读,bits
成员变量按位与掩码得到class_rw_t
结构体。
3、获取某个对象所属的类
每个类只有一个,它在内存中只有一份。我们可以通过实例对象的-class
方法或Runtime的APIobject_getClass
函数来获取某个对象所属的类:
#import <objc/runtime.h>
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
Class class1 = [object1 class];
Class class2 = [object2 class];
Class class3 = object_getClass(object1);
Class class4 = object_getClass(object1);
NSLog(@"%p", class1);// 0x7fff93310140
NSLog(@"%p", class2);// 0x7fff93310140
NSLog(@"%p", class3);// 0x7fff93310140
NSLog(@"%p", class4);// 0x7fff93310140
三、OC元类的本质
所谓元类,是指一个类所属的类,我们每创建一个类,系统就会自动帮我们创建好该类所属的类——即元类。元类和类的本质其实都是objc_class
,只不过它们的用途不一样,类的methods
成员变量里存储着该类所有的实例方法,而元类的methods
成员变量里存储着该类所有的类方法。
获取某个类所属的类——元类
每个元类也只有一个,它在内存中也只有一份。我们可以通过Runtime的APIobject_getClass
函数来获取某个类所属的类——元类,只不过要把一个类作为参数传进去:
#import <objc/runtime.h>
Class metaClass = object_getClass([NSObject class]);
NSLog(@"%p", metaClass);// 0x7fff933100f0
玩儿一下,既然说元类是类的类,那能不能通过class
方法来获取元类呢?
Class metaClass1 = [[NSObject class] class];
NSLog(@"%p", metaClass1);// 0x7fff93310140
我们发现metaClass1
和metaClass
的地址值是不一样的,这就表明不能通过class
方法来获取元类。因为[NSObject class]
里的NSObject
并不是一个实例对象,而是一个类,所以它调用的class
方法其实不是实例方法-class
,而是类方法+class
,实例方法-class
确实是返回该对象所属的类,而类方法+class
则是返回该类本身。下面是Runtime的源码(NSObject.mm
文件):
// 返回类本身
+ (Class)class {
return self;
}
// 返回该对象所属的类
- (Class)class {
return object_getClass(self);
}
// 这两个也可以认为是统一的,反正都是返回父类,你管它是当前类的父类还是当前对象所属类的父类呢
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
// 这两个是统一的,可以理解为:当前对象是不是某个类的实例
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass(self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
// 这两个是统一的,可以理解为:当前对象是不是某个类或其子类的实例
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
四、Runtime关于对象、类、元类的常用API
// 获取一个对象所属的类
Class object_getClass(id obj);
// 设置一个对象所属的类
Class object_setClass(id obj, Class cls);
// 获取一个类的父类
Class class_getSuperclass(Class cls);
// 判断一个对象是不是Class
BOOL object_isClass(id obj);
// 判断一个类是不是MetaClass
BOOL class_isMetaClass(Class cls);
// 动态创建一个类(参数:父类,类名,额外的内存空间通常传0即可)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes);
// 注册一个类(创建好一个类,在注册之前,我们通常会调用class_addIvar、class_addMethod等函数为类添加成员变量、方法等)
void objc_registerClassPair(Class cls);
// 销毁一个类
void objc_disposeClassPair(Class cls);