https://opensource.apple.com/tarballs/objc4/
一个NSObject对象占用多少内存?
答案:一个NSObject对象底层实现是一个结构体,纯净的NSObject对象的这个结构体里面就一个指针成员,所以占用内存就是存储一个指针变量所占用的大小。
展开讲就是在64bit机器实际上分配了16个字节的存储空间给NSObject对象,但是初始化时只使用了8个字节去存储isa指针。真正有使用的空间是:存储一个指针变量所占用的大小(64bit,8个字节。32bit,4个字节)
由这道题引出了下面一系列论证
展开前情:一个指针在64bit的机器上占用8字节
64位编译器下,以下数据类型占用字节数
char :1个字节
short int : 2个字节
int: 4个字节
double: 8个字节
怎么验证是一个指针变量大小呢?
1.通过地址内存查看
NSObject *obj = [[NSObject alloc]init];
可以通过打印 p/x obj地址后,把地址拷贝放到Debug->Debug Workflow->View Memory里可以看到只有8字节在使用(打断点才能调出View Memory,或者使用快捷键)
MJ在课堂里讲的是obj->isa地址查看
为了通过内存查看也是直接p/x obj的地址也才查看obj的分配情况。
(lldb) p/x obj
(NSObject *) $0 = 0x0000600002519760
然后把0x0000600002519760放到View Memory可以看到占用8字节。
2.通过class_getInstanceSize查看
还可以通过"#import <malloc/malloc.h>"里的malloc_size查看具体给obj分了多少空间(malloc_size返回指针所指向对象字节数。)
然后通过"#import <objc/runtime.h>"里的class_getInstanceSize查看具体利用空间
//运行时的class_getInstanceSize查看具体利用空间
//malloc_size查看具体分配空间;malloc_size接收的是个C对象,需要桥接,头文件里malloc_size定义如右extern size_t malloc_size(const void *ptr);
NSLog(@"%zd,%zd",class_getInstanceSize([NSObject class]),malloc_size((__bridge void *)obj)); // 8 16
通过函数打印可以看到占用是一个指针变量大小,下面在通过源码来验证
3.通过转C++文件查看
https://opensource.apple.com/tarballs/objc4/可以下载oc部分源码,从而查看源码实现
在源码里搜索allocwithZone可以看到size分配情况,调用时写的
//CF requires all objects be at least 16 bytes (源码注释里写的)
if(size < 16) size = 16;
我们平时编写的Objective-C代码,底层实现其实都是C\C++代码
所以Objective-C的面向对象都是基于C\C++的数据结构实现的
思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
结构体
将Objective-C代码转换为C\C++代码
//通过clang命令,可以将Objective-C代码转换为C\C++代码,比如下面指令可以将main.m里的OC代码转成C++
clang -rewrite-objc main.m -o main.cpp
//但是不建议用clang直接转,因为没有指定平台、架构等各种东西,比如下面指定,运行在真机环境的,arm64架构的C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
open ./ 是打开当前文件夹,然后把cpp拖到项目里便于查看
例如我们在main.m写一句
NSObject *obj = [[NSObject alloc]init]; //实例对象
这句话在oc文件里是这样定义的 (直接点击NSObject就可以看到)
@interface NSObject {
Class isa;
}
@end
可以理解为obj指针指向的一个NSObject *类型在OC里是上面这样定义。
将NSObject *obj = [[NSObject alloc]init];转成C++文件后 (将文件转成CPP后,看到NSObject类就是一个结构体)
struct NSObject_IMPL{
Class isa; //占用8字节
}
为什么说NSObject_IMPL就是obj指向的实现呢,有两点猜测原因,其一,跟刚才oc的定义大致相同,都是只有一个指针;其次,下面的一些代码也可以论证,比如自定义对象里有age什么的,都是在这个NSObject_IMPL结构体里。所以就猜想NSObject_IMPL结构体是obj的实现。可以很明显的看到,只是存了一个isa,也就是一个指针,所以obj结构体里isa就是一个指向类对象的指针
由上面这些推论,可以看到NSObject类的本质就是结构体,结构体里有一个isa指针,所以便验证是NSObject对象占用一个指针变量大小的内存的题目
NSObject *obj = [[NSObject alloc]init];这句话在内存中是怎么体现的呢?
首先通过[[NSObject alloc]init]会在内存中分配一块内存给NSObject对象。分配完空间后,会将空间的地址值赋值给obj指针。由于NSObject对象本质就是一个结构体,而且结构体此时“唯一”且第一个元素是isa,所以结构体的地址值也就是isa的地址值了,此时obj指针存储的值就是isa变量的地址值,也是结构体的地址值。
以下自定义继承NSObject的类又是什么样的呢
@interface Student : NSObject
{
@public
int _age;
int _num;
}
@end
在经过下面三步初始化赋值后
Student *stu = [[Student alloc] init];
stu->_age = 1; //(c写法)
stu->_num = 2;
转成C++文件后展示为
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _num;
};
struct NSObject_IMPL {
Class isa;
};
为了便于查看,合成如下
struct Student_IMPL {
Class isa; //这里为了方便,把右边的NSObject_IMPL考到了左边 Student_IMPL这个类的。源文件展示为struct NSObject_IMPL{
//Class ias;
//} //占用8字节
int _age; //占用4字节
int _num; //占用4字节
};
此时如何验证Student在内存中是长上面合成的样子呢?
首先在.m里定义一个结构体
struct Student_IMPL {
Class isa;
int _age;
int _num;
};
然后
Student *stu = [[Student alloc]init];
stu->_age = 1;
stu->_num = 2;
之后通过桥接,将stu转成Student_IMPL结构体形式,通过结构体访问结构体里的
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"_age = %d,_num = %d",stuImpl->_age,stuImpl->_num);
打印结果为
_age = 1,_num = 2
//通过运行时的class_getInstanceSize可以很便捷的看到占用内存(真正占用的)情况
#import <objc/runtime.h>
NSLog(@"%zd %zd",class_getInstanceSize([NSObject class]),class_getInstanceSize([Student class])); // 8 16
//malloc_size可以看到分配了多少内存给stu这个指针指向的内存
Student *stu = [[Student alloc]init];
NSLog(@"%zd",malloc_size((__bridge void *)stu));
如何查看Student在内存中的布局呢?
方法一,通过View Memory,输入stu的地址值,可以看到前16个字节的存储情况(涉及到大端小端模式,小端模式从高地址开始读(从右向左读))
方法二,通过lldb指令(打印框的调试器)
(lldb) print stu (将内存地址打印出来了) //print可简写成p print是普通打印,想打印对象,要用po
(Student *) $0 = 0x0000600003f20030
(lldb) po stu (以对象的形式打印)
<Student: 0x600003f20030>
(lldb) memory read 0x600003f20030 (通过memory read也可以读取stu的地址值里内存的情况,可以看到前16个字节的存储情况)
0x600003f20030: 60 55 08 0f 01 00 00 00 01 00 00 00 02 00 00 00 `U..............
0x600003f20040: 80 84 87 02 00 60 00 00 09 00 73 74 00 00 00 00 .....`....st....
#常用LLDB指令
memory read/数量格式字节数 内存地址
x/数量格式字节数 内存地址
memory write 内存地址 数值 (修改内存中的值)
格式:
x是16进制,f是浮点,d是10进制
字节大小:
b:byte 1字节
h:half word 2字节
w:word 4字节
g:giant word 8字节
eg:
(lldb) x 0x600003f20030 (利用x读取一个地址值)
0x600003f20030: 60 55 08 0f 01 00 00 00 01 00 00 00 02 00 00 00 `U..............
0x600003f20040: 80 84 87 02 00 60 00 00 09 00 73 74 00 00 00 00 .....`....st....
(lldb) x/4xw 0x600003f20030 (第一个x读取一个地址值,4代表读四次,第二个x代表用16进制输出,w代表4个字节4个字节的去读)
0x600003f20030: 0x0f085560 0x00000001 0x00000001 0x00000002
(lldb) x/4dw 0x600003f20030 (第一个x读取一个地址值,4代表读四次,d代表用10进制输出,w代表4个字节4个字节的去读)
0x600003f20030: 252204384
0x600003f20034: 1
0x600003f20038: 1
0x600003f2003c: 2
延伸->一个person类,一个student,student继承自person,person继承NSObject,那么在64bit下,内存占用情况如何
/* Person */
@interface Person : NSObject
{
@public
int _age;
}
/* Student */
@interface Student : Person
{
int _no;
}
在C++文件里,分配情况如下
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8个字节
int _age; // 4个字节
}; // 内存对齐后:16个字节
struct Student_IMPL {
struct Person_IMPL Person_IVARS; // 16个字节
int _no; // 4个字节 (4个字节可以补在Person_IMPL里空余没使用的4字节里,所以Student_IMPL总体还是16字节)
}; // 内存对齐后:16个字节
所以Person、Student都占用16字节
如果上题中Person长下面这样,,打印内存变成了Person占用24字节,分配了32字节 (添加一个字段,内存虽然只用4字节,但分配8字节(以8的倍数增长,为什么以8的倍数呢,因为这个结构体里最大的成员isa的内存占用是8,所以以最大的成员占有的倍数增长),下次再添加一个字段,补在这空余的4字节里,以此轮循)
/* Person */
@interface Person : NSObject
{
@public
int _age;
int _num;
int _height;
}
@end
Person *per = [[Person alloc]init];
NSLog(@"%zd,%zd",class_getInstanceSize([Person class]),malloc_size((__bridge void *)per)); //24 32
内存占用以最大的成员占有的倍数增长,但是内存分配以16的倍数增长,为什么呢?
内存分配的时候也有内存对齐,以16的倍数来对齐
以上所讨论的占用内存,都是创建的实例对象所占用的内存。里面isa以及存储变量的具体值。












网友评论