前言
最近在学习底层原理相关知识,想着写点东西,一则加深理解,二则希望能和大家一起交流,扩展思路。😸但简书搜索 iOS对象本质,惊了,我这是有多过时,各种文章满天飞,瞬间不知如何下笔。但还是硬着头皮往下写....,毕竟别人的终归是别人的,做好自己就好,这是我的处女作,写的不好解释不恰当的,请大家见谅,并帮忙指出。
废话不多说,直接入正题(以下内容都基于arm64进行分析)
一、NSObject的本质是什么?让我们来解开它的神秘面纱
#import <Foundation/Foundation.h>
//创建一个动物类
@interface Animal : NSObject
@end
@implementation Animal
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
}
return 0;
}
- xcode新建项目,main文件中编写上图代码,打开终端,进入main.m所在的文件夹,通过如下命令
clang -rewrite-objc main.m -o main.cpp
或者
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
分析:
xcrun -sdk iphoneos
使用xcode命令行工具xcrun指定sdk类型为iphoneos
clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
使用clang编译器,指定编译的架构为arm64,-rewrite-objc重写main.m文件,-o 输出文件为main-arm64.cpp
未指定架构和sdk类型的,则会编译出所有sdk类型(例:macos,ipados)和所有架构(例:armv7 -i386)的代码,编译自然会慢一些
生成编译后的main-arm64.cpp文件(摘录部分代码)
//NSObject编译后的结构
struct NSObject_IMPL {
Class isa;
};
//Class为结构体指针类型
typedef struct objc_class *Class;
//Animal类编译后结构
struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSObject *object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
}
return 0;
}
从上面的代码可看出,NSObject编译后的结构为结构体类型,其中包含一个Class类型的isa指针,Animal编译后的结构也为结构体类型,其中包含一个NSObject_IMPL的结构体。分析到这里,我们可以确认,OC代码的底层实现是C/C++。
接下来让我们分析几个问题,来巩固我们上面说的知识
二、NSObject对象占多少内存?
//NSObject编译后的结构
struct NSObject_IMPL {
Class isa;
};
//Class为结构体指针类型
typedef struct objc_class *Class;
NSObject *object = [[NSObject alloc] init];
// 获得object指针所指向内存的大小 16
NSLog(@"%zd",malloc_size((__bridge const void *)(object)));
// 获得NSObject实例对象的成员变量所占用的大小 8
NSLog(@"%zd",class_getInstanceSize([NSObject class]));
先简单通过打印分析一下,按照刚才我们所知的NSObject编译后的结构体,可以知道他包含了一个指针(arm64架构下,指针所占的内存为8个字节)。class_getInstanceSize获取的是类成员变量所占用的大小,因此该结果应该为8个字节,上图malloc_size获取的是object指针所指向的内存大小,按结构分析应该也是8个字节才对,但是实际打印却是16,接下来让我们跟踪源码苹果官方源码来分析下为何class_getInstanceSize得到的大小为8,malloc_size得到的大小为16。
分析class_getInstanceSize
- 通过查看class_getInstanceSize的头文件,路径在objc/runtime.h,由此可知该函数属于objc库中,源码库中对应为objc4的库。或者我们可以利用强大的搜索引擎进行搜索,如果存在源码,广大网友们会告知你去下载哪个开源库的。从以下源码可知,class_getInstanceSize获取的是类成员变量字节对齐后所占用的大小,而NSObject中只有一个指针,指针在arm64系统下为8个字节
// May be unaligned depending on class's ivars.
//获取(unaligned)字节对齐前类的(ivars)成员变量的大小
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
// Class's ivar size rounded up to a pointer-size boundary.
//获取类的(ivar)成员变量字节对齐后的大小
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
分析malloc_size
- 通过查看malloc_size头文件路径malloc/malloc.h文件中,在源码库中查找malloc,我查找到了libmalloc库,但是很遗憾,只能看到表层,里层代码是闭源的,无法探知其原理。
- 然后我从开辟空间的角度去探究,毕竟malloc_size仅仅是获取内存大小,实际开辟内存的是alloc方法,alloc通过头文件可知在objc库中,下载objc4-781,接下来看我找到的代码段:
//大家可以下载源码自己跟踪,alloc调用的是allocWithZone,然后即可接着定位到如下函数
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
//从此处可知,size的大小通过Class的instanceSize获取
if (outAllocatedSize) *outAllocatedSize = size;
//后续代码不重要,省略
.....
}
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- 根据instanceSize函数,可知alloc开辟空间的大小,首先获取类成员变量字节对齐后的大小,然后再根据系统本身约束来开辟空间,此处由于size为8,因此if(size < 16)会把size强制设置为16,所以alloc开辟的空间至少为16字节,因此malloc_size获取NSObject对象所占内存时,得到的结果为16。
根据上面的源码追踪,我相信都能理解了,也自然也能准确的回答NSObject所占的内存了。
NSObject对象占多少内存?
我的答案:在arm64架构下,NSObject所占内存大小为16个字节。其中8个字节为NSObject对象中的isa指针所占用的字节,另外8个字节是系统为了更优的访问内存额外开辟的空间,不存放任何数据。
三、NSObject对象占用多少内存我们已经知道了,那下面几个对象呢,让我们一起来分析一下
@interface Animal : NSObject
{
int age;//年龄
int weight;//重量
}
@end
@implementation Animal
@end
@interface Dog : Animal
{
int no;//编号
}
@end
@implementation Dog
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
Dog *dog = [[Dog alloc] init];
NSLog(@"%zd\n",class_getInstanceSize([Animal class]));
NSLog(@"%zd\n",malloc_size((__bridge const void *)(animal)));
NSLog(@"%zd\n",class_getInstanceSize([Dog class]));
NSLog(@"%zd\n",malloc_size((__bridge const void *)(dog)));
}
return 0;
}
- Animal 和Dog编译后的结构如下,int在arm64系统下为4个字节
struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
int weight;
};
struct Dog_IMPL {
struct Animal_IMPL Animal_IVARS;
int no;
};
- Animal通过class_getInstanceSize得到的大小为16,malloc_size得到的大小也为16。这时候也许会有疑问,NSObject对象都16了,为何加了两字段还是16。其实NSObject对象虽然占16字节,但其后面8个字节并未存放任何数据,因此系统会去将这两个int值填充进去,而不是new更多内存。
- Dog通过class_getInstanceSize得到的大小为20?还是24呢?因为class_getInstanceSize获取的是字节对齐后成员变量所占内存大小,因此结果为24,结构体字节对齐大家可以百度去了解一下。那通过malloc_size得到的大小是多少呢?有点意外,不是字节对齐后的24,而是32,这里涉及到一个新的知识点,操作系统内存对齐的概念,为了访问内存最优,操作系统也需要对开辟内存空间有要求,由于这个知识点涉及的知识有点多,大家可以自行搜索相关文章。libmalloc中对于操作系统字节对齐,有一个这样的宏,也就是说iOS操作系统在对内存分配的时候以16的倍数进行内存分配,而Dog实际占用字节为24,靠近32,因此系统会分配32个字节。
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
总结:
iOS对象本质,讲到这里就结束了。之所以把我自己思考问题的过程写在文章里,是希望能够帮助大家在了解其他知识的时候,多一个思考的方向,也希望各位能提出疑问,我尽量查漏补缺。