- - -
OC对象的本质
平时编写的OC代码,底层实现其实都是C/C++。经历了
OC——>C/C++——>汇编语言——>机器语言
的过程。
那OC是基于C/C++的哪种数据结构呢?答案是结构体(struct),因为只有结构体struct(结构体)能够容纳不同类型的内容(比如不同属性)。那如何验证呢?可以将OC源文件通过clang编译器编译生成C++代码查看。
1、将Objective-C代码转换为C \ C ++代码
创建一个工程验证:macOS—>command line tool
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"Hello, World!");
}
return 0;
}
cd到当前工程,编程成C++代码有两种方式:
1、clang -rewrite-objc main.m -o main.cpp(cpp表示c++)
将源文件转写成通用的cpp文件
2、xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
xcode(xc)执行(run)手机系统(iphoneos)的sdk 编译(clang) arm64 重写objc(-rewrite-objc)文件(main.m)输出(-o)main-arm64.cpp
嗯…
就是通过xcode的clang编译用arm64手机架构模式把main.m文件转成main-arm64.cpp文件。
为了方便查看,我们把生成的cpp文件拖入到工程中,此时编译器报错,原因是工程中有两个main入口,可以在build phase—>compile sources取消main-arm64.cpp文件的编译。
main-arm64.cpp 文件中搜索NSObject,可以找到NSObject_IMPL( IMPL—implementation )
struct NSObject_IMPL {
Class isa;
};
为了进一步验证,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
}
去掉预编译指令和协议,这个样子:
@interface NSObject {
Class isa;
}
由此我们可知:
Class isa又是什么呢?搜typedef struct objc_class *Class;
我们发现Class其实就是一个结构体指针,这个指针指向objc_class结构体。
总结一句话:NSObject对象底层实现其实就是一个指向Class指针的结构体。
2、自定义类的内部实现
既然NSObject对象底层实现是一个结构体,结构体内的isa指针指向objc_class(也是个结构体)。而OC中所有的对象都继承自NSObject,我们看一下自定义类既继承自NSObject对象的内部实现。
@interface Student : NSObject {
@public
int _age;
}
@end
重新编译成成C++之后
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
struct NSObject_IMPL {
Class isa;
};
struct NSObject_IMPL里面只包含一个Class isa,为了简化可以直接替换,变成这样,
struct Student_IMPL {
Class isa; //8字节
int _age; //4字节
};
总结:OC对象的本质实际上是一个包含了所有父类成员变量+ 自身成员变量的结构体
3、OC对象实际占用内存与开辟内存关系
C语言规定一个指针变量在64位的机器上大小是8个字节,在32位机器上是4个字节。
我们通过NSObject对象对应的结构体发现,结构体中只有一个isa指针变量。按理来说NSObject对象需要的内存大小只要能够满足存放一个指针大小就可以了,也就是说只要有8个字节的内存空间就能满足存放一个NSObject对象了。
那是不是说一个NSObject对象就占用8个字节大小的内存呢?实际上不是这样的。我们需要分清楚两个概念
- 对象实际利用的内存,即对象本身占用的内存空间
- 对象占用的内存空间,即系统实际给对象分配的内存
对象占用的内存空间和编译器实际分配的内存空间。
我们可以用坐车的例子来说明一下这两个概念的区别:对象占用的内存空间就好比汽车的载客数量,对象实际利用的内存空间就好比车上实际的乘客数量,实际的乘客数量是不会超过车辆的最大载客数量的。NSObject对象中的isa指针变量就好比车上的乘客,实际上系统给一个NSObject对象分配了16个字节大小的内存空间。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface Student : NSObject {
@public
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"objc对象实际需要的内存大小: %zd", class_getInstanceSize([obj class]));
NSLog(@"objc对象实际分配的内存大小: %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"Hello, World!");
}
return 0;
}
//打印结果:
2018-06-19 19:09:42.936282+0800 Essence[9152:659378] objc对象实际需要的内存大小: 8
2018-06-19 19:09:42.937249+0800 Essence[9152:659378] objc对象实际分配的内存大小: 16
对象的实例变量利用的内存大小,通过runtime中的class_getInstanceSize可以获得
对象实际占用的内存大小,就是系统实际分配给对象的内存大小,OC对象是通过alloc方法得到的对象大小,我们可以通过malloc中库函数malloc_size来得到结果。
为什么一个NSObject对象明明只需要8个字节的内存大小就可以了,但是还是分配到了16个字节大小的内存空间?对于这个问题我们可以通过阅读objc4的源代码来找到答案。通过查看跟踪obj4中alloc和allocWithZone两个函数的实现,会发现这个连个函数都会调用一个instanceSize的函数:
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16bytes.
if (size < 16) size = 16;
return size;
}
4、内存对齐
按照分析,再看代码
@interface Student : NSObject {
@public
int _age;
}
@end
NSObject在64位机上虽然大小只有一个指针站8位,再加上int类型(8 + 4)=12,但是OC默认分配到了16个字节,所以
Student对象实际利用的内存 = 12
Student对象占用的内存空间 = 16
我们通过class_getInstanceSize和class_getInstanceSize打印发现
Student *stu = [[Student alloc] init];
NSLog(@"objc对象实际需要的内存大小: %zd", class_getInstanceSize([stu class]));
NSLog(@"objc对象实际分配的内存大小: %zd", malloc_size((__bridge const void *)(stu)));
//结果
2018-06-19 19:26:32.744222+0800 Essence[9205:666817] objc对象实际需要的内存大小: 16
2018-06-19 19:26:32.744353+0800 Essence[9205:666817] objc对象实际分配的内存大小: 16
objc对象实际需要的内存大小是16而不是推算的12,这其中涉及到了结构体成员变量的内存对齐的问题,结构体内存对齐其中有一条要求结构体大小需要是最大成员变量大小的整数倍,这里的最大成员变量是指针变量(8个字节),结构体的最终的大小需要是8的整数倍,所以结果是16而不是12。系统实际分配的大小也是16字节,这个就比较好理解了,之前我们提到系统最小分配的内存大小是16字节。
内存对其规则可以查看另一篇文章:https://blog.csdn.net/zhw521411/article/details/105153812
我们在加一个属性
@interface Student : NSObject {
@public
int _no;
int _age;
}
@end
打印发现
实际需要的内存大小: 16
对象实际分配的内存大小: 16
也就是说指针站8位,int _no站4位,没有int _age时有四个空位,加上int _age整好补齐。
总结:计算内存大小不要忽略isa指针,如果计算C/C++要记得内存对其规则,计算OC对象的分配大小一定16的整数倍。
5、内存布局窥探
再Student实例对象前面加断点,获取到Student虚拟内存地址,debug—>debug workflow—>view memory中搜索断点显示的地址。
查看虚拟内存中的地址
_no的内存区域为07 00 00 00 4个字节,值为7,在低地址,可见是小端模式。
小端模式:低低高高(低地址存放低字节,高地址存放高字节)
大端模式:低高高低(低地址存放高字节,高地址存放低字节)
基本都是小端模式,如果大端模式,应该变成:00 00 00 07
参考资料
小码哥视频:https://ke.qq.com/course/314526
希望大家多多支持,不要像我一样白嫖!