runtimeiOS-Runtime

探寻OC对象的本质

2019-10-22  本文已影响0人  二猪哥

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


OC代码的转化过程

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

OC如下代码

#import <Foundation/Foundation.h>

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 <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对象的内存布局入下图所示:


内存布局

二、一个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查看:
View Memory

输入objc的地址


内存空间

这个是什么意思呢?其实一个 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 方法往里面查看,从 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 相关文档。

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

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

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

(1)自定义一个 Student 类继承 NSObject :
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@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);   //<student: 0x102974240>
        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 实例对象的本质以及其在内存中布局如下图所示:


内存布局
内存布局

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

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

内存布局
我们知道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 <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

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;
}
内存布局

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

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

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

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 种:

(1) instance对象(实例对象)

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

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
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);
类对象

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));
元类对象

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,最后找到类方法的实现进行调用。

isa指针指向
(3)当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class类对象superclass指针。
[stu personMethod];
[stu init];

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

对象调用父类对象方法
(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指向图,经过上面的分析我们在来看这张图,就显得清晰明了很多。 superClass && isa指向图
总结如下:

对isa、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指针: 打印object的isa指针和objectClass的地址

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

ISA_MASK 我们通过位运算进行验证。 isa通过位运算计算出正确的地址

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

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

当我们以同样的方式打印objectClass->isa指针时,发现无法打印: 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);

此时我们重新验证一下:


objectClass2->isa

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

上一篇下一篇

猜你喜欢

热点阅读