OC 对象的本质
一个 NSObject 对象占用多少内存?
一个指针变量所占用的大小(64bit->8个字节,32bit->4个字节)
我们平时编写的Objective-C
代码,底层实现都是C/C++
代码,Objective-C
的面向对象都是基于C/C++
的数据结构实现的。
Objective-C
->C/C++
->汇编语言
->机器语言
如果想研究一些本质问题,最好将Objective-C
代码转化成C/C++
代码,才比较容易分析出来原理。
Objective-C
的对象、类主要是基于C/C++
的什么数据结构实现的?
假设一个Person
类,有下面属性
@interface Person : NSObject
{
int _age;
int _no;
double _height;
NSString *_name;
}
@end
对应C/C++
是以结构体的形式存在的
struct Person {
int _age;
int _no;
double _height;
char *_name;
}
将main.m
中下面代码转成C++
代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%p", obj);
}
return 0;
}
终端输入如下命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
只生成在真机上
arm64
架构下的文件,如果不指定架构,则生成文件包含其它架构,并且文件要比这个大一点。
main.cpp
中的cpp
就是c plus plus
的意思
并且项目中多了一个main-arm64.cpp
文件
将main-arm64.cpp
文件用Xcode
打开后,你会发现,虽然在main.m
中只写了一行代码,转换成C++
的代码就有3w
多行。
在main-arm64.cpp
文件中可以发现在NSObject
的IMPL(Imeplemetation)
方法中只有一个Class isa
struct NSObject_IMPL {
Class isa;
};
我们可以猜想在NSObject
的底层实现,就是一个NSObject_IMPL
。
一个 OC 对象在内存中是如何布局的?
在Foundation
框架中我们可以看到NSObject
类的声明
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end
可以省略成如下格式
@interface NSObject {
Class isa;
}
@end
对比main-arm64.cpp
中NSObject
类的实现
struct NSObject_IMPL {
Class isa;
};
相当于NSObject
对象创建后在内存中就是以结构体的形式存在的。
该结构体中只有一个成员isa
,为指针类型,在64位结构下占用8个字节。
NSObject
对象的声明中除了Class isa;
这个成员变量外,也还有其它的一些方法,只不过不存在于NSObject
对象的存储空间。存处于其它对应的位置。
NSObject *obj = [[NSObject alloc] init];
这句代码做了什么事情?
-
alloc
分配存储空间给NSObject
对象 - 将存储空间的地址值(
isa
的地址值)赋值给obj
指针进行存储。obj
就可以指向NSObject
对象。(obj
指针存储的值就是NSObject
对象中isa
的地址值)
自定义类探究
上面研究了NSObject
对象的本质,那么我们平时工作中创建的对象的本质是什么样的呢?
自定义一个Student
类,直接写在main.m
中。
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
将其转换成C++
代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
我们可以发现在生成的C++
文件内,Student
类的实现是如下格式
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
这其中包含的struct NSObject_IMPL NSObject_IVARS;
是指NSObject
的实现,由于NSObject
的实现里面只有一个Class isa;
,所以NSObject_IMPL
这个结构体占用的内存空间和单独一个isa
占用的内存空间都是8
个字节。
因此,上面的Student_IMPL
结构体等价如下
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
所以,我们创建一个Student
对象需要Class isa(8字节)
+int _no(4字节)
+int _age(4字节)
=16字节
通过结构体访问属性
在main.m
中增加如下代码
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
用结构体去访问属性
Student *student = [[Student alloc] init];
student->_no = 4;
student->_age = 5;
struct Student_IMPL *studentImpl = (__bridge struct Student_IMPL *)student;
NSLog(@"_no = %d, _age = %d", studentImpl->_no, studentImpl->_age);
输出
_no = 4, _age = 5
由此说明,stundet
指针指向的内存确实是Student_IMPL
结构体。
通过运行时获取实例对象占用内存的大小
class_getInstanceSize()
我们同样可以通过上面的运行时方法获取到实例对象占用的内存大小
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zd", class_getInstanceSize([Student class]));
输出结果仍为
NSobject = 8
Student = 16
窥探内存结构
通过Debug
的View Memory
查看
调用Debug
->Debug Workflow
->View Memory
在下图Address
位置输入stundent
对象地址后,我们就可以看到其内存结构
由于现在计算机普遍都是小端模式,计算机读取地址的时候也是从高地址开始读取。因此,成员内存地址分别为
0x001D808001000011E9
0x00000004
0x00000005
通过lldb
查看
先po(print object)
打印出student
的地址
(lldb) po student
<Student: 0x10040fb20>
再进行内存读取memory read (address)
(lldb) memory read 0x10040fb20
0x10040fb20: e9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00 ................
0x10040fb30: d0 fa 40 00 01 00 00 00 10 ee 40 00 01 00 00 00 ..@.......@.....
由此可以看到,和我们用Debug
的View Memory
的内存结构是一样的。
拓展
x
等价于memory read
(lldb) x 0x10040fb20
0x10040fb20: e9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00 ................
0x10040fb30: d0 fa 40 00 01 00 00 00 10 ee 40 00 01 00 00 00 ..@.......@.....
以固定格式读取内存
4
次、4
个字节、16
进制格式读取
(lldb) x/4xw 0x10040fb20
0x10040fb20: 0x000011e9 0x001d8001 0x00000004 0x00000005
-
print
、p
:打印 -
po
:打印对象
读取内存
-
memory read
/数量、格式、字节大小 内存地址 -
x
/数量、格式、字节大小 内存地址 - 例如 :
x/4xw 0x10040fb20
格式
- x
-
16
进制
-
- f
- 浮点
- d
-
10
进制
-
字节大小
-
b
-
byte
1
字节
-
-
h
-
half word
2
字节
-
-
w
-
word
4
字节
-
-
g
-
giant word
8
字节
-
修改内存中的值
memory write 0x1000000
比如我们想要修改Student
类中_no = 4;
的值
先查看内存地址
(lldb) x/16xb 0x10040fb20
0x10040fb20: 0xe9 0x11 0x00 0x00 0x01 0x80 0x1d 0x00
0x10040fb28: 0x04 0x00 0x00 0x00 0x05 0x00 0x00 0x00
注意此时0x10040fb28
地址中第一个字节的值为4
,下面进行修改
memory write 0x10040fb28 6
可以看到,_no
的值被修改为6
了。
更复杂继承关系的探究
Student
->Person
->NSObject
这种继承关系的本质是怎样的呢?
代码如下
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
如果再求Person
、Student
类分别占用多少内存空间呢?
NSLog(@"%zd", class_getInstanceSize([Person class]));
NSLog(@"%zd", class_getInstanceSize([Student class]));
结果会输出
16
16
下面我们来探究一下,将代码转成C++
代码,我们可以看到Student
对象的实现,和Person
对象的实现。
Student
对象的实现中包含了Person
对象的实现,Person
对象的实现包含了NSObject
对象的实现。
因此,Person
对象的实现等同于如下
struct Person_IMPL {
Class isa;
int _age;
};
isa
占用8
字节,_age
占用4
字节,根据结构体所占用内存空间大小并根据内存对其原则可知,Person
对象占用16
个字节。注意 : 不是12
个字节
Student
对象的实现
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no;
};
里面包含Person
对象的实现(占16
字节)和int _no
(4
字节),计算Student
对象所占的字节数,同样还是16
字节,因为Person
里虽然占了16
个字节,但是实际上有4
个字节是空闲的,直接给Student
对象的int _no
用就可以了,不用再开辟新的存储空间了。注意 : 不能算成是20
个字节
假如Student
对象中还有一个int _height
对象呢,Student
占用多少内存空间呢?
答案是24
个,还是根据内存对齐原则算出的。