本次讲解的不少内容都涉及到objc的源码,有兴趣的能够去下载最新版本的objc4源码。c++
咱们平时开发中说用到了绝大多数的类都是以NSObject
做为基类。咱们进入NSObject.h
文件能够看到NSObject
类的定义以下:缓存
@interface NSObject <NSObject> {
Class isa ;
}
复制代码
咱们将OC代码转成c/c++代码后能够看到,NSObject
类是经过结构体
来实现的,以下所示:bash
struct NSObject_IMPL {
Class isa;
};
// Class的定义
typedef struct object_class *Class;
复制代码
从上面能够看出这个结构体
和OC中NSObject
类的定义是一致的。这个类中只包含一个Class
类型的属性,而Class是一个指向object_class
结构体的指针(结构体的地址就是结构体中第一个成员的地址
),因此isa
就是一个指针,占8个字节(64位机器上面)。那么,是否是意味着一个NSObject对象在内存中就占8个字节呢?咱们经过代码测试一下:数据结构
NSObject *obj = [[NSObject alloc] init];
// class_getInstanceSize()函数须要引入头文件#import <objc/runtime.h>
NSLog(@"---%zd",class_getInstanceSize([obj class]));
// malloc_size()函数须要引入头文件#import <malloc/malloc.h>
NSLog(@"---%zd",malloc_size((__bridge const void *)(obj)));
// ***************打印结果***************
2020-01-03 11:48:21.302884+0800 AppTest[62149:5950838] ---8
2020-01-03 11:48:21.303065+0800 AppTest[62149:5950838] ---16
复制代码
class_getInstanceSize()
函数获得的结果和咱们预期是一致的,这个函数是runtime
的一个函数,它返回的是类的一个实例的大小。咱们查看objc4
源码能够看到这个函数返回的是类的成员变量所占内存的大小(是内存对齐后的大小,结构体内存对齐的规则是结构体总大小必须是结构体中最大成员所占内存大小的倍数),因此获得的结果是8.malloc_size()
函数返回的是系统实际分配的内存大小,是16个字节,可是实际使用的只有8个字节。因此,一个NSObject对象所占用的内存是16个字节
。为何会分配16个字节呢?咱们能够去objc4源码
看看alloc
方法(在NSObject.mm
文件中)的调用流程:app
alloc
-->_objc_rootAlloc()
-->callAlloc()
-->class_createInstance()
-->_class_createInstanceFromZone()
。函数
在_class_createInstanceFromZone()
函数中调用了instanceSize()
来肯定一个对象要分配的内存的大小,其函数实现以下:布局
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
复制代码
能够看到给一个对象分配内存的大小最小为16(这是系统硬性规定)。另外要注意的是一个实例对象占用多少内存和类中是否有方法是没有关系的,由于类中的方法并不存放在实例对象中。性能
咱们先定义一个继承自NSObject
的Student
类,Student
类声明了2个int类型属性:测试
@interface Student : NSObject
@property (nonatomic , assign) int age;
@property (nonatomic , assign) int height;
@end
复制代码
咱们将其转换为c/c++
代码查看其底层结构:ui
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
int height;
};
struct NSObject_IMPL {
Class isa;
};
复制代码
Student_IMPL
结构体的第一个成员就是其父类的结构体,从上面咱们能够获得2个信息:子类的成员变量包含了父类的成员变量;父类成员变量放在子类成员变量的前面。因此Student
类有3个成员变量:isa(8字节)、age(4字节)和height(4字节)。那一个Student
的实例对象是否是占16个字节呢?下面咱们测试一下:
Student *stu = [[Student alloc] init];
NSLog(@"%zd",malloc_size((__bridge const void *)(stu)));
// ***************打印结果***************
2020-01-03 17:36:47.773438+0800 CommandLine[62909:6064600] 16
复制代码
若是此时咱们在Student
类中新增一个int类型的属性weight,那一个Student
的实例对象是否是就占20个字节呢?测试发现结果并非20,而是32
。为何会这样呢,这就涉及到了iOS的内存字节对齐,其结果就是系统给一个对象分配的内存大小都是16的倍数。因此系统给一个自定义类的实例对象分配内存时,先计算类的全部成员变量(包括父类以及整个继承链的成员变量)的大小size,若是size恰好是16的倍数,那分配的内存的大小就是size;若是size不是16的倍数,那就将size补齐到恰好是16的倍数为止,补齐后的结果就是实际分配的内存大小。(结构体内存对齐字节数是8,OC对象内存对齐字节数是16,有关iOS系统分配内存时的对齐规则能够查看libmalloc
库中的malloc.c
文件中的malloc_zone_calloc()
函数)。
为何要进行内存对齐
呢?简单来讲就是未对齐的数据会大大下降CPU的性能。由于CPU读取数据时不是一个字节一个字节进行读取的,而是每次读取一块数据,块的大小在不一样的系统上是不同的,能够是二、四、八、16个字节。好比说若是CPU一次读取16个字节,若是一个对象占用内存的大小不是16的倍数,那么CPU读取这个对象数据时就须要作一些额外的操做,影响CPU的性能。
前面提到的对象都是实例对象,OC中除了实例对象
以外,还有另外两种对象:类对象
和元类对象
。
实例对象就是经过类alloc出来的对象,好比Student *student = [[Student alloc] init];
,这样就建立了Student类的一个实例对象,每次调用alloc都会产生新的实例对象。
一个实例对象在内存中存储的信息前面也提到了,它的内存结构是比较简单的,就只存了实例对象的全部成员变量的数据:
OC中每一个类都有一个与之对应的类对象,并且有且只有一个类对象
。与实例对象相比,类对象的内存结构要复杂不少(关于类的底层数据结构后面再作介绍),其在内存中存储的信息主要包括:
-
开头的方法)信息获取类对象的方法有多种,无论哪种方法获取到的类对象都是同样的。
Student *stu = [[Student alloc] init];
// 1. 调用实例对象的class方法来获取
Class stuClass1 = [stu class];
// 2. 调用类的class方法来获取
Class stuClass2 = [Student class];
// 3. 调用runtime的object_getClass(object1);
Class stuClass3 = object_getClass(stu);
复制代码
注意第3种方法不要写错了,runtime中还有另一个很类似的函数:
// 上面用的是这个函数,传入一个,返回这个对象所属的类
Class object_getClass(id _Nullable obj);
// 这个方法是传入一个字符串,返回类名是这个字符串的类
Class objc_getClass(const char * _Nonnull name);
复制代码
从上面介绍咱们能够看出,类对象是用来存储实例对象的信息的(好比实例方法、属性等信息),那类对象的信息(好比类的类方法信息)又是存在哪里呢?这就是咱们要介绍的元类对象
。
每一个类在内存中有且只有一个元类对象
,元类对象和类对象的内存结构是同样的,只是具体存储的信息不一样,用途也不一样。元类对象存储的信息主要包括:
+
开头的方法)信息获取元类对象也是调用object_getClass()
函数,只是传入参数是类对象。换句话说object_getClass()
函数传入的是实例对象的话就返回类对象,传入的是类对象的话就返回元类对象。
// 获取元类对象
Class metaClass = object_getClass([Student class]);
// 判断某个对象是不是元类对象
BOOL isMetaClass = class_isMetaClass([Student class]);
复制代码
这里要注意[[Student class] class]
这种写法,[Student class]
返回的是类对象,那类对象再调用class方法是否是就返回的是元类对象呢?答案是否认的,一个类对象调用class方法返回的就是它自身。
从前面介绍能够看出,全部继承自NSObject的对象都有isa指针,全部类对象和元类对象都有superclass
指针。那这两种指针到底有什么用呢?
咱们首先来了解一下OC的方法调用原理,这属于runtime的知识,这里只是简单介绍一下,不作深刻讲解。调用OC方法底层是经过c语言的发送消息机制来实现的,好比一个实例对象stu调用study方法[stu study]
,其底层就是给stu对象发送消息(objc_msgSend(stu, @selector(study))
)。可是study方法的相关信息并非存储在实例对象中,而是在类对象中,那实例对象如何查找到study方法呢?这里isa指针就起做用了,实例对象的isa指针就是指向实例所属的类对象的(严格来讲,isa指针并非一个普通的指针,它里面存储的信息除了类对象的地址外,还包括不少其余信息,这里不作深刻讲解,咱们简单理解为实例对象的isa就是指向类对象便可)。
实例对象经过isa指针找到了类对象,而后在类对象中查找study方法并执行。可是若是study方法是Student
的父类实现的,那么在Student
类中是找不到study方法的,此时就要根据superclass指针
找到父类对象(superclass指针存储的就是父类的地址,这和isa指针是不同的),若是父类也找不到那就继续沿着继承链进行查找。若是一直找到NSObject基类都没找到的话,就会抛出unrecognized selector
异常(这里不考虑runtime的消息转发)。
对于类方法的调用也是同样的流程,只不过是从给实例对象发消息变成了给类对象发消息。类对象会根据本身的isa指针找到元类对象,而后在元类对象中查找类方法,查找不到也是根据元类的superclass指针沿着继承链查找。
isa指针和superclass指针的指向能够总结为下面一张图:
类的父类
的元类(有点绕);super_class
指针指向的是根类(NSObject)。最后一点要格外注意,举个例子,若是一个以NSObject为基类的类MyClss
,MyClass中声明了一个类方法+(void)myTest;
,可是并无实现这个类方法(整个继承链上都没有实现),若是咱们调用[MyClass myTest]
的话是会报unrecognized selector
异常的。
可是,若是咱们给NSObject添加一个分类,在分类中实现了一个实例方法-(void)myTest;
,此时再调用[MyClass myTest]
的话时能正常运行的,并且执行的就是分类中添加的实例方法-(void)myTest;
。这个其实能够用上面那张图进行解释:首先MyClass类对象会根据其isa指针找到其元类对象,而后在元类对象和元类的继承链上进行查找,一直查找到根元类对象都没有找到一个名叫myTest
的方法,而后跟元类又会沿着其superclass指针找到NSObject类对象,而NSObject类对象中恰好有个叫myTest
的方法,因此就直接执行这个方法。
前面已经提过,不论是类对象仍是元类对象,它们在内存中的存储结构是同样的。相关信息在objc4
源码中。下面我会列出一些关键信息,想要了解完整信息能够去查看源码。
首先咱们来看下objc_class
这个结构体(这是c++语法,结构体能够继承也能够在结构体里面定义函数),这个结构体我只列出了部分信息:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
}
复制代码
可见objc_class
继承自objc_object
,而objc_object
结构体里面就只有isa
这一个成员。咱们再来看看objc_class
里面的内容:
superclass
指针cache
,方法缓存。是cache_t
的结构体,这个结构体的定义能够去看源码。bits
是一个class_data_bits_t
结构体,这个结构体详细信息能够去看源码,这里咱们主要介绍它后面的那个函数bits.data()
,看源码可知,这个函数的实现其实就是bits & FAST_DATA_MASK)
,这个操做就是取出bits的某些位获得的就是一个指向结构体的指针,也就是class_rw_t
这个结构体。下面咱们来看看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; // 属性列表
}
复制代码
咱们发如今class_rw_t
和class_ro_t
都有方法列表、属性列表和协议列表,好比在class_rw_t
中的方法列表是methods
,在class_ro_t
中的方法列表是baseMethodList
,那这两个有什么区别呢?class_ro_t
的初始化是在编译的过程当中完成的,对于一个类对象来讲,编译完成后,class_ro_t
中的baseMethodList
存着实例方法列表,这部份内容是不能够修改的,当class_rw_t
进行初始化时,会先将baseMethodList
拷贝放入methods
中,以后程序运行过程当中动态添加的方法也是存放在methods
中。对于属性列表和协议列表也是同样的。