NSObject的内存本质
NSObject的定义
在Xcode中进入查看NSObject的定义如下
@interface NSObject {
Class isa;
}
NSObject的底层实现
在终端进入对应文件夹目录下使用以下代码可以生成对应文件的c++代码(以main.m为例)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp- 如果需要链接其他框架,使用-framework参数。比如-framework UIKit
在main.cpp文件中可以找到以下结构体
struct NSObject_IMPL {
Class isa;
};
这个结构体就是NSObject的底层实现;由此可知一个NSObject对象本质上就只有一个isa指针,而指针在arm64架构上是占8个字节的。所以NSObject类的实例大小是8个字节。
OC的runtime函数同样提供了一个class_getInstanceSize()函数(该函数返回的是内存对齐后的大小)去获取对应实例的大小,但是这个并不是实例所分配到的内存大小。要想获取实例分配到的内存大小需要使用malloc_size()(注意传的参数需要进行桥接)函数。
代码验证如下:
NSObject *objc = [[NSObject alloc] init];
// 实例对象实际需要的内存
NSLog(@"---%zd---",class_getInstanceSize([objc class]));
// 获得objc所指向的内存大小(实际分配内存)
NSLog(@"---%zd---",malloc_size((__bridge const void *)(objc)));
输出结果分别为8和16。(如果想要查看这两个函数的具体实现可以去opensource.apple.com/tarballs/ob…上下载对应的源码查看)
分配内存一般都是调用alloc函数,在OC中实际上是调用了allocWithZone函数,在源码NSObject.mm文件中找到函数的实现:
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
继续在objc-runtime--new.mm文件中找到_objc_rootAllocWithZone函数的实现如下:
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
继续找_class_createInstancesFromZone函数的实现如下:
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是分配内存的大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
在objc-runtime--new.h文件中找到cls的instanceSize方法:
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;
}
由上可知当实例对象所需的内存至少是16个字节
一个NSObject对象只有一个指针所以它占16个字节
通过LLDB指令窥探NSObject内存
常用的LLDB指令如下:
- print、p:打印
- po:打印对象
读取内存
- memory read/数量格式字节数 内存地址
- x/数量格式字节数 内存地址
- x/3xw 0x10010(表示打印三串数据每串数据都是16进制占4个字节)
格式
x是16进制,f是浮点,d是10进制
字节大小
b:byte 1字节,h:half word 2字节
w:word 4字节,g:giant word 8字节
修改内存的中的值
-
memory write 内存地址 数值
-
memory write 0x000010 10
由图可知一个NSObject对象它占16个字节
Student的本质
将如下代码转换为c++代码后
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
}
return 0;
}
找到Student的定义如下:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
之前已经知道一个 NSObject_IMPL 结构体内部只有一个isa指针(arm64下占8个字节),两个int占8个字节,加起来刚好是16个字节。
运行以下代码:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
struct NSObject_IMPL {
Class isa;
};
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d,age is %d",stuImpl->_no,stuImpl->_age);
NSLog(@"%zd",class_getInstanceSize([Student class]));
NSLog(@"%zd",malloc_size((__bridge const void *)stu));
}
return 0;
}
在stuImpl处下断点,找到内存地址后使用Xcode自带的View Memory功能输入地址后
graph LR
Debug --> B[Debug Workflow]--> C[View Memory]
可以找到 _no 和 _age 的值如上图。输入LLDB指令后重新查看内存:
放开断点后打印如下:
实际上Student对象的内存分配就类似下图:
更复杂的继承结构
如下代码运行后:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
//struct NSObject_IMPL {
// Class isa;
//};
//
//struct Person_IMPL {
// struct NSObject_IMPL NSObject_IVARS;
// int _age;
//};
//
//
//struct Student_IMPL {
// struct Person_IMPL Person_IVARS;
// int _no;
//};
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
NSLog(@"stu - %zd",class_getInstanceSize([Student class]));
NSLog(@"stu - %zd",malloc_size((__bridge const void *)stu));
Person *person = [[Person alloc] init];
NSLog(@"person - %zd",class_getInstanceSize([Person class]));
NSLog(@"person - %zd",malloc_size((__bridge const void *)person));
}
return 0;
}
输出结果是:
说明当分配给Person的内存大小能够装的下Student的成员变量时会使用Person的内存。
给Student增加一个NSInteger修饰的_score后打印结果变为:
将 _score 放在 _no后运行后打印结果为:
这是因为内存对齐:结构体的大小必须是最大成员变量的倍数(此时Student结构体中最大的成员变量是Person_IMPL占16字节),所以stu对齐后返回的内存大小是32。