NSObject相关

465 阅读7分钟

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());
}
  1. 将代码转化为cpp源码,可以看到类对象底层实现其实就是一个简单的结构体,包含一个isa的类对象指针,在64位系统下,一个指针占用内存就是8个字节!
  2. 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 (nonatomicassignint 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

通过底层源码来窥探实例对象变量,方法,类方法等信息的存储情况:

实例对象

截屏2021-01-18 下午11.55.03.png 类对象

截屏2021-01-19 上午12.09.05.png 元类对象

截屏2021-01-18 下午11.54.32.png

  • 实例对象

小结

  • 对象方法、属性、成员变量、协议信息,存放在class对象中;
  • 类方法,存放在meta-class对象中;
  • 成员变量的具体值,存放在instance对象;

isa、superclass总结

640.webp

  • 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 类型的。

扫码_搜索联合传播样式-标准色版.png