NSObject,再熟悉不过,它可以指向任何 Objective-C 对象,也就是说它是一切 Objective-C 类的基类,这和 Java 中的 Object 类很像。
创建形式为 [[NSObject alloc] init]
,那么,通过这种形式创建的 NSObject 对象在内存中有多大?究其根源,就要知道一个 NSObject 类型的指针所指向的内存空间布局是怎样的便可得知 NSObject 对象的占用内存的大小。但前提是要首先弄懂,Objective-C 代码的本质。
Objective-C 代码的本质
我们所编写的 Objective-C 底层都是 C\C++ 的代码,然后编译器将 C\C++ 代码转换为汇编语言代码,然后转成汇编语言代码又转成只有 1 和 0 的机器码,最终运行在手机上。
image.png
这也可得出结论,Objective-C 的相面对象都是基于 C\C++ 的数据结构实现的。
Objective-C 的代码也能通过命令转为 .cpp
格式的代码,命令如下:
clang -rewrite-objc 目标文件.m -o 目标文件.cpp
clang | Xcode 内置的 LLVM 编译器的前端 |
-rewrite-objc | 表重写 Objective-C 代码 |
-o | 表输出 |
运行后,目录下已多了一个 cpp 文件,打开这个文件查看内容会发现,即使内容很少的 Objetive-C 代码最终转成 C++ 代码也会高达数十万行,前面的 [[NSObject alloc] init]
最终也转成如下的形式:
NSObject* obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
但假如要参考平台、架构来将 Objective-C 代码转成 C ++ 代码 可通过如下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 目标文件.m -o 目标文件.cpp
xcrun | Xcode 内置的 工具 |
-sdk iphoneos | 表示最终的代码支持 iPhone 的硬件 |
-arch | 表示不同的架构,如 arm64、armv7、i386 |
不同的平台架构,最终得到的代码也是不一样的。
运行后得到的 cpp 体积和代码量都减少了很多,这份代码便是能够运行在 iPhone 手机上的 arm64 架构的代码。
NSObject 的本质数据结构
在代码中,可发现一块有关 NSObject 的结构体,如下:
struct NSObject_IMPL {
Class isa;
};
NSObject_IMPL 由名字可猜想这可能是 NSObject Implementation 的意思。
那么在查看 NSObject Definition 也能看到:
@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;
}
对比 C++ 和 Objective-C 有关 NSObject 的两种定义也可再一次验证 Objective-C 类的底层通过结构体来实现。
Class 的定义为
typedef struct objc_class *Class
,是指向结构体的指针。
既然是指针,在 64 位环境下占 8 个字节,32 位环境下占 4 个字节。那么可猜想,该结构体也可能只占 8 个字节,毕竟该结构体目前只有一个成员。
借助 runtime 函数验证猜想
在 runtime 中,通过 class_getInstanceSize(Class _Nullable __unsafe_unretained cls)
从字面意思上理解为可得到一个类的实例的大小。
此时打印:
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
得到结果:
8
此时似乎进一步验证了 8 个字节的猜想,但事实并非如此。
借助 malloc_size 函数验证猜想
通过 malloc_size(const void *ptr)
函数可获得指针指向的内存地址的大小。
此时打印:
NSObject* obj = [[NSObject alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)(obj)));
(__bridge const void *) 指将 Objective-C 指针转成 C 的指针。
打印结果:
16
借助 objc 源码
关于 Objective-C 某些底层的实现苹果已经开源,开源网址,objc4 文件夹就是 Objective-C 部分底层源码。
列表中可找到最新的源码下载。
class_getInstanceSize
项目搜索 class_getInstanceSize
可发现在实现中,调用了 alignedInstanceSize()
方法,关于该方法,注释是这么写的:
image.pngClass's ivar size rounded up to a pointer-size boundary.
大概就是说返回类的成员变量所占据的大小。
那么也就意味着,class_getInstanceSize
方法返回的是实力对象的成员变量的大小。
由上图可知,系统为一个 NSObject 对象分配了 16 个字节,但是真正使用的,即 isa 占用的仅有 8 个字节。
alloc
一个 NSObject 对象的实例化都是通过 alloc -> init 实现, 在源码中搜索 alloc 相关可找到 NSObject 的 id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
方法,接下来的调用顺序如下:
由图可知,最终给对象分配内存还是调用 C 语言的 alloc 方法。值得注意的是 instanceSize
方法,整个方法体是这样的:
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
if (size < 16) size = 16;
return size;
}
一目了然,这个函数中,假如创建的对象「体积」一旦小于 16,则直接分配 16 个字节。也就是说,一个 NSObject 对象,至少有 16 个字节的空间。
借助 Xcode
在 NSObject* objc = [[NSObject alloc] init]
后打上断点,运行到断点后可看到其内存地址,如下:
内存地址为 NSObject: 0x10065d6a0
然后 Debug -> Debug Workflow -> View Memory 可看到如下面板:
在最下方的 Address 输入 obj 的内存地址值,得到显示:
显示结果中,内存地址是以 16 进制显示,两个数表示一个字节(如 41、A1、B0...)
一个十六进制数表示 4 个二进制位,两个十六进制数表示 8 个二进制位,8 位表示一个字节,所以如 B0 这样的数表示一个字节。
上图中第一行即是 obj 的地址:
image.png
观察发现,刚好后八位都是 0 ,第二行开始的内存地址已然不是 obj 所属的空间了。41 A1 B0 A3 FF FF 1D 00
就是 isa 所占用的空间。
在终端借助 memeory read 内存地址 (或者 x 内存地址)
LLDB 指令可读取一段内存空间,如下:
从 41 - 00 都是 obj 所占用的内存空间。
另一种读取内存的 LLDM 指令 x/数量格式字节大小 内存地址
,表示从该内存地址后的若干地址空间读取。
数量 | 表示要打印多少内存地址 |
格式 | x 表示 16 进制,f 表浮点,d 表示 10 进制 |
字节大小 | b 表示 1 字节,h 表示 2 字节,w 表示 4 字节,g 表示 8 字节 |
如键入 x/3xg 0x10065d6a0
表示从 0x10065d6a0 打印 3 个地址空间,格式为 16 进制,每段地址空间占 8 个字节,如下显示:
第一串为 obj 的内存地址,0x001dffffa3b0a141 位 isa 占用的空间。
第二串起就不再是 obj 的空间。
修改内存地址的 LLDB 指令为
memory write 内存地址
。
结论
NSObject 本质 C/C++ 数据结构为结构体,有一个 Class 类型的成员变量 isa,在 64 位的环境当中,系统为其对象分配了 16 个字节的空间,但是实际使用(isa 占用)的只有 8 个字节。
延伸
复杂对象的内存结构
那么,更复杂的对象的内存分布是如何的?在此,本人用自己的名字创建了一个类,包括年龄(int 型)和身高(int 型)两个成员变量。
@interface Valenti : NSObject
{
@public
int _age;
int _height;
}
@end
转成 C++ 代码后可发现关于 Valenti 类其底层数据结构为下:
struct Valenti_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
由于 struct NSObject_IMPL NSObject_IVARS
中 NSObject_IMPL
只有一个成员 Class isa
则可转化为:
struct Valenti_IMPL {
Class isa;
int _age;
int _height;
};
对其初始化并赋值:
Valenti* v = [[Valenti alloc] init];
v -> _age = 26;
v -> _height = 183;
其内存分配如下:
image.png
当我在代码中用重新定义的 Valenti_IMPL 结构体类型的指针指向 Valenti 类型对象并打印结构体中 _age 和 _height 两个成员的时候发现:
struct Valenti_IMPL* v_struct = (__bridge struct Valenti_IMPL *)(v);
NSLog(@"%d, %d", v_struct -> _age, v_struct->_height);
打印结果为:
26, 183
可得出结论 v 所指向的对象本质为 Valenti_IMPL 结构。
借助 class_getInstanceSize
和 malloc_size
函数打印大小:
NSLog(@"%zd", class_getInstanceSize([Valenti class]));
NSLog(@"%zd", malloc_size((__bridge struct Valenti_IMPL *)(v)));
打印结果为:
16
16
可知,v 这个对象在内存中所占用的空间为 16(isa + _age + _height),成员占用情况如下图:
image.png
若去掉成员 _height,此时理论上内存空间占用为 isa 8 个 + _age 4 个 = 12 个字节,但在 struct 中有内存对齐法则:结构体占用内存大小必须为最大成员的整数倍,所以在此借用两个函数打印大小结果还是 16(isa * 2)。
那么,假如再加一个成员变量 int 型的体重打印结果又会如何??
经过上面的分析,参考内存对齐,我相信答案一定脱口而出为 24(身高 4 + 年龄 4 + 体重 4 + isa 8 = 20,遵循内存对齐结果是 24)但实际打印结果:
class_getInstanceSize() 的结果为 24
malloc_size() 的结果为 32
这又是为什么?此时还得回到 allocWithZone 的源码一探究竟。由于前面没有贴过源代码,在这里贴一下 _class_createInstanceFromZone
的源码,因为 allocWithZone() 最终会走到这里并调用 calloc():
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
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;
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
其中需要值得注意的是 size_t size = cls->instanceSize(extraBytes)
与 obj = (id)calloc(1, size)
两句,其中 extraBytes
是 0,从 _objc_rootAllocWithZone()
内部一路走到这里来可以发现,extraBytes 参数的值一直都是 0,而 instanceSize()
最终调用的是 alignedInstanceSize()
也就是和 class_getInstanceSize()
一样,那么 size 的值就是 24,但到了 obj 这里,obj 的 size 就变成了 32, 那么 calloc() 就很值得考究了,calloc() 为 C 标准库函数。
在开源网址中我们可找到有关内存的源码:
在 malloc.c 文件中找到 calloc() 函数,发现其内部调用的是 malloc_zone_calloc()
,其内部是现实:
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
return ptr;
}
这是系统的内存分配方式,也有一套内存对齐原则,这和前面的结构体 struct 的分配原则不同,但系统的内存分配原则也遵循着是谁的倍数的原则。内存中的堆空间可能是 16、32、48,最大是 256,源码中有最大空间的定义:
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
但在 iOS 系统中创建一个 Objective-C 对象,系统分配内存都是 16 的倍数,所以当 obj 的 size 为 24 的时候,系统会为其分配 32 的内存空间。
我们看到 libmalloc 源码中有很多 xxx_malloc.c 文件,这些内存分配都有着不同的原则,malloc.c 中的原则是通用的原则。
所以这里有个结论:
- class_getInstanceSize() 返回的是一个对象至少需要多少内存空间,这和运算符 sizeof() 很像;
- malloc_size() 返回得失系统为其分配多少内存空间。
更复杂的内存结构
首先声明 Person 类,只有一个 int 型年龄成员:
@interface Person : NSObject
{
@public
int _age;
}
@end
再声明 Valenti 类继承自 Person,并只有一个 int 型身高成员:
@interface Valenti : Person
{
@public
int _height;
}
@end
那么,一个 Valenti 实例占用多少内存空间?理论上为 Person 的 16 个字节 + _height 4个字节 = 20,再根据内存对齐法则可得占用空间为 16 的倍数 32,但当使用 class_getInstanceSize
和 malloc_size
函数打印大小结果为:
16
16
因为在 Person 的 16 个字节中,有 4 个是空余的,所以 Valenti 类中的 int 型身高刚好可以放到这 4 个空间内,所以最终结果为 16。
关于内存对齐
关于内存对齐,objc 源码中也有体现,上面提到过的 alignedInstanceSize()
中也是调用并返回了 word_align(unalignedInstanceSize())
的结果, word_align()
便已经「对齐」了内存。
网友评论