NSObject 底层本质

  • 一、OC 转 C/C++
  • 二、NSObject 对象内存布局
  • 三、NSObject 内存大小
  • 四、OC 对象内存布局
  • 五、OC 对象内存大小
  • 六、对象分类(instance对象、class对象、meta-calss对象)
  • 七、isa 和 superClass 指针
  • 八、对象属性、方法数据结构

一、OC 转 C/C++

OC 的底层是通过 C\C++ 实现,所以 OC 代码编译过程一般是先将 OC 转为 C\C++ ,C\C++ 进一步转为汇编语言,最终转为机器代码。OC 的对象映射到 C\C++ 主要对应的是结构体,这里面的 “结构体” 并非 C 语言里面的结构体,而是 C++ 语言里面的结构体,而且这个概念仅限字面意思的结构体。严格来讲,其实C++ 中的struct关键字定义的是类,跟 class 关键字定义的类除了默认访问权限的区别,没有区别。C++ 中的 struct 对 C 中的 struct 进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。如:能包含成员函数、可以继承、可以实现多态。

通过 xcrun 命令可以将 OC 代码转为不同平台CPU下支持的 C\C++ 代码,如 OC 代码转为 arm64 架构 CPU 代码,对应的命令为:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

二、NSObject 对象内存布局

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

点击可查看NSObject定义为如下,可以看出 NSObject 类中包含了一个 isa 成员变量。

@interface NSObject  {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

上述代码借助 xcrun 命令生成的文件中包含如下代码,实际上NSObject的定义最终也是转为如下代码。

//其中 Class 的定义为:typedef struct objc_class *Class; 64位系统中,指针占据 8 个字节
struct NSObject_IMPL {
    Class isa; // 8个字节
};
// 可以看出 Class 是一个指针
// 64位系统中,该指针占据 8 个字节
typedef struct objc_class *Class

NSObject *obj = [[NSObject alloc] init];的内存布局如下。alloc相当于为右侧蓝色的结构体开辟一块空间,结构体中保存着 isa 成员,isa 成员的指针的地址相当于结构体地址空间,初始化成功后,结构体的地址赋值给 obj 对象,因此 isa 地址和 obj 地址相同。

三、对象内存大小

3.1 查看内存管大小

  //#import 
  NSObject *obj = [[NSObject alloc] init];
  // 获得NSObject实例对象的成员变量所占用的大小 >> 8
  NSLog(@"%zd", class_getInstanceSize([NSObject class]));
  // 获得obj指针所指向内存的大小 >> 16
  NSLog(@"%zd", malloc_size((__bridge const void *)obj));

class_getInstanceSize方法可以获取实例对象的成员变量大小,即创建一个实例对象,至少需要多少内存。malloc_size方法可以获取对象指针所指向内存大小,即创建一个实例对象,实际上分配了多少内存。两个方法的区别具体可以看runtime底层源码观察其区别。OC 底层源码一般可在该网站查看。

3.2 class_getInstanceSize 函数

class_getInstanceSize底层实现如下,其中英文注释很清晰的描述了该方法返回的是成员变量(Class's ivar)大小。

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() {
        return word_align(unalignedInstanceSize());
    }

3.3 alloc 函数

OC 中的alloc在底层调用 runtime 的allocWithZone方法:

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}
id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    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 {
          //该处调用了C语言的 calloc 函数开辟空间,所以查看NSObject 对象的创建开辟空间大小应当依次为切入点,查看size 参数来源
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

上述代码调用了C语言的 calloc函数开辟空间,所以查看NSObject 对象的创建开辟空间大小应当依次为切入点,查看size参数来源。alignedInstanceSize()方法是 class_getInstanceSize 方法底层来源,所以下面代码中的extraBytes变量值实际为 0。非常值得注意的是,下述代码中明确指出CF requires all objects be at least 16 bytes.,即 CF 对象至少为 16 位大小。

size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

3.4 小结

综上,系统分配了 16 个字节给 NSObject 对象(通过 malloc_size 函数获得),但 NSObject 对象内部只使用了 8 个字节的空间,这8个字节主要用来存放 isa( 64bit 环境下,可以通过 class_getInstanceSize 函数获得)。

四、OC 对象内存布局

4.1 简单对象

@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];
    }
    return 0;
}

利用 xcrun 命名生成 C/C++ 代码,代码中会包含如下部分:

struct Student_IMPL {
    Class NSObject_IMP NSObject_IVARS;
    int _no;
    int _age;
};

其中NSObject_IMP 定义为:

struct NSObject_IMPL {
    Class isa; 
};

Student_IMP底层进而可转为如下结构,不难看出如果对象之间存在继承关系,最终子类转化对应的结构体会包含父类的结构体成分,且父类结构体成份在前。

struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

为了进一步说明OC对象底层是结构体实现的,可以将OC对象强制转为结构体,发现代码依然正常运行。

Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
//使用__bridge 将 OC 转为 C
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);

结合上述代码,可以总结出Student对象的底层实现结构图:



内存布局大概是这样,stu 指针指向结构体地址,结构体地址即为首个成员变量的地址。


4.2 继承对象

如果是 Person 继承自 NSObject ,Student 继承自 Person 类,Person 中包含 age 成员变量, Student 包含 no 成员变量,则底层实现结构如下图。


表面上看 Person_IMPL 占据 8 + 4 = 12 个字节,但是 OC 中明确指出一个 NSObject 对象大小至少为 16 字节。从内存对齐角度分析来看,Person_IMPL结构体也至少为 16 字节,即结构体的大小必须是最大成员的倍数,即8 * 2 = 16。

表面上看 Student_IMPL 占据 16 + 4 = 20 个字节实际并非如此,应该为 16 字节。因为 Student_IMPL 中虽然包含Person_IMPL,但是在继承关系中,Person_IMPL中 age 对应的内存空间并未被使用,所以应该是 8 + 4 + 4 = 16 字节。

五、OC 对象内存大小

@interface Person : NSObject
{
    int _age;
    int _height;
    int _no;
}
@end
Person *p = [[Person alloc] init];
NSLog(@"%zd  %zd",class_getInstanceSize([Person class]), malloc_size((__bridge const void *)(p))); // 24 32

p 对象对应的结构体为:

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
    int _no;
}; // 计算结构体大小,内存对齐,24

上述代码打印结果分别为 24 和 32,按照前面所说的结构体内存对齐原则来说,打印结果应该都为 24,即 8 的最小倍数, 8 * 3 = 24。class_getInstanceSize为结构体内存大小 8 + 4 + 4 + 4 = 24,但和实际情况确有一些出入。

size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

结合 3.3 小结的源码来分析来看,class_getInstanceSizealloc方法最终本质都是调用了alignedInstanceSize函数,其中alloc调用alignedInstanceSize函数中间使用到了 C 语言的 calloc函数,并且还涉及到一个变量extraBytes,但是此变量的值一般为 0,所以综合来看 malloc_size((__bridge const void *)(p))的值为 32 和calloc密不可分。因为在calloc函数中存在内存对齐一说(注意此内存对齐不等同于结构体内存对齐),即在堆区每次分配内存为 16 的倍数。如果比较感兴趣,可进一步查看 calloc函数源码 libmalloc。

六、对象分类(instance对象、class对象、meta-calss对象)

6.1 OC 对象分为三类

isa
  • instance 对象(实例对象):内部包含成员变量的具体值、isa 指针
  • class 对象(类对象):内部包含对象方法、属性、成员变量、协议信息、isa 指针、superclass 指针
  • meta-class 对象(元类对象,即描述对象的对象,元数据是指描述数据的数据):内部包含类方法、isa 指针、superclass 指针。

无论是instance、class、meta-class 底层本质上都是 OC 对象。

6.2 类对象和元类对象获取方式

// 获取类对象的方式
[NSObject class];
[obj class];

// 获取元类对象,
// 传入 instance 返回 class 对象
// 传入 class 返回 meta-class 对象
// 传入 meta-class 返回 NSObject 的 meta-class 对象
Class metaClass= object_getClass([NSObject class]);
// 判断是否为元类对象
class_isMetaClass([NSObject class]);

七、isa 和 superclass 指针

类对象的 superclass 指针

当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用。

元类对象的 superclass 指针

当Student的class对象要调用Person的class方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到class方法的实现进行调用。

isa 和 superclass

instance的isa指向class;class的isa指向meta-class;meta-class的isa指向基类的meta-class。

class的superclass指向父类的class,如果没有父类,superclass指针为nil。meta-class的superclass指向父类的meta-class,基类(Root)的meta-class的superclass指向基类的class(即类方法找不到时会调用对象方向,对象方法找不到才会报错)。

instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类。class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类。

相关面试题

为什么 instance、class、meta-class 对象中都包含 isa 指针成员变量?
因为所有的 OC 类都继承自 NSObject。

对象的isa指针指向聊里?
instance对象的isa指向class对象
class对象的isa指向meta-class对象
meta-class对象的isa指向基受的meta- class对象

0C的类信息存故在哪里?
0对象方法、属性、成员变量、协议信息,存故在class对象中
0类方法,存故在meta-class对象中
0成员变量的具体值,存故在instance对象

八、对象属性、方法数据结构

无论是class对象、meta-class对象,其底层数据结构都和上图保持一致。

bits & FAST_DATA_MASK 可以获取到对应的类信息,类信息中包含方法列表、属性列表、协议列表等信息。

你可能感兴趣的:(NSObject 底层本质)