探寻OC对象的本质

本文主要通过三个问题,一起探寻一下OC对象实现的底层原理。

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

Objective-C的本质

我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。

image.png

其实Objective-C的面向对象都是基于C\C++的结构体实现的。
我们可以通过下面的命令将Objective-C代码转换为C\C++代码来看一下具体的实现。

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

OC代码

#import <Foundation/Foundation.h>

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

转换的C++文件


image.png

我们都知道每个OC对象都包含一个isa指针,在NSObject的头文件中,我们可以看到如下代码:

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

typedef struct objc_class *Class;

在转换的C++代码中,我们也可以看到类似的结构体:

struct NSObject_IMPL {
    Class isa;
};

struct NSArray_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

struct NSString_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

struct NSDictionary_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

不难猜想,struct NSObject_IMPL其实就是NSObject的底层实现。

我们思考一下,一个OC对象在内存中是如何布局的?

image.png

NSObject其实就是结构体struct NSObject_IMPL,包含了一个isa指。在64位架构下,一个指针地址占用8个字节,是不是NSObjec对象所占用的内存就是8个字节呢?下面我们通过几种方法验证一下。

  1. 使用runtime里的class_getInstanceSize ()方法,获得NSObject实例对象的成员变量所占用的大小。

    image.png

  2. 使用malloc_size(),获得obj指针所指向内存的大小。

    image.png

为什么两次结果会截然不同呢?我们看一下class_getInstanceSize的具体实现。

// May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

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

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

从源码里我们可以看到,系统在计算分配内存的时进行了字节对齐。
64位系统下define WORD_MASK 7UL

假设环境是64位机器, 传进来的数字是2
return (2 + 7) & (~7);
return (0000 1001) & (~0000 0111);
return (0000 1001) & (1111 1000) = (0000 1000)
在计算机中, 位运算效率很高。

那为什么通过malloc_size ()方法获取的是16个字节呢?

我们知道OC中创建一个对象需要调用alloc方法,下面我们通过设定一个符号断电objc_alloc_init看一下实际的调用堆栈。

image.png

通过调用堆栈信息,我们发现alloc方法会调用_objc_rootAllocWithZone方法,下面我们看一下这个方法的具体实现:

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

我们发现,系统为对象开辟内存空间实际调用的是这个obj = (id)calloc(1, size);,这里的size是需要分配的内存的大小。

size = cls->instanceSize(extraBytes);

size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

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

通过代码,我们不难发现,CF框架内部规定:当创建一个实例对象的时候,为其分配的空间不能小于16个字节。

总结:

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


对象的isa指针指向哪里?

OC的类信息存放在哪里?

OC对象的分类

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

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

instance

instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。NSObject *obj = [[NSObject alloc] init];

instance对象在内存中存储的信息包括:

  • isa指针
  • 其他成员变量

class

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];    
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = object_getClass(object1);
Class objectClass4 = object_getClass(object2);
Class objectClass5 = [NSObject class];

objectClass1 ~ objectClass5都是NSObject的class对象(类对象),每个类在内存中有且只有一个class对象。

class对象在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的属性信息(@property)、类的对象方法信息(instance method)
  • 类的协议信息(protocol)、类的成员变量信息(ivar)
  • ......

meta-class

Class objectMetaClass = object_getClass(objectClass5);

objectMetaClass是NSObject的meta-class对象(元类对象),每个类在内存中有且只有一个meta-class对象。
meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)
  • ......

class_isMetaClass()查看Class是否为meta-class

image.png
  • 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找父类
image.png

从64bit开始,isa需要进行一次位运算,才能计算出真实地址。

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

struct objc_class的结构

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
....
}

struct class_rw_ext_t {
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

...
}

image.png

总结

对象的isa指针指向哪里?
instance对象的isa指向class对象
class对象的isa指向meta-class对象
meta-class对象的isa指向基类的meta-class对象

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。