OC对象的本质
平常我们使用Objective-C语法来编写代码,但是它的底层其实都是C或C++代码。Objective-C其实是在C语言的基础上增加了面向对象的特性。我们可以通过以下命令将Objective-C代码转换成C++代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC文件 -o 输出目标cpp文件
如果OC文件需要链接其它的框架,可以使用-framework参数:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC文件 -o 输出目标cpp文件 -framework 框架名称
与此同时,还需要下载runtime的源码,通过objc源码地址下载最新版本的objc源码,以便于后续使用。
OC对象的底层实现
在开发过程中,最常用到的就是OC的对象。几乎所有的类对象都是NSObject的子类,但是抛开OC的限制,NSObject底层是如何实现的呢?上文说到,所有的OC代码最后都会转换成C代码,所以我们通过一个例子来认识NSObject的底层实现。
- 首先,创建一个XLPerson对象
@interface XLPerson : NSObject
@end
@implementation XLPerson
@end
- 进入XLPerson.m文件所在目录,使用如下指令,将XLPerson.m文件转换成XLPerson_cpp.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson.m -o XLPerson_cpp.cpp
- 在生成的XLPerson_cpp.cpp文件中搜索XLPerson,就能找到以下结构体定义
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
在XLPerson_IMPL中包含一个结构体成员NSObject_IVARS,它是NSObject_IMPL类型,查看NSObject_IMPL的代码如下:
struct NSObject_IMPL {
Class isa;
};
由此可以看出,OC中的对象其实就是通过结构体来实现的。在NSObject_IMPL包含了一个Class类型的成员isa。继续查看Class的定义:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
可以发现其实Class就是一个objc_class类型的结构体指针。在最新的objc4的源码中的objc-runtime-new.h文件中,可以找到最新的objc_class的定义如下:
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指针
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
由于继承关系,结构体objc_class自然也就继承了objc_object的isa指针,所以objc_class也可以转换成如下写法:
struct objc_class {
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();
}
}
- isa是继承自objc_object的属性(具体作用后文会说明)
- superclass表示当前类的父类
- cache则代表方法缓存。
- bits是class_data_bits_t类型的属性,用来存放类的具体信息。
查看class_data_bits_t的具体实现如下:
//此处只列出核心的代码
struct class_data_bits_t {
......
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
......
}
这时候发现了通过bits的内部函数data()可以拿到class_rw_t类型的数据,查看class_rw_t的源码如下:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //只读的属性ro
method_array_t methods; //方法列表
property_array_t properties; //属性列表
protocol_array_t protocols; //协议列表
Class firstSubclass;
Class nextSiblingClass;
}
在结构体class_rw_t中存放着
- 方法列表methods
- 属性列表properties
- 协议列表protocols。
- 一个class_ro_t类型的只读变量ro
继续查看class_ro_t的源码如下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //当前instance对象占用内存的大小
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_ro_t中存放着类最原始的方法列表,属性列表等等,这些在编译期就已经生成了,而且它是只读的,在运行期无法修改。而class_rw_t不仅包含了编译器生成的方法列表、属性列表,还包含了运行时动态生成的方法和属性。它是可读可写的。至于class_rw_t和class_ro_t更深层次的区别,我会放在介绍runtime的时候详细说明。
OC对象的内存分配
allocWithZone
在iOS中一般使用如下[[NSObject alloc] init]创建对象,其中[NSObject alloc]就是为NSObject分配内存空间,下面,我们就从源码入手,来理解OC对象是如何分配内存的。
- [NSObject alloc]其实是调用allocWithZone方法来分配内存空间,所以我们查看objc源码中的NSObject.mm文件。找到_objc_rootAllocWithZone函数:
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
- 找到其中分配内存的方法class_createInstance(cls, 0);
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
- 找到_class_createInstanceFromZone(cls, extraBytes, nil)方法,由于方法较长,此处只展示核心代码:
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
......
}
可以看出,其中真正用来分配内存的是C函数calloc,calloc函数传入了两个参数,第一个参数表示对象的个数,第二个参数size表示对象占据的内存字节数。因此size就表示当前对象所需要的内存大小。
- 这里的size变量表示当前对象所占用的内存大小,可以查看cls->instanceSize(extraBytes)属性内部实现,如下:
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
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;
}
其中cls->unalignedInstanceSize()表示未进行内存对齐的内存大小,cls->alignedInstanceSize()是对未对齐的内存进行内存对齐操作,得到最终所需的内存大小。
这里有个细节,就是执行对齐操作得到的内存大小如果小于16个字节,那么最后分配的内存大小为16个字节,也就是说,我们创建对象时,分配的内存最少是16个字节。
OC对象的内存分配
获取内存大小的方法?
在iOS中,我们可以通过三种方式来获取一个对象的内存大小。
sizeof
sizeof,它其实不是一个函数,而是一个运算符,它和宏定义类似,在编译期就将传入的类型转换成具体的占用内存的大小。例如int是4个字节,那么sizeof(int)在编译期就会直接被替换成4
注意:sizeof需要传入一个类型过去,它返回的是一个类型所占用的内存空间
class_getInstanceSize
class_getInstanceSize(Class _Nullable cls),传入一个Class类型的对象就能得到当前Class所占用的内存大小。例如,class_getInstanceSize([NSObject class]),最后返回的是8,也就说明NSObject对象在内存中占用8个字节,而且由于NSObject最后会转化成结构体NSObject_IMPL,而且内部只有一个isa指针,所以也就可以理解为isa指针占用8个字节的存储空间。
class_getInstanceSize函数内部其实就是调用alignedInstanceSize函数获取到对象所需要的真实内存大小。
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
在调用calloc函数进行内存分配的时候,是将alignedInstanceSize的值当作参数赋值给calloc函数,因此calloc函数可以有如下写法:
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
......
size_t size = class_getInstanceSize(cls);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
......
}
由此可以看出,class_getInstanceSize(Class _Nullable cls)所返回的其实是对象实际所需要的内存大小。
malloc_size
malloc_size(const void *ptr)函数,传入const void *类型的参数,就可以获取到当前操作系统所分配的内存大小。例如:还是利用NSObject来进行测试,malloc_size((__bridge const void *)([[NSObject alloc] init])),将NSObject类型的实例对象作为参数,最后得到的值为16,和我们之前使用class_getInstanceSize([NSObject class])得到的8不相同。
这是因为在iOS中,在分配内存时,如果对象所需要的内存大小小于16个字节,那么就分配给这个对象16个字节的内存空间。也就是每个对象至少分配16个字节的内存空间
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;
}
三种获取内存方法对比
- 首先,创建对象XLPerson,添加3个属性,如下
@interface XLPerson : NSObject{
int _height;
int _age;
long _num;
}
- 在main函数中创建XLPerson实例对象,并且使用上面的三种方式分别获取XLPerson类的内存大小:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface XLPerson : NSObject{
@public
int _height;
int _age;
long _num;
}
@end
@implementation XLPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p = [[XLPerson alloc] init];
p->_height = 10;
p->_age = 20;
p->_num = 25;
NSLog(@"sizeof --> %lu", sizeof(p));
NSLog(@"class_getInstanceSize --> %lu", class_getInstanceSize([XLPerson class]));
NSLog(@"malloc_size --> %lu", malloc_size((__bridge const void *)(p)));
}
return 0;
}
- 运行程序,打印出结果如下:
可以看出此时sizeof(p)返回8个字节,class_getInstanceSize返回24个字节,malloc_size则返回32个字节。3个方法返回的内存大小都不一样,这是为什么呢?
sizeof(p)为什么只返回8个字节呢?
其实sizeof(p)返回8个字节,这个很好理解,因为sizeof传入的是p,而p在此处表示的是一个指向XLPerson实例对象的一个指针,在iOS中,指针类型所占用的内存大小为8个字节。因此sizeof(p)所返回的并不是XLPerson对象的内存大小。
要想使用sizeof获取到XLPerson对象的内存大小,就需要知道XLPerson最终会转换成什么类型。通过上文的学习,我们知道,XLPerson内部其实是一个结构体,通过xcrun指令将文件转换成.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
分析main.cpp文件可以得出,XLPerson最终会转换成如下结构体类型
struct NSObject_IMPL {
Class isa;
};
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _height;
int _age;
long _num;
};
此时调用sizeof(struct XLPerson_IMPL)就可以得出struct XLPerson_IMPL类型所占用的内存为24字节,其实也就是XLPerson所占用的内存是24个字节。
由此可看出运算符sizeof(struct XLPerson_IMPL)和函数class_getInstanceSize([XLPerson class]返回的是对象真正所需要的内存大小。
为何malloc_size返回的内存大小和对象实际所需内存不同?
在了解malloc_size函数之前,我们先来分析一下XLPerson内部结构体所需要的真实内存大小。
struct XLPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _height;
int _age;
long _num;
};
- 首先我们知道NSObject_IMPL内部其实只有一个isa指针,因此它所占用的内存是8个字节
- int类型的变量_height占用4个字节
- int类型的变量_age占用4个字节
- long类型的变量_num占用8个字节
所以,单纯从结构体层面分析的话,我们可以看出XLPerson_IMPL结构体所需要的内存是24个字节,这和上文的sizeof(struct XLPerson_IMPL)以及函数class_getInstanceSize([XLPerson class]返回的内存大小一致。由此可见,XLPerson所需要的内存就是24个字节。
可是为什么malloc_size所返回的内存大小确是32个字节呢?这就要说到内存对齐操作。
结构体的内存对齐操作
首先,我们先将上文中提到的XLPerson属性进行修改,去掉其中的_age属性
@interface XLPerson : NSObject{
@public
int _height;
long _num;
}
@end
然后重新运行项目,可以看到XLPerson实际占用的内存还是24个字节,而通过分析我们可以发现XLPerson只需要20个字节的内存空间。
这就是结构体内存对齐操作所导致的,也就是上文中所说的alignedInstanceSize函数的作用。那么什么是结构体的内存对齐操作?
结构体不像数组,结构体中可以存放不同类型的数据,它的大小也不是简单的各个数据成员大小之和,限于读取内存的要求,而是每个成员在内存中的存储都要按照一定偏移量来存储,根据类型的不同,每个成员都要按照一定的对齐数进行对齐存储,最后整个结构体的大小也要按照一定的对齐数进行对齐。
结构体的内存对齐规则如下:
- 第一个成员的首地址为0
- 每个成员的首地址是自身大小的整数倍
- 结构体的内存总大小是其成员中所含最大类型的整数倍
这就是为何XLPerson的内存大小为24个字节的原因。
iOS系统的内存对齐操作
既然XLPerson的内存占用为24个字节,那么为什么系统会给它分配32个字节呢?其实在iOS系统中也存在内存对齐操作。
我们可以通过打印内存信息来查看是否分配了32个字节,依旧是使用上面的例子
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p = [[XLPerson alloc] init];
p->_height = 1;
p->_num = 3;
}
return 0;
}
- 断点状态下,使用po p获取到p的内存地址为0x1005b4fd0
(lldb) po p
<XLPerson: 0x1005b4fd0>
- 打开Xcode下Debug->Debug Workflow->View Memory,输入刚刚获取到的内存地址,可以得到对象p的内存分配情况
其中前8个字节存储着isa指针,蓝色框中的四个字节存放着_height=1,而绿色框中的8个字节存放着_num=3,这里因为结构体内存对齐原则,所以_num=3的内存地址从第17个字节开始,整个红色框的32个字节,就是系统分配给XLPerson实例对象的内存空间,这也证明了malloc_size((__bridge const void *)(p))返回的确实是系统分配给p对象的内存空间。
OC对象的分类
OC对象主要分为3种
- instance实例对象
- class类对象
- mata-class元类对象
instance对象
instance对象就是通过alloc操作创建出来的对象,每次调用alloc操作都会创建出不同的instance对象,它们拥有各自独立分配的内存空间。例如上文中使用的XLPerson的实例对象
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
其中p1、p2就是实例对象,在内存中可以同时拥有多个同一类对象的实例对象。它们各自拥有一块内存空间,用来存储独有的信息。实例对象内部存放的内容如下(以XLPerson的实例对象为例):
XLPerson *p1 = [[XLPerson alloc] init];
p->_height = 10;
p->_num = 25;
- isa指针,指向它的类对象
- _height = 10
- _num = 25
因为通过[XLPerson alloc]就能创建一个实例对象,所以每个实例对象内部会存放着一个isa指针,指向它的类对象,还存放着定义好的其它的成员变量的具体值。
class类对象
类对象是将具有相似属性和方法的对象抽象出来,从而形成类对象。它可以定义一些相似的方法和属性,不同的实例对象去引用类对象的属性或者方法,能减少代码的重复率。
运行如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Class c1 = [XLPerson class];
Class c2 = [p1 class];
Class c3 = [p2 class];
Class c4 = object_getClass(p1);
Class c5 = object_getClass(p2);
NSLog(@"\n c1 -> %p,\n c2 -> %p,\n c3 -> %p,\n c4 -> %p,\n c5 -> %p", c1, c2, c3, c4, c5);
}
return 0;
}
可以得到结果为:
通过结果可以发现,所有的class对象的内存地址都是相同的,这也就说明在内存中只有一个class对象,不管是使用上面的哪种方法获取到的class对象都是同一个。
class对象内部其实就是一个object_class的结构体,具体的结构定义在上文已经介绍过,这里只列举出class对象存储的主要信息:
- isa指针
- superClass
- 属性信息(properties),存放着属性的名称,属性的类型等等,这些信息在内存中只需要存放一份
- 对象方法信息(methods)
- 协议信息(protocols)
- 成员变量信息等等(ivars)
mata-class元类对象
元类其实也是一个class类型的对象,它内部的结构和类对象一致,但是元类对象中只存放了如下信息:
- isa指针
- superClass
- 类方法信息(class method)
元类和类一样,在内存中只会存在一个元类对象。可以通过runtime的方法获取元类对象
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Class c1 = object_getClass(p1);
Class c2 = object_getClass(p2);
Class mataC1 = object_getClass(c1);
Class mataC2 = object_getClass(c2);
BOOL c1_isMataClass = class_isMetaClass(c1);
BOOL c2_isMataClass = class_isMetaClass(c2);
BOOL mataC1_isMataClass = class_isMetaClass(mataC1);
BOOL mataC2_isMataClass = class_isMetaClass(mataC2);
NSLog(@"\n c1_isMataClass:%d,\n c2_isMataClass:%d,\n mataC1_isMataClass:%d,\n mataC2_isMataClass:%d"
,c1_isMataClass, c2_isMataClass, mataC1_isMataClass, mataC2_isMataClass);
NSLog(@"\n c1 -> %p,\n c2 -> %p,\n mataC1 -> %p,\n mataC2 -> %p",
c1, c2, mataC1, mataC2);
}
return 0;
}
调用结果如下:
在上图中,c1和c2都是类对象,所以返回0,mataC1和mataC2都是元类对象,所以返回1。同时mataC1和mataC2的内存地址完全相同,这也说明了元类对象在内存中确实只存在一份。
instance对象、class对象和mata-class对象的关系
上文多次提到,在Class对象内部都会有一个isa指针,那么这个isa指针的作用是什么呢?其实isa指针是instance对象、class对象和mata-class对象之间的桥梁。
isa指针的作用
- instance对象的isa指针指向class对象,而且上文也说到,在instance对象中只存储了isa指针和具体的属性的值,当我们调用instance对象的实例方法时,其实是通过isa指针找到它的类对象,在类对象的对象方法列表(methods)中查找到方法的实现,并调用。
- class对象的isa指针指向mata-class对象,当调用类方法时,会通过类对象的isa指针找到它的元类对象mata-class,然后在mata-class的方法列表中找到对应类方法的实现并执行。
superClass指针的作用
superClass其实就是指向class对象或者mata-class对象的父类,下面我们以一个简单的例子来具体说明:
@interface XLPerson : NSObject
- (void)run;
+ (void)sleep;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XLPerson *p1 = [[XLPerson alloc] init];
[p1 run];
[XLPerson sleep];
}
}
class对象中superClass指针的作用
XLPerson继承自NSObject,并且声明了一个类方法sleep()和一个对象方法run(),当p1调用对象方法run()时
- 首先会通过实例对象p1内部的的isa指针找到它的类对象。在类对象的方法列表中查找run()方法。
- 如果类对象的方法列表中没有此方法的实现,那么会通过类对象的superClass指针找到它的父类对象,在此处就是NSObject对象,并且在NSObject对象的方法列表中寻找run()方法的实现,然后调用。
mata-class对象中superClass指针的作用
还是以上面的例子来说明,当XLPerson调用类方法sleep()时
- 首先通过XLPerson的isa指针找到它的元类对象,在元类对象的方法列表中寻找sleep()方法。
- 如果在XLPerson的元类对象中没有找到sleep()方法,那么会通过XLPerson的元类对象的superClass指针找到XLPerson的父类对象的元类对象,此处就是NSObject的元类对象,在NSObject元类对象的方法列表中找到sleep()方法并执行。
isa、superClass总结
首先先看一张非常经典的描述instance对象、类对象以及元类对象之间关系的图片。途中虚线代表isa指针,实线代表superClass指针。
- instance对象的isa指针指向它的class对象
- class对象的isa指针指向它的mata-class对象
- mata-class对象的isa指向基类对象mata-class
- class对象的superClass指向它的父类的class对象,基类的class对象的superClass指向nil
- mata-class的superClass指向父类对象的mata-class,基类对象的mata-class的superClass指向基类对象自身(此处是比较特殊的地方)
- 实例方法查找路线
- 首先会通过isa指针到class对象中找
- 如果找不到,通过superClass到父类的class对象中找
- 如果还找不到,再到基类的class对象中查找
- 类方法的查找路线
- 首先会通过类对象的isa指针到mata-class中查找
- 如果mata-class中找不到,通过superClass到父类的元类对象中查找
- 如果在父类的元类对象中找不到,就到基类对象的元类对象中查找
- 如果基类的元类对象中找不到,那么会到基类对象中查找。
OC对象面试题
一个NSObject对象占用了多少内存?
系统给一个NSObject对象分配了16个字节的内存空间(通过malloc_size函数申请内存),但是NSObject对象内部只有一个isa指针,所以它实际使用到了8个字节的内存,而由于ios的内存对齐原则,系统最少分配16个字节的内存空间。
可以通过class_getInstanceSize函数来获取NSObject占用内存大小
对象的isa指向哪里?有什么作用?
- 实例对象的isa指针指向class对象
- class对象的isa指针指向mata-class对象
- meta-class对象的isa指针指向基类的meta-class对象
OC的类的信息(方法、属性、成员变量等等)分别存放在哪?
- OC中实例对象的方法、属性、成员变量、协议信息等等存放在class对象中
- 类方法存放在mata-class中
- 成员变量的具体值存放在实例对象中,因为成员变量的描述信息比如它的类型是int等等,在内存中只需存储一份,所以将属性描述信息存放在类对象中,但是成员变量的值每个实例变量都不相同,所以每个实例对象存放一份
结束语
以上内容纯属个人理解,如果有什么不对的地方欢迎留言指正。
一起学习,一起进步~~~