NSObject知识
一个NSObject对象占用多少内存?
🌰
NSObject *object = [[NSObject alloc] init];
//获取类对象所占有内存大小
NSLog(@"%zu", class_getInstanceSize([object class]));
//获取NSObject实例对象所占用内存大小
NSLog(@"%zu", malloc_size((__bridge void *)object)); //16
class_getInstanceSize是获取类对象在内存中的大小是8个字节,而malloc_size是获取实例对象在内存中的大小是16个字节,那究竟一个NSObject在内存中是占用8个字节还是16个字节呢,我们来继续深入了解。
类对象大小
struct NSObject_IMPL {
Class isa;
};
size_t class_getInstanceSize(Class cls)
{
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
- 将代码转化为cpp源码,可以看到类对象底层实现其实就是一个简单的结构体,包含一个isa的类对象指针,在64位系统下,一个指针占用内存就是8个字节!
- class_getInstanceSize源码底层是获取类对象中变量所占用的内存大小,因为类对象结构体中只有一个isa指针变量,所以一个类对象占用内存大小是8个字节。所以类的本质是一个占用内存八个字节的结构体!
实例对象大小
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;
}
当我们使用alloc方法生成一个对象底层会调用到instanceSize方法,如果我们的对象中变量所占用的内存大小小于16个字节的话,系统默认会帮我们将对象内存大小扩充到16个字节,而实际中对象所使用到的内存只有8个字节而已!
🌰
@interface JZPerson : NSObject
{
@public
int age;
int num;
}
@end
@implementation JZPerson
@end
@interface JZBoy : JZPerson
{
@public
int sex;
int sex2;
}
@end
@implementation JZBoy
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
JZBoy *boy = [[JZBoy alloc] init];
NSLog(@"%zu", class_getInstanceSize([JZBoy class]));
NSLog(@"%zu", malloc_size((__bridge void *)boy));
}
return 0;
}
- 第一行输出结果是24,这里涉及到内存对齐问题,结构体的大小必须是最大成员内存大小的倍数,本来这里的变量占用内存大小只有20个字节,但是系统会帮我们分配24个字节,即8的整数倍。
- 第二行输出的结果是32,这里到底为什么是32,我们要继续分析alloc方法的底层实现代码!
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
void *bytes;
size_t size;
//获取对象中变量实际占用内存大小,extraBytes一般为0
size = cls->alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
//这个方法苹果帮我们的内存大小做了处理
bytes = calloc(1, size);
return objc_constructInstance(cls, bytes);
}
calloc(size_t num_items, size_t size) {
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
}
- 我们下载了malloc源码,可以看到苹果底层是以桶的概念来分配内存的,而且分配对象内存大小是以16字节的整数倍来进行分配的!
小结
- 类的本质其实是结构体来实现的,占用的内存大小为一个isa的类指针大小!
- 实例对象占用内存大小其实是按照16的整数倍来进行内存大小的分配,以空间换时间,提高系统的运行效率!
对象的isa指针指向哪里?
🌰
# define ISA_MASK 0x00007ffffffffff8ULL
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
//为了获取类对象的isa指针地址
struct NSObject_Class {
Class isa;
Class superclass;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
//instance 实例对象
Person *personObject = [[Person alloc] init];
//class 类对象
Class personClass = [personObject class];
//meta-class 元类对象
Class personMetaClass = object_getClass(personClass);
struct NSObject_Class *objectClass = (__bridge struct NSObject_Class *)personClass;
struct NSObject_Class *metaClass = (__bridge struct NSObject_Class *)personMetaClass;
}
return 0;
}
OC底层实现源码其实就是将对象的isa指针&ISA_MASK就得到了父类的地址。
- 实例对象personObject的isa&ISA_MASK = personClass类对象的地址;
- 类对象personClass的isa&ISA_MASK = personMetaClass元类对象的地址;
这个就是验证的结果:
(lldb) p/x personObject->isa
(Class) $0 = 0x001d8001000011d9 Person
(lldb) p/x personClass
(Class) $1 = 0x00000001000011d8 Person
(lldb) p/x 0x001d8001000011d9 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x00000001000011d8
(lldb) p/x objectClass->isa
(Class) $3 = 0x00000001000011b0
(lldb) p/x personMetaClass
(Class) $4 = 0x00000001000011b0
(lldb) p/x 0x00000001000011b0 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00000001000011b0
小结
- instance对象的isa指向class对象;
- class对象的isa指向meta-class对象;
- meta-class对象的isa指向基类的meta-class对象;
OC的类信息存放在哪里?
🌰
@interface Person : NSObject<NSCoding>
{
@public
int _age;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
- (void)name {
NSLog(@"name");
}
+ (void)eat {
NSLog(@"eat");
}
@end
通过底层源码来窥探实例对象变量,方法,类方法等信息的存储情况:
实例对象
类对象
元类对象
- 实例对象
小结
- 对象方法、属性、成员变量、协议信息,存放在class对象中;
- 类方法,存放在meta-class对象中;
- 成员变量的具体值,存放在instance对象;
isa、superclass总结
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class
- class的superclass指向父类的class,如果没有父类,superclass指针为nil
- meta-class的superclass指向父类的meta-class基类的meta-class的superclass指向基类的class
- instance调用对象方法的轨迹isa找到class,方法不存在,就通过superclass找父类
- class调用类方法的轨迹isa找meta-class,方法不存在,就通过superclass找父类
TaggedPoint
为了 NSNumber、NSString 等对象的使用效率提高和内存占用率降低,苹果爸爸引入了 TaggedPoint 使对象的一些操作变得更加方便和快速!
NSString *strShort = [NSString stringWithFormat:@"mjz"];
NSString *strLong = [NSString stringWithFormat:@"mjzjsygdsb"];
NSLog(@"%@--%@", [strShort class], [strLong class]);
NSLog(@"%p--%p", strShort, strLong);
//打印结果:
//NSTaggedPointerString--__NSCFString
//0x9129a2c95fa4749d--0x600003ee8e20
- 如果我们的字符串比较短,那么将使用 TaggedPoint 技术用对象指针地址来存放字符串的值。
- 如果字符串太长,那将使用对象的堆空间地址来存放字符串。
- 一个 NSTaggedPointerString 指针地址所占有内存在 64 位下占用空间是 8 个字节;一个__NSCFString 对象分别是指针所占用空间是 8 个字节加上对象所占有空间 16 个字节,所以一个 NSString 对象至少占用 24 个字节!
判断 TaggedPoint
#if OBJC_MSB_TAGGED_POINTERS (Mac项目下)
# define _OBJC_TAG_MASK (1UL<<63)
#else (iOS项目下)
# define _OBJC_TAG_MASK 1UL
- 在 Mac 项目下,是使用低地址位为 1 来判断是否为 TaggedPoint 类型的。
- 在 iOS 项目下,是使用高地址为 1 来判断是否为 TaggedPoint 类型的。