对小码哥底层班视频学习的总结与记录
NSObject是所有类的基类,所有的类都继承自它,它太平常了,平常到我们从不去多加任何思考,但是它又那么重要,因为他是OC的基础,所以今天我们将通过几点对NSObject
抽丝剥茧,看看他的底层到底是怎样的:
- 一个 NSObject 对象占用多少内存?
- 对象的 isa 指针指向哪里?
- OC 的类信息存放在哪里?
一个NSObject对象占用多少内存?
- 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
- 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
Objective-C的本质
平时我们编写的OC代码,底层实现都是C/C++代码
Objective-C --> C/C++ --> 汇编语言 --> 机器码
所以Objective-C的面向对象都是基于C/C++的数据结构实现的,所以我们可以将Objective-C代码转换成C/C++代码,来研究OC对象的本质。
思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
结构体
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
我们在main函数里面定义一个简单对象,然后通过 clang -rewrite-objc main.m -o main.cpp
命令,将main.m
文件进行重写,即可转换出对应的C/C++代码。但是可以看到一个问题,就是转换出来的文件过长,将近10w行。
clang -rewrite-objc main.m -o main.cpp
64 warnings generated.
因为不同平台支持的代码不同(Windows/Mac/iOS),那么同样一句OC代码,经过编译,转成C/C++代码,以及最终的汇编码,是不一样的,汇编指令严重依赖平台环境。
我们当前关注iOS开发,所以,我们只需要生成iOS支持的C/C++代码。因此,可以使用如下命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_arm64.cpp
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <输出的cpp文件>
-sdk
:指定sdk
-arch
:指定机器cpu架构(模拟器-i386、32bit、64bit-arm64 )
如果需要链接其他框架,使用-framework参数,比如-framework UIKit
一般我们手机都已经普及arm64,所以这里的架构参数用arm64
接下来,我们查看一下main_arm64.cpp源文件,如果熟悉这个文件,你将会发现这么一个结构体
struct NSObject_IMPL {
Class isa;
};
这就是NSObject
的底层实现,我们也可以直接进入到NSObject.h
的头文件中看看NSObject
是如何定义的:
对比看一下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
简化一下,就是
@interface NSObject {
Class isa ;
}
@end
是不是猜到点什么了?没错,struct NSObject_IMPL
其实就是NSObject的底层结构,或者说底层实现。换个角度理解,可以说C/C++的结构体类型支撑了OC的面相对象。
点进Class的定义,我们可以看到 是typedef struct objc_class *Class;
// 指针
// typedef struct objc_class *Class;
Class isa; 等价于 struct objc_class *isa;
所以NSObject对象内部就是放了一个名叫isa
的指针,指向了一个结构体 struct objc_class
。
现在知道了Class
是一个指针,而NSObject
底层就只有一个Class isa
那我们就知道了NSObject
占用多少内存了.因为指针在64位系统中占8个字节,在32位系统中占4个字节.所以我么可以猜测:NSObject
在内存中占8个字节.
总结一:一个OC对象在内存中是如何布局的?
*猜想:NSObject对象的底层就是一个包含了一个指针的结构体,那么它的大小是不是就是8字节(64位下指针类型占8个字节)?*
为了验证猜想,我们需要借助runtime提供的一些工具,导入runtime头文件,class_getInstanceSize ()
方法可以计算一个类的实例对象所实际需要的的空间大小
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject对象的大小:%zd",size);
}
return 0;
}
结果是
注意:class_getInstanceSize()
是获取某一个类创建出来的==实例对象所占用的内存大小.==
系统中还有一个方法是取出一个指针所指向的内存的大小:malloc_size(<#const void *ptr#>)
现在有两个结果:8 和 16.哪一个才一个 NSObject 对象占用多少内存?
的结果呢?答案是16个字节.因为class_getInstanceSize ()
返回的其实并不是一个对象的全部内存大小,实际上它返回的是一个类的实例对象的成员变量所占用的内存大小
介绍另一个库#import <malloc/malloc.h>
,其下有个方法 malloc_size()
,该函数的参数是一个指针,可以计算所传入指针 所指向内存空间的大小
。我们来用一下
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject实例对象的大小:%zd",size);
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);
}
return 0;
}
想要真正弄清楚其中的缘由,就需要去苹果官方的开源代码里面去一探究竟了。苹果的开源代请看这里。
先看一下class_getInstanceSize
的实现。我们需要进到objc4/文件里面下载一份最新的源码,我当前最新的版本是objc4-750.1.tar.gz。下载解压之后,打开工程,就可以查看runtime的实现源码。
搜索class_getInstanceSize
找到实现代码
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
再点进alignedInstanceSize
方法的实现
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
可以看到该方法的注释说明Class's ivar size rounded up to a pointer-size boundary.
,意思就是获得类的成员变量的大小,其实也就是计算类所对应的底层结构体的大小,注意后面的这个rounded up to a pointer-size boundary
指的是系统在为类的结构体分配内存时所进行的内存对齐,要以一个指针的长度作为对齐系数,64位系统指针长度(字长)是8个字节,那么返回的结果肯定是8的最小整数倍。为什么需要用指针长度作为对齐系数呢?因为类所对应的结构体,在头部的肯定是一个isa
指针,所以指针肯定是该结构体中最大的基本数据类型,所以根据结构体的内存对齐规则,才做此设定。如果对这里有疑惑的话,请先复习一下有关内存对齐的知识,便一目了然了。
*所以class_getInstanceSize
方法,可以帮我们获取一个类的的实例对象所对应的结构体的实际大小。*
class_getInstanceSize
和 malloc_size
的关系就好比下图:
打开runtime
源码 --> 搜索allocWithZone
--> 点击进入class_createInstanceFromZone
方法 --> 点击进入 class_createInstanceFromZone
可以看到创建的alloc
方法是调用obj = (id)calloc(1, size);
传入了一个size,而这个size
是调用instanceSize(extraBytes)
获得,我们再进入instanceSize(extraBytes)
内部
我们再从alloc
方法探究一下,alloc
方法里面实际上是AllocWithZone
方法,我们在objc
源码工程里面搜索一下,可以在Object.mm
文件里面找到一个_objc_rootAllocWithZone
方法。
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
再点进里面的关键方法class_createInstance
的实现看一下
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
继续点进_class_createInstanceFromZone
方法
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
这个方法有点长,有时分析一个方法,不要过分拘泥细节,先针对我们寻找的问题,找到关键点,像这个比较长的方法,我们知道,它的主要功能就是创建一个实例,为其开辟内存空间,我们可以发现中间的这句代码obj = (id)calloc(1, size);
,是在分配内存,这里的size
是需要分配的内存的大小,那这句应该就是为对象开辟内存的核心代码,再看它里面的参数size
,我们能在上两行代码中找到size_t size = cls->instanceSize(extraBytes);
,于是我们继续点进instanceSize
看看
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;//小于 16 ,就让他等于 16.
return size;
}
翻译一下这句注//CF requires all objects be at least 16 bytes.
我们就明白了,CF作出了硬性的规定:当创建一个实例对象的时候,为其分配的空间不能小于16
个字节
这个size_t instanceSize(size_t extraBytes)
返回的字节数,其实就是为 为一个类创建实例对象所需要分配的内存空间。这里我们的NSObject
类创建一个实例对象,就分配了16个字节。
我们在点进上面代码中的alignedInstanceSize
方法
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
这不就是我们上面分析class_getInstanceSize
方法里面看到的那个alignedInstanceSize
嘛。
总结二:class_getInstanceSize
&malloc_size
的区别
-
class_getInstanceSize
:获取一个objc类的实例的实际大小,这个大小可以理解为创建这个实例对象至少需要的空间(系统实际为这个对象分配的空间可能会比这个大,这是出于系统内存对齐的原因)。 -
malloc_size
:得到一个指针所指向的内存空间的大小。我们的OC对象就是一个指针,利用这个函数,我们可以得到该对象所占用的内存大小,也就是系统为这个对象(指针)所指向对象所实际分配的内存大小。
sizeof()
:获取一个类型或者变量所占用的存储空间,这是一个运算符。 -
[NSObject alloc]
之后,系统为其分配了16个字节的内存,最终obj
对象(也就是struct NSObject_IMPL
结构体),实际使用了其中的8个字节内存,(也就是其内部的那个isa
指针所用的8个字节,这里我们是在64位系统为前提下来说的)
关于运算符和函数的一些对比理解
- 函数在编译完之后,是可以在程序运行阶段被调用的,有调用行为的发生
- 运算符则是在编译按一刻,直接被替换成运算后的结果常量,跟宏定义有些类似,不存在调用的行为,所以效率非常高
更为复杂的自定义类
我们开发中会自定义各种各样的类,基本上都是NSObject
的子类。更为复杂的子类对象的内存布局又是如何的呢?我们新建一个NSObject
的子类Student
,并为其增加一些成员变量
@interface Student : NSObject
{
@public
int _age;
int _no;
}
@end
@implementation Student
@end
使用我们之前介绍过的方法,查看一下这个类的底层实现代码
struct NSObject_IMPL {
Class isa;
};
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;
};
我们发现其实Student
的底层结构里,包含了它的成员变量,还有一个NSObject_IMPL
结构体变量,也就是它的父类的结构体。根据我们上面的总结,NSObject_IMPL
结构体需要的空间是8字节,但是系统给NSObject
对象实际分配的内存是16字节,那么这里Student
的底层结构体里面的成员变量NSObject_IMPL
应该会得到多少的内存分配呢?我们验证一下。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
NSLog(@"%zd", class_getInstanceSize([Student class]));
NSLog(@"%zd", malloc_size((__bridge const void *)stu));
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
}
return 0;
}
Student
类的底层结构体等同于
struct Student_IMPL {
Class isa;
int _age;
int _no;
};
总结一下就是,一个子类的底层结构体,相当于 其父类结构体里面的所有成员变量 + 该子类自身定义的成员变量 所组成的一个结构体。在类对象中只存放了实例对象和属性,并没有方法.这是因为,一个类的实例对象可以有很多,每个实例对象的值也不一样,而类方法只需要一份供实例对象调用即可,所以类对象中只存储实例对象和属性,不存储方法.那么方法存放在哪里呢?