面试题
- 一个NSObject对象占用多少内存?
准备工作
- 我们新建一个demo来探究:一个NSObject对象占多大内存
- 我们平时编写的Objective-C代码,底层实现其实都是C\C++代码,也就是说首先会转成C++代码,再转成汇编语言,最终变成机器语言。故最好把OC转成C++才能看清它的本质。
- 将Objective-C代码转换为C\C++代码
clang -rewrite-objc main.m -o main.cpp
- 使用如下代码可以指定架构,其中
iphoneos
代表是真机,arch
表示指定架构版本,如果需要链接其他框架,使用-framework
参数,比如:-framework UIKit
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
- 我们可以得到一个
main-arm64.cpp
文件
- 把cpp文件直接拽入Xcode,为了不显示报错信息,我们不让它参与编译
探究
- 点击NSObject我们可以看到OC的定义
- 在main.cpp文件里面搜索NSObject_IMPL,我们可以看到NSObject转成C++后的本质是一个结构体
- 我们再点击Class进去,可以看到其实它就是一个指针。那么我们知道在64位环境下,它占8个字节。
论证
- 有了这个推论,我们还可以进一步论证我们的观点,这里我们用到
runtime
,也就是运行时的一个方法class_getInstanceSize
- 为了验证推论的准确性,我们先找到苹果开源的源码,打开浏览器http://opensource.apple.com/tarballs/搜索
objc
- 点击进去,我们找到数字最大的一个,一般来说也是最新的一个
- 下载压缩包解压后并打开
- 搜索
class_getInstanceSize
,并在.mm文件里面可以看到它的实现
- 点击3中的alignedInstanceSize()方法,我们可以看到进一步注释,这个方法返回的是类对象成员变量所占用的内存大小
- 所以通过调用runtime的这个方法我们可以看到,NSObject类对象所占用的内存大小为8个字节
- 我们还有个叫malloc_size的方法可以查看系统分配内存的大小。由于这里需要传一个C的指针,所以我们打印obj的时候需要桥接一下。然后我们会发现,返回的大小是16而不是8!
总结
- 为什么会造成这个不一致呢?我们还是要从源码入手分析,首先还是在objc的源码里面搜索
allocWithZone
这个方法,查看创建一个对象是内存是怎么分配
- 我们点击进入
class_createInstance
这个方法,看到返回的是_class_createInstanceFromZone
方法,我们再次点击进去就能很快锁定我们需要的信息:size
- 我们可以从注释看到,所有对象至少分配16 bytes 的大小,从方法实现来看,如果size < 16,则size = 16
答:NSObject对象内部只使用了一个指针变量所占用的大小(64bit,8个字节,32bit,4个字节,可以通过objc_getInstanceSize
函数获得),但系统分配了16个字节给NSObject对象(通过malloc_size
函数获得),即总结如下:系统分配了16个字节给NSObject对象(通过malloc_size
函数获得),但NSObject
对象内部只使用了8个字节空间(64bit环境下,可以通过class_getInstanceSize
函数获得)
- 所以Objective-C的面向对象都是基于C\C++的数据结构实现的。
- 思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
- 结构体
- 思考:一个OC对象在内存中是如何布局的?
- NSObject的底层实现
@interface NSObject { Class isa; } 最终被转换为 struct NSObject_IMP { Class isa; }
- 相当于创建的NSObject对象最终在内存中就是一个结构体,并且这个结构体只有一个指针成员,而指针在64位的机器上占用8个字节,所以这个结构体在内存中占用8个字节,故一个NSObject对象在内存中也占用8个字节。虽然NSObject也声明了很多方法,但这些方法占用的空间并不是NSObject的,而是其他的,以后再讲。
- 因为NSObject对象只有一个isa的指针,所以NSObject对象的地址也是isa指针的地址,也是结构体的地址值。
- 自定义对象占用多少内存?
准备工作
我们新建一个继承自NSObject的Person类,帮它增加三个成员变量,那么Person又会占据多大的内存
- 照惯例我们还是使用runtime和malloc的函数来查看一下,会发现得到的是24和32,但是为什么是24和32呢?
照惯例我们还是使用runtime和malloc的函数来查看一下,会发现得到的是24和32,但是为什么是24和32呢?
论证
打开main.m所在的文件夹,在终端输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
,得到重新编译后的C++文件,并把它拽入Xcode中,通过搜索可以看到Person
在底层C++的实现,正如我们看到的,一个NSObject
对象占8个字节,一个int占4个字节,那么一共占8+4+4+4=20个字节,为什么会打印出来24和32呢?
- 这里涉及一个内存对齐原则,系统在计算结构体大小时,分配的空间是所需最大内存的倍数,在Person的结构体中,需要最大的内存是8个字节,所以当实际使用20个字节时,实际分配的是3 * 8 = 24个字节,所以Person指向的内存大小为24个字节
- 我们也可以通过sizeof这个函数,打印出Person_IMPL这个结构体的长度,进一步论证Person转成C++结构体后的实际长度为24
疑问
- 那为什么通过malloc函数打印出来的长度会是32呢?我们还是从malloc的源码入手分析一波,打开网址https://opensource.apple.com/tarballs/,搜索libmalloc,选择编号最大的一份下载
- 解压后打开源码,我们搜索bucket(可以理解为桶,容器),我们可以从注释看到,其实iOS系统在给我们分配内存时有它自己的原则,分配的大小都是16的倍数,所以即使Person_IMPL结构体为24,但是系统也会为它分配32个字节大小的内存
总结
- 系统在计算Person时只需要24个字节就够用了,但是实际上在OC分配规则下,系统实际分配的内存空间是32个字节
- 我们在源码里直接搜索malloc.c,可以看到其实OC内存分配的实现方法并不止一种,只是我们刚刚的这个例子用到的分配方法是这个而已。
- 以下
Student
的探究和上面类似
@interface Student:NSObject
{
@public
int _no;
int _age;
}
Student *stu = [[Student alloc]init];
stu->_no = 4;
stu->_age = 5;
student对象经过clang之后被转换成为
struct Student_IMPL
{
Class isa;
int _no;
int _age;
}
- 结构体
Student_IMPL
占用多少字节,student对象即占用多少字节 - student对象一共占用8个字节,8个字节的isa指针,两个分别占用4个字节的_no和_age
- isa的地址是最低的,所以isa指针的地址就是student对象的地址
-
student_IMPL
和student对象之间的强转
struct Student_IMPL *stuImpl = (_bridge struct Student_IMPL *)stu;
NSLog(@"_no = %d, _age = %d",stuImpl -> _no, stuImpl -> _age);
- 还可以通过以下方法来获取对象占用的内存大小,对象的存储空间里只存放实例变量,不存放方法
#import <objc/runtime.h>
class_getInstanceSize([Student class]);
- 或者使用
malloc_size(stu)
来获得stu
指针指向的内存的大小,其与class_getInstanceSize
打印的值可能不一样,一个NSObject
对象alloc时至少分配16个字节,这是Core Foundation
内部规定的,这个规定在runtime
源码部分的allocWithZone
方法内部。
NSLog(@"%zd",malloc_size((_bridge const void *)stu));
窥探内存结构
- 实时查看内存数据:
Debug -> Debug Workflow -> View Memory
,但这里一定要打断点才行,然后在出现的页面下方的address里输入stu对象的地址即可。 - 还可以使用LLDB指令:
po stu
和p stu
来打印对象地址 - 还可以用
memory read 0x10050e810
或x 0x10050e810
来打印出对象的内存结构,其中0x10050e810
表示的是对象的内存地址,优化过后的指令为x/4xw 0x10050e810
表示以4个字节来打印对象 - 改写内存中的数据用:
memory write 0x10050e810 6
,即把地址为0x10050e810
里存放的数据改为6 - 结构体占用的字节是不是简简单单变量所占用的字节之和,还有一个字节对齐的规则,子类的结构体里包含了父类的实现,父类里面又包含了父类的父类的实现。
- 看苹果开放的源码地址是:https://opensource.apple.com
- 内存对齐规则:结构体的大小必须是占用最大内存成员的大小的倍数
- 相比
objc_getInstanceSize
我们更关注malloc_size
的值大小,objc_getInstanceSize
也是内存对齐过后的大小。 - 内存分配注意点:iOS在分配内存的时候一般是16的倍数,这个iOS底层的规则决定的,最大是256字节,目的是为了内存访问速度最快。
- GUN:全名是
GUN not unix
,其开源了很多免费的工具。
网友评论