NSObject
NSObject 内存布局
下载 objc 源码:https://opensource.apple.com/tarballs/objc4/
,github 仓库:https://github.com/opensource-apple/objc4
下载 CoreFoundation 源码:https://opensource.apple.com/tarballs/CF/
,github 仓库:https://github.com/opensource-apple/CF
OC 代码经过编译器的 rewrite 后,会变成 C/C++代码,这是 OC 的底层。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
重写 main.m
文件:
int main(int argc, char * argv[]) {
NSObject *obj = [[NSObject alloc] init];
return 0;
}
产出 main.cpp
文件:
int main(int argc, char * argv[]) {
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"));
return 0;
}
在 Objective-C 中,所有的方法调用都通过 objc_msgSend
函数进行。
OC 的面向对象是基于 C/C++ 的数据结构实现的,在上面 main.cpp
文件中,找到的重写后的 NSObject
:
struct NSObject_IMPL {
Class isa;
}
Class
是什么?找到它的定义:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
NSObject
存储的内容就是一个 isa
指针,指针在 64 位系统所占内存空间是 8 个字节。
结构体的地址,就是它里面第一个成员的地 址。isa
指针的地址,也就是 NSObject_IMPL
结构体的地址。isa
存储的值,就是 NSObject
类对象的地址。
指针存储的值是别人的内存地址。指针存储了谁的内存地址,就称为指针指向了谁。此时访问这个指针,就等同于访问指针指向的那块内存。
下面探究 NSObject
占用多少内存:
#import <malloc/malloc.h>
#import <objc/runtime.h>
NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 8
// `sizeof(type)` yields the size in bytes of the object representation of type.
NSLog(@"%zd", sizeof([NSObject class])); // 8
// Returns size of given ptr
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)obj)); // 16
在 objc 源码中找到 class_getInstanceSize
的实现:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// 传入未对齐的内存大小,返回对齐后的内存大小
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
class_getInstanceSize
得到的值是一个对象的所有实例变量经过内存对齐后所需的内存。sizeof
是编译期的运算符,在编译期计算出操作数的所需内存。malloc_size
得到的值是运行时系统实际分配的值。
下面看看对象是怎么分配内存的,alloc
方法的描述:For historical reasons, alloc
invokes allocWithZone:
.
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
NEVER_INLINE
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);
}
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)
{
size = cls->instanceSize(extraBytes); // 获取应该分配的内存大小
obj = (id)calloc(1, size); // 分配内存
}
inline size_t instanceSize(size_t extraBytes) const {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
通过阅读源码得知,CoreFoundation
要求所有对象至少分配 16 个字节,属于框架的硬性规定。因此,64 位系统上, NSObject
分配了 16 字节内存,但其内部只用了 8 字节。
查看内存
这里显示的数字是 16 进制,16 * 16 = 2 ^ 8 = 256
,两个 16 进制数表示一个字节,可以看到前 8 个字节是对象的内容,后 8 个字节没有内容、全是 0。
lldb 指令 memory read
可以读取内存中的数据,简写 x
:
Printing description of obj:
<NSObject: 0x600002764480>
(lldb) x 0x600002764480
0x600002764480: e8 b6 4b 86 ff 7f 00 00 00 00 00 00 00 00 00 00 ..K.............
0x600002764490: 90 44 ac 54 17 31 00 00 4b 00 49 00 00 00 00 00 .D.T.1..K.I.....
x/nuf <addr>
:
- n 表示要显示的内存单元的个数
- u 表示一个内存单元的长度,g 对应 8 个字节
- f 表示显示方式,x 代表 16 进制
Printing description of obj:
<NSObject: 0x1060ac280>
(lldb) x/4g 0x1060ac280
0x1060ac280: 0x010000021de7e331 0x0000000000000000
0x1060ac290: 0x6b636950534e5b2d 0x426863756f547265
Person 内存布局
现扩展到 Person
类,
@interface Person : NSObject {
@public
int _age;
}
@end
同样地将 main.cpp
重写为 C++ 代码:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8个字节
int _age; // 4个字节
}
明明只需要 12 个字节,为什么获取实例大小返回 16 呢?这是因为内存对齐。内存对齐要求,结构体的内存大小必须是其最大成员的大小的整数倍。
NSLog(@"%zd", class_getInstanceSize([Person class])); // 16
给 _age
赋值为 4,然后查看内存,
现代计算机都是小端序 (Little-Endian),即,一个多位数的低位放在较小的地址处,高位放在较大的地址处。
所以这里的内存读取出来不是 0x04000000
,而是 0x00000004
。
将 Person
里的实例变量改为属性:
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
它重写后的结构体 还是和上面的 Person_IMPL
一样,不会有变化。而 getter
和 setter
则变成了函数,这些函数会存在类对象的方法列表里面,以供调用:
static int _I_Person_age(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_age)); } // self 指针偏移 8 个字节
static void _I_Person_setAge_(Person * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }
类可以被实例化成无数对象,因此结构体里面只存成员变量,实例方法在内存中有一份就够了,实例方法是不可能放在结构体里存的。