- 一、OC 转 C/C++
- 二、NSObject 对象内存布局
- 三、NSObject 内存大小
- 四、OC 对象内存布局
- 五、OC 对象内存大小
- 六、对象分类(instance对象、class对象、meta-calss对象)
- 七、isa 和 superClass 指针
- 八、对象属性、方法数据结构
一、OC 转 C/C++
OC 的底层是通过 C\C++ 实现,所以 OC 代码编译过程一般是先将 OC 转为 C\C++ ,C\C++ 进一步转为汇编语言,最终转为机器代码。OC 的对象映射到 C\C++ 主要对应的是结构体,这里面的 “结构体” 并非 C 语言里面的结构体,而是 C++ 语言里面的结构体,而且这个概念仅限字面意思的结构体。严格来讲,其实C++ 中的struct关键字定义的是类,跟 class 关键字定义的类除了默认访问权限的区别,没有区别。C++ 中的 struct 对 C 中的 struct 进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。如:能包含成员函数、可以继承、可以实现多态。
通过 xcrun 命令可以将 OC 代码转为不同平台CPU下支持的 C\C++ 代码,如 OC 代码转为 arm64 架构 CPU 代码,对应的命令为:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
二、NSObject 对象内存布局
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
点击可查看NSObject
定义为如下,可以看出 NSObject
类中包含了一个 isa 成员变量。
@interface NSObject {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
上述代码借助 xcrun
命令生成的文件中包含如下代码,实际上NSObject
的定义最终也是转为如下代码。
//其中 Class 的定义为:typedef struct objc_class *Class; 64位系统中,指针占据 8 个字节
struct NSObject_IMPL {
Class isa; // 8个字节
};
// 可以看出 Class 是一个指针
// 64位系统中,该指针占据 8 个字节
typedef struct objc_class *Class
NSObject *obj = [[NSObject alloc] init];
的内存布局如下。alloc
相当于为右侧蓝色的结构体开辟一块空间,结构体中保存着 isa
成员,isa
成员的指针的地址相当于结构体地址空间,初始化成功后,结构体的地址赋值给 obj
对象,因此 isa
地址和 obj
地址相同。
三、对象内存大小
3.1 查看内存管大小
//#import
NSObject *obj = [[NSObject alloc] init];
// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
class_getInstanceSize
方法可以获取实例对象的成员变量大小,即创建一个实例对象,至少需要多少内存。malloc_size
方法可以获取对象指针所指向内存大小,即创建一个实例对象,实际上分配了多少内存。两个方法的区别具体可以看runtime
底层源码观察其区别。OC 底层源码一般可在该网站查看。
3.2 class_getInstanceSize 函数
class_getInstanceSize
底层实现如下,其中英文注释很清晰的描述了该方法返回的是成员变量(Class's ivar)大小。
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
3.3 alloc 函数
OC 中的alloc
在底层调用 runtime 的allocWithZone
方法:
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;
}
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
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);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
//该处调用了C语言的 calloc 函数开辟空间,所以查看NSObject 对象的创建开辟空间大小应当依次为切入点,查看size 参数来源
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
上述代码调用了C语言的 calloc
函数开辟空间,所以查看NSObject 对象的创建开辟空间大小应当依次为切入点,查看size
参数来源。alignedInstanceSize()
方法是 class_getInstanceSize
方法底层来源,所以下面代码中的extraBytes
变量值实际为 0。非常值得注意的是,下述代码中明确指出CF requires all objects be at least 16 bytes.
,即 CF 对象至少为 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;
}
3.4 小结
综上,系统分配了 16 个字节给 NSObject 对象(通过 malloc_size 函数获得),但 NSObject 对象内部只使用了 8 个字节的空间,这8个字节主要用来存放 isa( 64bit 环境下,可以通过 class_getInstanceSize 函数获得)。
四、OC 对象内存布局
4.1 简单对象
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
}
return 0;
}
利用 xcrun 命名生成 C/C++ 代码,代码中会包含如下部分:
struct Student_IMPL {
Class NSObject_IMP NSObject_IVARS;
int _no;
int _age;
};
其中NSObject_IMP 定义为:
struct NSObject_IMPL {
Class isa;
};
Student_IMP底层进而可转为如下结构,不难看出如果对象之间存在继承关系,最终子类转化对应的结构体会包含父类的结构体成分,且父类结构体成份在前。
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
为了进一步说明OC对象底层是结构体实现的,可以将OC对象强制转为结构体,发现代码依然正常运行。
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
//使用__bridge 将 OC 转为 C
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
结合上述代码,可以总结出Student对象的底层实现结构图:
内存布局大概是这样,stu 指针指向结构体地址,结构体地址即为首个成员变量的地址。
4.2 继承对象
如果是 Person 继承自 NSObject ,Student 继承自 Person 类,Person 中包含 age 成员变量, Student 包含 no 成员变量,则底层实现结构如下图。
表面上看
Person_IMPL
占据 8 + 4 = 12 个字节,但是 OC 中明确指出一个 NSObject 对象大小至少为 16 字节。从内存对齐角度分析来看,Person_IMPL
结构体也至少为 16 字节,即结构体的大小必须是最大成员的倍数,即8 * 2 = 16。
表面上看 Student_IMPL
占据 16 + 4 = 20 个字节实际并非如此,应该为 16 字节。因为 Student_IMPL
中虽然包含Person_IMPL
,但是在继承关系中,Person_IMPL
中 age 对应的内存空间并未被使用,所以应该是 8 + 4 + 4 = 16 字节。
五、OC 对象内存大小
@interface Person : NSObject
{
int _age;
int _height;
int _no;
}
@end
Person *p = [[Person alloc] init];
NSLog(@"%zd %zd",class_getInstanceSize([Person class]), malloc_size((__bridge const void *)(p))); // 24 32
p 对象对应的结构体为:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
int _no;
}; // 计算结构体大小,内存对齐,24
上述代码打印结果分别为 24 和 32,按照前面所说的结构体内存对齐原则来说,打印结果应该都为 24,即 8 的最小倍数, 8 * 3 = 24。class_getInstanceSize
为结构体内存大小 8 + 4 + 4 + 4 = 24,但和实际情况确有一些出入。
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;
}
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
结合 3.3 小结的源码来分析来看,class_getInstanceSize
和alloc
方法最终本质都是调用了alignedInstanceSize
函数,其中alloc
调用alignedInstanceSize
函数中间使用到了 C 语言的 calloc
函数,并且还涉及到一个变量extraBytes
,但是此变量的值一般为 0,所以综合来看 malloc_size((__bridge const void *)(p))
的值为 32 和calloc
密不可分。因为在calloc
函数中存在内存对齐一说(注意此内存对齐不等同于结构体内存对齐),即在堆区每次分配内存为 16 的倍数。如果比较感兴趣,可进一步查看 calloc
函数源码 libmalloc。
六、对象分类(instance对象、class对象、meta-calss对象)
6.1 OC 对象分为三类
- instance 对象(实例对象):内部包含成员变量的具体值、isa 指针
- class 对象(类对象):内部包含对象方法、属性、成员变量、协议信息、isa 指针、superclass 指针
- meta-class 对象(元类对象,即描述对象的对象,元数据是指描述数据的数据):内部包含类方法、isa 指针、superclass 指针。
无论是instance、class、meta-class 底层本质上都是 OC 对象。
6.2 类对象和元类对象获取方式
// 获取类对象的方式
[NSObject class];
[obj class];
// 获取元类对象,
// 传入 instance 返回 class 对象
// 传入 class 返回 meta-class 对象
// 传入 meta-class 返回 NSObject 的 meta-class 对象
Class metaClass= object_getClass([NSObject class]);
// 判断是否为元类对象
class_isMetaClass([NSObject class]);
七、isa 和 superclass 指针
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用。
当Student的class对象要调用Person的class方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到class方法的实现进行调用。
instance的isa指向class;class的isa指向meta-class;meta-class的isa指向基类的meta-class。
class的superclass指向父类的class,如果没有父类,superclass指针为nil。meta-class的superclass指向父类的meta-class,基类(Root)的meta-class的superclass指向基类的class
(即类方法找不到时会调用对象方向,对象方法找不到才会报错)。
instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类。class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类。
相关面试题
为什么 instance、class、meta-class 对象中都包含 isa 指针成员变量?
因为所有的 OC 类都继承自 NSObject。
对象的isa指针指向聊里?
instance对象的isa指向class对象
class对象的isa指向meta-class对象
meta-class对象的isa指向基受的meta- class对象
0C的类信息存故在哪里?
0对象方法、属性、成员变量、协议信息,存故在class对象中
0类方法,存故在meta-class对象中
0成员变量的具体值,存故在instance对象
八、对象属性、方法数据结构
无论是class对象、meta-class对象,其底层数据结构都和上图保持一致。
bits & FAST_DATA_MASK 可以获取到对应的类信息,类信息中包含方法列表、属性列表、协议列表等信息。