探寻OC对象的本质

探寻OC对象的本质,我们平时编写的Objective-C代码,底层实现其实都是C\C++代码,如图所示:


OC代码的转化过程

OC的对象结构都是通过基础C\C++的结构体实现的。
我们通过创建OC文件及对象,并将OC文件转化为C++文件来探寻OC对象的本质。

OC如下代码

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        NSLog(@"objc的内存地址:%p",&objc);
    }
    return 0;
}

我们通过命令行将OC的mian.m文件转化为c++文件。(生成 main.cpp)

clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构 其中cpp代表(c plus plus)

我们可以指定架构模式的命令行,使用xcode工具 xcrun。(生成 main-arm64.cpp )

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 实现)。

NSObject_IMPL内部的实现

typedef struct objc_class *Class;
struct NSObject_IMPL {
    Class isa;
};
// isa 本质就是一个指向 objc_class 结构体的指针。

NSObjcet的底层实现,点击NSObjcet进入发现NSObject的内部实现。

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

转化为c语言其实就是一个结构体

struct NSObject_IMPL {
    Class isa;
};

一、一个OC对象在内存中是如何布局的?

一个OC对象的内存布局入下图所示:


探寻OC对象的本质_第1张图片
内存布局

二、一个NSObject对象占用多少内存?

既然 isa 本质上就是一个指针,一个指针在32位环境下占用4个字节,64位环境下占用8个字节。一个 NSObject 对象结构体内部就包含一个 isa 指针,那么,我们可以认为一个 NSObject 对象就占用8个字节么? NO NO NO,虽然表面如此,但是实际上并不是。那么一个NSObject对象占多大的内存空间呢?
实际上:
1、系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
2 、但 NSObject 对象内部只使用了8个字节的空间(64bit环境下,可以通过 class_getInstanceSize 函数获得)

NSObject *obj = [[NSObject alloc] init];

// 获得 NSObject 实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 输出为8

// 获得 obj 指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)(obj))); // 输出为 16
我们可以通过打断点。Xcode -> Debug -> Debug Workflow -> View Memory查看:
探寻OC对象的本质_第2张图片
View Memory

输入objc的地址


探寻OC对象的本质_第3张图片
内存空间

这个是什么意思呢?其实一个 NSObject 实例对象的大小确实为8个字节,但是系统给其分配的内存其实是16个字节。

接下来我们通过objc4源码来探究下到底是为什么。

objc源码: https://opensource.apple.com/tarballs/objc4/

打开下载好的objc4-750 —> objc-class.mm源码,搜索class_getInstanceSize方法:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

我们会发现这个方法返回值是cls->alignedInstanceSize(),点进去查看如下:

// Class's ivar size rounded up to a pointer-size boundary.
// 返回值成员变量的占用内存大小
uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
}

我们继续看下 malloc_size,由于苹果部分源码不公开,不过不影响今天讨论内容,我们先 malloc.h 文件中函数声明:

/* Returns size of given ptr */
// 注释意思:返回分配给指针的占用内存大小
extern size_t malloc_size(const void *ptr);

总结:通过阅读源码,发现一个 NSObject 对象,系统给其分配的空间为 16 个字节,只不过其真正利用起来的只有 8 个字节。

真的是分配 16 个字节么?
NSObject *obj = [[NSObject alloc] init];

上面这行代码,可以发现,创建一个新的实例对象,分为两步:

  • alloc:分配一块内存空间
  • init:初始化

所以,我们想探究实质的话可以从 alloc 方法往里面查看,从 alloc 开始搜索的话太多了,我们直接从 allocWithZone 开始查看,感兴趣的同学可以从 alloc 开始进行查看。

NSObject.mm

+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

allocWithZone 调用的是: _objc_rootAllocWithZone

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;
}

_objc_rootAllocWithZone 分配内存空间其实是: class_createInstance

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

继续点击进去查看:

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 {
            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 分配内存函数,我们发现传入了一个 size 参数, size 通过 cls 的 instanceSize 函数获得。

    uint32_t unalignedInstanceSize() {
        assert(isRealized());
        return data()->ro->instanceSize;
    }

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

    size_t instanceSize(size_t extraBytes) {
        //如果是 NSObject ,下面这行代码相当于 size_t size = 8;
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

通过注释和代码可以发现,CF:CoreFoundation,硬性规定,返回 size 最小为16
这是为什么呢,因为苹果设计 CF 框架,包括我们自己设计一套框架,为了我们的框架能够更好的运行,肯定会做出一些规定、约束,这样就可以理解了。
至于 word_align,涉及到 内存对齐 概念,下面的的章节也会提到一些,但不会涉及太深,感兴趣的同学可以 Google 相关文档。

三、一个自定义类的对象占用多少内存?

我们可以总结内存对齐为两个原则:

  • 原则 1. 前面的地址必须是后面的地址整数倍,不是就补齐。
  • 原则 2. 整个Struct的地址必须是最大字节的整数倍。

讲到这里,相信很多小伙伴还是有很多疑问的。刚才只讲了NSObject相关知识。我们平常开发中肯定不会只用NSObject对象,基本上都是我们自定义自己的对象,接下来,来通过两个复杂一点的例子来进行讲解。

(1)自定义一个 Student 类继承 NSObject :
#import 
#import 

@interface Student : NSObject{
    @public
    int _no;
    int _age;
}
@end

@implementation Student

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
              stu -> _no = 4;
              stu -> _age = 5;
              
        NSLog(@"%@",stu);   //
        NSLog(@"%zd",class_getInstanceSize([Student class]));  //16
    }
    return 0;
}
@end

按照上述步骤同样生成c++文件。并查找Student,我们发现Student_IMPL 整数

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

根据上文通过 NSObject 实例对象讲解的铺垫,Student 实例对象的本质以及其在内存中布局如下图所示:


探寻OC对象的本质_第4张图片
内存布局

探寻OC对象的本质_第5张图片
内存布局

从图中最下面把实例对象 stu 强转成结构体类型 stu2,通过结构体可以正常进行访问,也从另一角度证明 stu 底层结构确实为 Student_IMPL 结构体类型。当然也可以从 View Memory 或者 LLDB 进行证明。

内存布局这样画可能理解更清楚:

探寻OC对象的本质_第6张图片
内存布局

我们知道sutdent对象中,包含一个isa指针,一个int类型的_no成员变量,和一个int类型的_age成员变量,同样isa指针8个字节,_age成员变量4个字节,_no成员变量4个字节,刚好满足原则1和原则2,所以student对象占据的内存空间也是16个字节。
student实际占用内存为16字节,系统分配的内存也是16字节。

(2)举一反三,当 Person 继承 NSObject,Student 继承 Person 的情况,一个 Person 对象,一个 Student 对象占用多少内存空间?

Student: Person: NSObject:

#import 
#import 
#import 

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 {
    @public
    int _age;
}
@end

@implementation Person
@end

@interface Student: Person {
    @public
    int _no;
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu->_no = 5;
        NSLog(@"student:%zd, %zd", class_getInstanceSize([Student class]), malloc_size((__bridge const void*)stu));

        Person *per = [[Person alloc] init];
        per->_age = 4;
        NSLog(@"person:%zd, %zd", class_getInstanceSize([Person class]), malloc_size((__bridge const void*)per));
    }
    return 0;
}
探寻OC对象的本质_第7张图片
内存布局

我们先来分析下 Person 实例对象占用多少内存空间:

  • struct NSObject_IMPL NSObject_IVARS;即 isa 占用8个字节,int _age; 占4个字节,那么 Person 实例对象占 8 + 4 = 12 个字节么,错,上文中也有提到,一个 OC 对象至少占用 16 个字节,所以 Person 实例对象占用 16 个字节。

  • 从另外一个角度,其实还有 内存对齐 这个概念,就算是没有 OC对象 至少占用 16 个字节这个规定, Person_IMPL 也占用 16 个字节,内存对齐有一条规定:结构体的大小比必须是最大成员大小的倍数。

内存对齐还有很多规定,属于计算机知识范畴,感兴趣的同学可以自行 Google。

我们再来分析下 Student 实例对象占用多少内存空间:

  • struct Person_IMPL Person_IVARS:占用 16 个字节,int _no:占用 4 个字节,16 + 4 = 20,而且刚讲了内存对齐规定结构体大小必须是最大成员变量大小的倍数,那么, Student_IMPL 占用 16 * 2 = 32 个字节么?错!结果为 16 * 1 = 16 个字节。
  • 为什么呢?因为 Person_IMPL 虽然分配16个字节,但是实际变量只占用了 12 个字节,还有 4 个字节空出来了,我们伟大的 iOS 系统会这么傻,白白浪费这 4 个字节的空间么,当然不会,**所以,int _no;其实被放到了 Person_IMPL 空余的 4 个字节空间当中。

malloc_size 我们已经没有太多疑问了,但是可能对 class_getInstanceSize 还存在疑问,class_getInstanceSize 返回 ivar size,即成员变量 size,那么上文 Person instance size 为什么不返回 12 呢?

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());
}

通过源码可以发现,class_getInstanceSize 实际返回的其实也是 word_align(unalignedInstanceSize()); 内存对齐过的大小。

四、OC对象的分类

Objective-C中的对象,简称OC对象,主要可以分为 3 种:

  • instance对象(实例对象)
  • class对象(类对象)
  • meta-class对象(元类对象)
(1) instance对象(实例对象)

instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
探寻OC对象的本质_第8张图片
instance对象

object1和object2都是NSObject的instace对象(实例对象),但他们是不同的两个对象,并且分别占据着两块不同的内存。
instance对象在内存中存储的信息包括:isa指针、其他成员变量。

(2) class对象(类对象)

我们通过class方法或runtime方法得到一个class对象。class对象也就是类对象

// instance 对象,实例对象
NSObject *object = [[NSObject alloc] init];

// class 对象,类对象      
Class objectClass1 = [object class];
Class objectClass2 = [NSObject class];
// class 方法返回的一直是class对象,类对象
// Class objectClass2 = [[[[NSObject class] class] class] class];
Class objectClass3 = object_getClass(object); // runtime

// 0x100444830 0x7fffa5ab9140 0x7fffa5ab9140 0x7fffa5ab9140
NSLog(@"%p %p %p %p", object, objectClass1, objectClass2, objectClass3);
探寻OC对象的本质_第9张图片
类对象

1、objectClass1~objectClass3都是NSObject的class对象(类对象)。
2、每一个类在内存中有且只有一个class对象。可以通过打印内存地址证明。
3、class对象在内存中存储的信息主要包括:
isa指针、superclass指针、类的属性信息(@property)、类的对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar,成员变量的值时存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了;所以存储在class对象中。)。

(3)meta-class对象(元类对象)
// class 对象,类对象
Class objectClass = [object class];

// meta-class 对象,元类对象
// 将类对象当做参数传入,获得元类对象;将实例对象当做参数传入,获得的是类对象
Class metaClass1 = object_getClass([object class]);
Class metaClass2 = object_getClass([NSObject class]);
Class metaClass3 = object_getClass(object_getClass(object));

// 0x7fffa5ab90f0 0x7fffa5ab90f0 0x7fffa5ab90f0 0 1
NSLog(@"%p %p %p %d %d", metaClass1, metaClass2, metaClass3, class_isMetaClass(objectClass), class_isMetaClass(metaClass1));
探寻OC对象的本质_第10张图片
元类对象

1、objectMetaClass是NSObject的meta-class对象(元类对象)。
2、每个类在内存中有且只有一个meta-class对象。
3、meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括:isa指针、superclass指针、类的类方法的信息(class method)。
4、meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。

注意:为什么说 meta-class 对象和 class 对象结构一样,但是图上画的却不一样呢,因为图上只是将比较重要的一些东西摘了出来,方便理解。其实本质是,class对象类方法信息存储的可能是null空的,meta-class内部属性信息、对象方法信息、协议信息、成员变量信息存储的可能是null空的。

objc_getClass、object_getClass方法区别?

本文和上篇文章有用到这几个方法,这几个方法有什么区别呢?

Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;
    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}

Class object_getClass(id obj)
{
    // 如果传入instance对象,返回class对象
    // 如果传入class对象,返回meta-class对象
    // 如果传入meta-class对象,返回NSObject的meta-class对象
    if (obj) return obj->getIsa();
    else return Nil;
}

从源码进行分析,objc_getClass传入参数为字符串,根据字符串去Map中取出类对象并返回。 object_getClass传入参数为 id,并且返回值是通过 getIsa 获得,说明返回 isa 指向的类型(即:传入instance对象,返回类对象;传入class对象,返回meta-class对象;传入meta-class对象,返回NSObject的meta-class对象)。

五、isa指针

我们发现,OC对象不管是instance对象、类对象还是meta-class都有一个isa指针,那么,isa指针都指向哪里呢,起到了什么作用。

我们都知道,OC对象调用方法是通过消息机制实现的,通过上面的总结我们也知道了实例方法存放在class对象中,类方法存放在meta-class对象中,那么对象是怎么查找到方法并实现调用呢?

这个时候就需要isa指针了,instance对象的isa指针指向class对象,class对象的isa指向meta-class对象,通过isa指针,instance对象、class对象、meta-class对象就可以串起来了,方法调用、以及各种作用就都可以实现了。

(1)当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class类对象中的,那么要想找到实例方法,就必须找到class类对象,那么此时isa的作用就来了。
[stu studentMethod];

instance对象的isa指向class对象,当调用对象方法时,通过instance对象的isa找到class对象,最后找到对象方法的实现进行调用。

(2)当类对象调用类方法的时候,同上,类方法是存储在meta-class元类对象中的。那么要找到类方法,就需要找到meta-class元类对象,而class类对象的isa指针就指向元类对象。
[Student studentClassMethod];

class的isa指向meta-class
当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用。

探寻OC对象的本质_第11张图片
isa指针指向
(3)当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class类对象superclass指针。
[stu personMethod];
[stu init];

当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用,同样如果Person发现自己没有响应的对象方法,又会通过Person的superclass指针找到NSObject的class对象,去寻找响应的方法。

探寻OC对象的本质_第12张图片
对象调用父类对象方法

(4)当类对象调用父类的类方法时,就需要先通过isa指针找到meta-class,然后通过superclass去寻找响应的方法。
[Student personClassMethod];
[Student load];

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

注意:isa指针并不是直接指向对象地址值,还需要逻辑与上一个掩码 ISA_MASK,这个了解下就行,如果不了解的话可以直接理解为isa直接指向class对象、meta-class对象。

最后附上这张经典的isa指向图,经过上面的分析我们在来看这张图,就显得清晰明了很多。
探寻OC对象的本质_第13张图片
superClass && isa指向图
总结如下:

对isa、superclass总结:

  • instance的isa指向class
  • class的isa指向meta-class
  • meta-class的isa指向基类的meta-class,基类的isa指向自己
  • class的superclass指向父类的class,如果没有父类,superclass指针为nil
  • meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  • instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类。
  • class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类。

如何证明isa指针的指向真的如上面所说?

我们通过如下代码证明:

NSObject *object = [[NSObject alloc] init];
Class objectClass = [NSObject class];
Class objectMetaClass = object_getClass([NSObject class]);
        
NSLog(@"%p %p %p", object, objectClass, objectMetaClass);

打断点并通过控制台打印相应对象的isa指针:
探寻OC对象的本质_第14张图片
打印object的isa指针和objectClass的地址

我们发现object->isa与objectClass的地址不同,这是因为从64bit开始,isa需要进行一次位运算,才能计算出真实地址。而位运算的值我们可以通过下载objc源代码找到。

探寻OC对象的本质_第15张图片
ISA_MASK

我们通过位运算进行验证。
探寻OC对象的本质_第16张图片
isa通过位运算计算出正确的地址

我们发现,object-isa指针地址0x001dffff96537141经过同0x00007ffffffffff8位运算,得出objectClass的地址0x00007fff96537140

接着我们来验证class对象的isa指针是否同样需要位运算计算出meta-class对象的地址。

当我们以同样的方式打印objectClass->isa指针时,发现无法打印:
探寻OC对象的本质_第17张图片
p/x objectClass->isa

同时也发现左边objectClass对象中并没有isa指针。我们来到Class内部看一下:

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

相信了解过isa指针的同学对objc_class结构体内的内容很熟悉了,今天这里不深入研究,我们只看第一个对象是一个isa指针,为了拿到isa指针的地址,我们自己创建一个同样的结构体并通过强制转化拿到isa指针。

struct xx_cc_objc_class{
    Class isa;
};

Class objectClass = [NSObject class];
struct xx_cc_objc_class *objectClass2 = (__bridge struct xx_cc_objc_class *)(objectClass);

此时我们重新验证一下:


探寻OC对象的本质_第18张图片
objectClass2->isa

确实,objectClass2的isa指针经过位运算之后的地址是meta-class的地址。

你可能感兴趣的:(探寻OC对象的本质)