一、一个NSObject对象占用多少内存?
- 针对问题, 我们创建一个项目工程, 并创建一个NSObject对象
- 我们平时编写的Objective-C代码, 底层实现其实都是C\C++代码
- 所以Objective-C的面向对象都是基于C\C++的数据结构实现的
思考: Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
结构体: Objective-C中类的属性多样, 只有结构体能承载
- 将Objective-C代码转换为C\C++代码, 使用Mac终端进行转换, C++的文件格式是
.cpp
$ clang -rewrite-objc main.m -o main.cpp
复制代码
- 使用Xcode查看
main.app
文件, 可以看到转换后的文件有9万多行代码, 并且我们创建NSObject
对象的代码就在文件的最后面
-
注意: 在不同平台上, 支持的代码是不一样的, 比如Windows, Mac, iOS等平台
-
所以在转换代码的时候, 可以指定某一个平台后再转换, 终端指令如下
# 指定: iOS操作系统, arm64架构, 将 main.m 文件转为 main-arm64.cpp 文件
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
复制代码
- 使用Xcode查看
main-arm64.cpp
文件, 此时转换后的C++代码只有3万多行, 创建NSObject
对象的代码同样在最底部
- 此时的项目工程有10个错误, 这些错误全都是
main-arm64.cpp
文件带来的, 所以可以使其不参与编译, 将错误消除
- 在
main-arm64.cpp
文件中, 找到下面的代码
-
NSObject_IMPL
实际意思是NSObject Implementation
, 即: NSObject类的实现 -
我们可以使用
command + 鼠标右键
点击NSObject类, 进入NSObject.h
文件中查看NSObject类的定义
通过这种方法, 可以从侧面证明 Objective-C 中的类, 在底层是C\C++中的结构体类型
所以NSObject对象, 在内存中就是一个结构体对象
-
思考: 一个OC对象在内存中是如何布局的?
-
NSObject的底层实现如下:
- 由图可知, 一个
NSObject_IMPL
结构体中, 只有一个成员变量Class isa
, 我们使用command + 鼠标右键
点击Class
类型查看一下
-
由图可知,
Class
类型是一个指针, 所以NSObject_IMPL
结构体中只包含一个指针变量 -
在
arm64
架构中, 一个地址占用8个字节, 所以isa
指针占用8个字节的内存空间 -
可以使用
runtime
中的class_getInstanceSize(Class _Nullable cls)
方法, 查看一个对象中, 所有成员变量占用的内存大小
-
那么一个NSObject对象, 到底占用多少的内存空间呢?
-
可以使用
malloc
框架中的malloc_size(const void *ptr)
方法查看
-
疑问: 为什么NSObject对象在内存中只使用了8个字节存储
isa
指针, 却分配了16个字节的内存空间呢? -
在官网中, 找到
Objc4
开源代码
- 下载最新代码
- 使用Xcode打开
Objc4
, 查找alloc
方法的底层方法_objc_rootAllocWithZone()
- 查看
_objc_rootAllocWithZone()
方法的实现, 可以找到class_createInstance()
方法, 这个就是创建对象调用的方法
- 继续查看
class_createInstance()
方法的实现, 方法中调用了_class_createInstanceFromZon()
方法
- 继续查看
_class_createInstanceFromZon()
方法的实现, 此时可以看到创建对象时调用calloc()
函数,calloc()
函数传入的第二个参数size
, 就是分配内存的大小,size
是通过instanceSize()
方法获取的
- 继续查看
instanceSize()
方法, 可以知道, size的最小是就是16
- 这就是为什么NSObject明明只有一个指针变量, 却占用了16个字节内存的原因
一个NSObject对象占用多少内存?
系统分配了16个字节给NSObject对象 (通过malloc_size函数获得)
但NSObject对象内存只使用了8个字节的空间(64bit环境下, 可以通过class_getInstanceSize函数获得)
二、窥探NSObject的内存
- 运行程序, 打断点, 打印obj对象, 获取obj对象的地址
- 查看内存
- 输入上面找到的obj地址, 查看obj对象在内存中的信息, 其中isa使用了8个字节, 剩余8个字节没有被使用
三、常用LLDB指令
1、print
、p
: 打印值
2、po
: 打印对象
3、memory read/数量格式字节数 内存地址
: 读取内存, 有缩略写法: x
- 读取内存的格式和字节数如下:
格式:
x: 16进制 f: 浮点数 d: 十进制
字节大小:
b: byte: 1字节 h: half word: 2字节
w: word: 4字节 g: giant word: 8字节
复制代码
- 指定条件读取内存
4、memory write 内存地址 数值
: 修改内存中的值
四、自定义类型的实例, 在内存中的大小
1、以自定义类Student
为例
- 定义
Student
类, 继承自NSObject
, 并添加两个int
类型的成员变量, 具体实现如下:
@interface Student: NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
// 类的实现部分
@end
复制代码
- 此时测试代码如下:
2、根据main.m
文件, 获取对应的.cpp
文件, 查看Student
在底层的具体实现
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
复制代码
- 查看
Student
的底层结构体实现:
Student_IMPL
中struct NSObject_IMPL NSObject_IVARS;
是父类NSObject
的实现
- 所以
Student_IMPL
的实际实现如下:
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
复制代码
3、测试Student
的实例, 是不是Student_IMPL
类型
- 使用
struct Student_IMPL *
指针, 指向Student
的一个实例, 代码如下
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL*)stu;
NSLog(@"no - %d, age - %d", stuImpl->_no, stuImpl->_age);
复制代码
- 运行后, 可以打印出
_no
和_age
的值, 就是4
和5
:
// 打印结果
no - 4, age - 5
复制代码
-
这从侧面可以验证
Student
底层就是Student_IMPL
类型 -
也可以在下图的位置打断点, 用来查看
Student
在内存中的数据
- 可以看到内存中的
Student
实例:
- 前八个字节是
isa
, 后八个字节是_no
和_age
, 因为一个int
类型只占4个字节 - 在结构体中, 成员变量之间的地址是连接在一起的, 所以
Student
的成员变量一共占用16
个字节的内存 - 由上面可知一个
NSObject
的实例最少分配16
个字节, 所以例子中定义的Student
类型的一个实例, 在内存中会占用16
个字节的内存, 并且这个16
个字节全部都被使用 - 使用
class_getInstanceSize
和malloc_size
进行测试
五、更复杂的继承结构
- 现有如下代码:
Person
继承自NSObject
, 有一个成员变量_age
,Student
继承自Person
, 有一个成员变量_no
@interface Person: NSObject
{
int _age;
}
@end
@implementation Person
@end
@interface Student: Person
{
int _no;
}
@end
@implementation Student
@end
复制代码
- 此时底层实现如下:
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no;
};
复制代码
-
可以推测出:
Person
的实例占用内存为isa + _age = 12
, 小于16
, 所以Person
的实例占用16
字节的内存Student
的实例占用内存为isa + _age + _no = 16
, 等于16, 所以Student
的实例占用16
字节的内存
-
使用代码检测:
注意:
我们知道class_getInstanceSize
函数获取到的, 是类型中所有成员变量一共占用内存的大小, 所以Person
获取到的成员变量应该是isa
+_age
=12
个字节, 但是为什么返回16
字节呢?
- 查看一下
class_getInstanceSize
的源码:
- 进入
alignedInstanceSize
函数:
实际上
class_getInstanceSize
获取到的大小, 是内存对齐
之后的大小, 即所有成员变量中, 占用内存最大的那个成员变量的倍数
,Person
内的NSObject_IMPL
有一个isa
占用8
个字节,_age
占用4
个字节, 所以class_getInstanceSize
获取到的是8
的倍数, 即16
个字节
-
对于
Person
,NSObject_IMPL
虽然占有16
个字节, 但是只有一个成员变量isa
, 所以有8
个字节是空的, 所以, 会将_age
放在空的位置, 而不是继续累加至20
个字节 -
对于
Student
, 有两个成员变量Person_IMPL
和_no
,Person_IMPL
占用16
个字节, 但是有4
个字节是空的, 所以分配给了_no
使用, 即Student
分配内存是16
个字节
1、属性会生成对应的成员变量
- 再次给
Student
添加一个成员属性height
@interface Student: Person
{
int _no;
}
@property (nonatomic, assign) int height;
@end
@implementation Student
@end
复制代码
- 此时
Student
的底层实现为:
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no;
int _height;
};
复制代码
- 测试
Student
在内存中的大小如下:
- 此时
Student
中的成员变量是isa + _age + _no + _height = 20
, 最大成员变量isa
占用内存为8
, 所以class_getInstanceSize
获取到的是24
个字节
注意:
OC中给实例对象分配空间时, 是按照16, 32, 48, 64, 80, 96...按照16
的倍数递增的, 所以malloc_size
函数获取到的Student
实例内存是32