最近准备找工作,在网上和面试中收集了很多面试题。利用一两个月时间做出整理和解答,目的是巩固自己的基础知识以及掌握这些问题该如何去回答。
本篇文章要回答的问题
- OC 对象的实现机制
- 一个 NSObject 对象占用多少内存?
- 创建一个对象的时候,比如调用 alloc 方法,系统怎么知道应该分配多大空间的?
- 一个 OC 对象的的存储空间是怎样的?或者说对象数据分别是存在哪里的?
- 对象的 isa 指针指向哪里?
- 你使用过 runtime 中的哪些方法?
1. OC 对象的实现机制
答:Objective-C的对象都是基于C/C++的结构体来实现的。
Objective-C 代码底层实现其实都是C/C++代码,
C/C++代码又会被编译器转成汇编代码,
最终汇编代码会被转成机器语言运行在我们的设备上。
思考:我们有没有什么办法来验证我们这个答案呢?
既然 Objective-C 代码是被编译器转成 C/C++ 代码的,
那么我们就可以使用编译器工具将它转换成功,
看看它转成C/C++之后究竟是什么样子!
我们使用Xcode自带的clang编译器来进行转换:
在终端进入将被转换成C/C++文件的.m文件所在的目录,执行命令(图一):
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
现在就可以在当前目录查看到.cpp文件了(图二),在Xcode中打开它即可查看C/C++代码。
我们在这个文件中搜索 NSObject,可以找到这样一段代码:(图三)
struct NSObject_IMPL {
Class isa;
}
这就是 NSObject Implementation,就是一个C/C++的结构体!
复制代码
图一
图二 图三2. 一个 NSObject 对象占用多少内存?
结论:
系统分配给了 NSObject 对象 16 个字节的空间,但实际上它真正利用的空间只有 8 个字节。(64bit)
分析:
上面我们已经知道了一个 NSObject 对象实际上就是一个 C/C++ 的结构体,并且这个结构体里面只有一个 isa,我们可以看到它的类型是 Class。那么现在的问题就是这个 Class 类型是什么,它占多少内存?
我们在 Xcode 中查看 NSObject.h 文件。
按住 command + control点击 Class。 在这里我们可以看到 Class 其实是一个指针。指针在64位机器中占用的是8个字节,在32位机器中占用的是4个字节。这里我们仅考虑64位机器。
到这里答案就已经浮出水面了:因为 isa 要占用8个字节,所以 NSObject_IMPL 结构体需要占用8个字节,所以一个 NSObject 对象在内存中需要占用8个字节的空间,注意我这里的措辞,是需要占用8个字节,而不是说它就占用了8个字节,接下来会继续说明。
现在我们在.m文件中编写这么一段代码:
NSObject *obj = [[NSObject alloc] init];
复制代码
我们现在要解决的问题就是: obj 这个指针指向的那一块的内存占用的字节数是多少。
我们将两个函数来探究这个问题,这两个函数看起来像是获取对象的内存大小:
class_getInstanceSize()
malloc_size()
继续编写代码:
#import <objc/runtime.h>
#import <malloc/malloc.h>
NSObject *obj = [[NSObject alloc] init];
NSLog(@"class_getInstanceSize - %zd", class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size - %zd", malloc_size((__bridge const void *)(obj)));
复制代码
运行之后可以看到输出的结果:
很奇怪,一个是8,一个是16。这两个函数看起来都是用来获取 NSObject 对象的内存大小的,为什么会不一样呢?我们要探究一下class_getInstanceSize()
的内部究竟是如何实现的才能解答这个问题。
先在苹果的开源网站 https://opensource.apple.com/tarballs/objc4/ 下载最新的 OC 底层源码(数字最大就是最新的),解压后用 Xcode 打开项目,全局搜索class_getInstanceSize
,找到它的实现方法:
alignedInstanceSize
可以看到
alignedInstanceSize
的实现,它上面写了一句注释说明这个方法返回的是类的ivar(成员变量)的大小。我们上面已经确定了 NSObject 转成的 C/C++ 结构体中的成员变量 isa 占的字节数是8,这也与控制台 log 的数据 8 是吻合的。 再根据函数的名称
word_align
,我们可以得出这样的结论:
[6. 你使用过 runtime 中的哪些方法?] class_getInstanceSize() 方法的作用是获取类的实例对象的成员变量内存对齐后所占用的大小。(接下来会用到很多runtime的方法,每个方法前面我都会用中括号打上标记,解答这道面试题的时候就可以搜索这个标记。) 关于内存对齐稍后会有简要说明。
malloc_size
是一个C语言函数,它的作用是获得指针所指向的内存的大小。
所以 obj 所指向的内存占用的字节数是16,但是实际上它真正用到的字节只有8个。
16 个字节是系统分配的,为什么系统会分配16个字节给它呢,下个问题中的分析中会有详细的解释。
我们对这个问题做一下延伸,创建一个 Person 类,Person 实例对象占用多少内存空间,以下代码输出什么?
我们可以先将代码转成C/C++代码,查看 Person 的底层实现: 我们可以知道NSObject_IVARS
占8个字节,
_age
占4个字节。 根据
结构体内存对齐规则,我们可以得出
Person_IMPL
结构体在对齐后占用16个字节,所以
class_getInstanceSize([Person class])
为16。运行后发现打印的两个值都是16。
再给 Person 添加两个成员变量
运行后发现打印的值分别是24,32。前面我们的指示完全可以解释第一个值为什么是24,现在我们还无法理解为什么系统会给 Person 对象分配32个字节。这里还需要再补充一点知识:操作系统对内存分配都有一定的规则,在iOS中,系统给对象分配的内存的大小必须是16的整数倍。
根据这个例子来说明,我们计算出来Person对象实际需要8(nsobject_ivars)+4+4+1一共17个字节,进行内存对齐之后它需要24个字节,再经过iOS操作系统管理之后,分配给它了32个字节。
看完上边的内容之后,我们可以对简单的内存计算问题进行处理了。还有更为复杂的问题,比如继承关系再复杂一些、结构体嵌套、成员变量的类型更多一些、成员变量的顺序打乱,遇到这些复杂的情况,需要根据结构体内存对齐规则去一点点地计算对齐后的内存大小,再根据iOS的规则去计算系统实际分配的大小。
3. 创建一个对象的时候,比如调用 alloc 方法,系统怎么知道应该分配多大空间的?
解答:
系统通过底层的instanceSize()
函数计算应该分配的空间。
分析:
解答这个问题必须阅读源码,看看 alloc 究竟是如何实现的。
在刚刚下载的源码中搜索 alloc,找到 alloc 的实现:
_objc_rootAlloc
:
按照同样的方法继续点,最终会来到
_class_createInstanceFromZone
这里:
这段代码中有一个
size
,这个 size 就是系统要分配的空间的大小。
我们可以看到:
size_t size = cls->instanceSize(extraBytes);
复制代码
在点的过程中我们也可以看到 extraBytes 这个参数的值是0。
我们再往里面继续看 instacneSize()
的实现
instanceSize()
函数来计算内存,这里也解释了上个问题的疑问。
4. 一个 OC 对象的的存储空间是怎样的?或者说对象数据分别是存在哪里的?
答案:
instance 对象存储的是 isa 指针、其它成员变量; class 对象存储的是 isa 指针、superclass 指针、类的属性信息、类的对象方法信息、类的协议信息、类的成员变量信息; meta-class 对象存储的是 isa 指针、superclass指针、类的类方法信息分析:
Objective-C 中的对象分为三种:
- instance 对象 (实例对象)
- class 对象 (类对象)
- meta-class (元类对象)
instance 对象
instance 对象就是通过类 alloc 出来的对象,每次调用 alloc 都会产生新的 instance 对象。
instance 对象在内存中存储的信息包括
- isa 指针
- 其它成员变量
class 对象
每个类在内存都有且只有一个 class 对象。
class 对象在内存中存储的信息包括
- isa 指针
- superclass 指针
- 类的属性信息(@property)、类的对象方法信息(instace method)
- 类的协议信息(protocol)、类的成员变量信息(ivar)(这里的成员变量信息指的不是成员变量的值,是成员变量的描述信息,比如类型,名称等)
获取 class 对象的方式有三种:
Class objectClass = [NSObject class];
Class objectClass = [obj class];
Class objectClass = object_getClass(obj); //Runtime API
如果在 Xcode 中打印这三个对象的地址,你会发现他们都是同一个地址,这也就说明了 class 对象在内存中有且只有一份。
meta-class 对象
用于描述类相关的一些东西。每个类在内存中有且只有一个 meta-class 对象。
获取 meta-class 对象: Class objectMetaClass = object_getClass([NSObject class]);
meta-class 对象和 class 对象的内存结构是一样的(它们的类型都是 Class 类型),但是用途不一样。meta-class 对象在内存中存储的信息包括:
- isa 指针
- superclass 指针
- 类的类方法信息(class method)
这里介绍一下object_getClass()
的作用:
[6. 你使用过 runtime 中的哪些方法?] object_getClass() 返回参数的 isa 指针指向的对象。
通过查看源码,我们就可以不难发现这点。关于 isa 指针,在下部分会有详细介绍。
5. 对象的 isa 指针指向哪里?
答案:
- instance 对象的 isa 指向 class 对象
- class 对象的 isa 指向 meta-class 对象
- meta-class 对象的 isa 指向基类的 meta-class 对象
分析:
- intance 的 isa 指向 class
- class 的 isa 指向 meta-class
- meta-class 的 isa 指向基类的 meta-class
- class 的 superclass 指向父类的 class。如果没有父类,superclass 为 nil
- meta-class 的 superclass指向父类的 meta-class。基类的 meta-class 的 superclass 指向基类的 class
- instance 调用对象方法的轨迹:通过 instance 的 isa 找到 class,如果 class 的方法列表中没有这个方法,就通过 class 的 superclass 找到父类的 class
- class 调用类方法的轨迹:通过 class 的 isa 找到 meta-class,如果 meta-class 的方法列表中没有这个方法,就通过 meta-class 的 superclass 找到父类的 meta-class