方便记忆:
- OC 三种对象:instance实例对象、class类对象、meta-class元类对象
- instance实例对象:NSObject转化为c语言其实就是一个结构体,系统分配内存空间,存放一个成员isa指针表示对象的地址(结构体的地址)64bit占用8个字节,32bit占用4个字节
- class类对象:类对象内存存储的信息:isa和superclass指针、类的属性信息和成员变量、对象方法和协议信息
- meta-class元类对象:元类对象和class对象的内存结构一样,isa指针指向基类对象,基类的元类对象的isa指针指向自己
- OC 三种对象原理:objc_class结构体的指针
关于OC对象的底层实现
寻OC对象的本质,我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。
OC的对象结构都是通过基础C\C++的结构体实现的。 我们通过创建OC文件及对象,并将OC文件转化为C++文件来探寻OC对象的本质
OC如下代码
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *objc = [[NSObject alloc] init]; NSLog(@"Hello, World!"); } return 0; }
我们通过命令行将OC的mian.m文件转化为c++文件
clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构
我们可以指定架构模式的命令行,使用xcode工具 xcrun
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp // 生成 main-arm64.cpp
main-arm64.cpp 文件中搜索NSObjcet,可以找到 NSObjcet_IMPL(IMPL代表 implementation 实现)
我们看一下NSObject_IMPL内部
struct NSObject_IMPL { Class isa; }; // 查看Class本质 typedef struct objc_class *Class; // 我们发现Class其实就是一个指针,对象底层实现其实就是这个样子。
思考: 一个OC对象在内存中是如何布局的?
NSObjcet的底层实现,点击NSObjcet进入发现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
转化为c语言其实就是一个结构体
struct NSObject_IMPL { Class isa; };
那么这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员,isa指针,而指针在64位架构中占8个字节。也就是说一个NSObjec对象所占用的内存是8个字节。
为了探寻OC对象在内存中如何体现,我们来看下面一段代码
NSObject *objc = [[NSObject alloc] init];
上面一段代码在内存中如何体现的呢?上述一段代码中系统为NSObject对象分配8个字节的内存空间,用来存放一个成员isa指针。那么isa指针这个变量的地址就是结构体的地址,也就是NSObjcet对象的地址。
假设isa的地址为0x100400110,那么上述代码分配存储空间给NSObject对象,然后将存储空间的地址赋值给objc指针。objc存储的就是isa的地址。objc指向内存中NSObject对象地址,即指向内存中的结构体,也就是isa的位置。
自定义类的内部实现
@interface Student : NSObject{ @public int _no; int _age; } @end @implementation Student int main(int argc, const char * argv[]) { @autoreleasepool { Student *stu = [[Student alloc] init]; stu -> _no = 4; stu -> _age = 5; NSLog(@"%@",stu); } return 0; } @end
按照上述步骤同样生成c++文件。并查找Student,我们发现Student_IMPL
struct Student_IMPL { struct NSObject_IMPL NSObject_IVARS; int _no; int _age; };
发现第一个是 NSObject_IMPL的实现。而通过上面的实验我们知道NSObject_IMPL内部其实就是Class isa 那么我们假设 struct NSObject_IMPL NSObject_IVARS;
struct Student_IMPL { Class *isa; int _no; int _age; };
因此此结构体占用多少存储空间,对象就占用多少存储空间。因此结构体占用的存储空间为,isa指针8个字节空间+int类型_no4个字节空间+int类型_age4个字节空间共16个字节空间
Student *stu = [[Student alloc] init]; stu -> _no = 4; stu -> _age = 5;
那么上述代码实际上在内存中的体现为,创建Student对象首先会分配16个字节,存储3个东西,isa指针8个字节,4个字节的_no ,4个字节的_age
sutdent对象的3个变量分别有自己的地址。而stu指向isa指针的地址。因此stu的地址为0x100400110,stu对象在内存中占用16个字节的空间。并且经过赋值,_no里面存储着4 ,_age里面存储着5
验证Student在内存中模样
struct Student_IMPL { Class isa; int _no; int _age; }; @interface Student : NSObject { @public int _no; int _age; } @end @implementation Student int main(int argc, const char * argv[]) { @autoreleasepool { // 强制转化 struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu; NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age); // 打印出 _no = 4, _age = 5 } return 0; }
上述代码将oc对象强转成Student_IMPL的结构体。也就是说把一个指向oc对象的指针,指向这种结构体。由于我们之前猜想,对象在内存中的布局与结构体在内存中的布局相同,那么如果可以转化成功,说明我们的猜想正确。由此说明stu这个对象指向的内存确实是一个结构体。
实际上想要获取对象占用内存的大小,可以通过更便捷的运行时方法来获取。
class_getInstanceSize([Student class]) NSLog(@"%zd,%zd", class_getInstanceSize([NSObject class]) ,class_getInstanceSize([Student class])); // 打印信息 8和16
窥探内存结构
实时查看内存数据
方式一:通过打断点。 Debug Workflow -> viewMemory address中输入stu的地址
从上图中,我们可以发现读取数据从高位数据开始读,查看前16位字节,每四个字节读出的数据为 16进制 0x0000004(4字节) 0x0000005(4字节) isa的地址为 00D1081000001119(8字节)
方式二:通过lldb指令xcode自带的调试器
memory read 0x10074c450 // 简写 x 0x10074c450 // 增加读取条件 // memory read/数量格式字节数 内存地址 // 简写 x/数量格式字节数 内存地址 // 格式 x是16进制,f是浮点,d是10进制 // 字节大小 b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节 示例:x/4xw // /后面表示如何读取数据 w表示4个字节4个字节读取,x表示以16进制的方式读取数据,4则表示读取4次
同时也可以通过lldb修改内存中的值
memory write 0x100400c68 6 将_no的值改为了6
那么一个NSObject对象占用多少内存? NSObjcet实际上是只有一个名为isa的指针的结构体,因此占用一个指针变量所占用的内存空间大小,如果64bit占用8个字节,如果32bit占用4个字节。