权哥的技术之路GitHub 中文社区程序员

objc源码解析-ObjectiveC对象结构

2017-03-19  本文已影响812人  程序员钙片吃多了

概要

本文将从源码角度分析 Objective-C 对象的数据结构,阅读本文需要对 Objective-C 语言有基本了解。本文的源码来自objc4-706,可在该页面下载源码。另附一份可运行的Runtime源码。

一、Objective-C 对象定义

Objective-C 是一种面向对象的语言,NSObject 是所有类的基类。我们可以打开 NSObject.h 文件查看到 NSObject 的类定义如下:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

这里表示一个 NSObject 拥有一个 Class 类型的成员变量,那么这个 Class 是什么意思?我们可以在 objc4-706 源码的 objc-private.h 中看到如下两个定义:

typedef struct objc_class *Class;
typedef struct objc_object *id;

从第一个定义中可以看出,Class 其实就是 C 语言定义的结构体类型(struct objc_class)的指针,这个声明说明 Objective-C 的类实际上就是 struct objc_class。

第二个定义中出现了我们经常遇到的 id 类型,这里可以看出 id 类型是 C 语言定义的结构体类型(struct objc_object)的指针,我们知道我们可以用 id 来声明一个对象,所以这也说明了 Objective-C 的对象实际上就是 struct objc_object。

在 objc4-680 源码中我们跳转到 objc_class 的定义:

// note:这里没有列出结构体中定义的方法
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
};

从上面可以看到 objc_class 是继承自 objc_object 的,所以 Objective-C 中的类自身也是一个对象,只是除了 objc_object 中定义的成员变量外,还有另外三个成员变量:superclass、cache 和 bits。

所以,Objective-C 中最基本的数据结构就是:struct objc_object,objc_object 结构体定义如下:

// note:这里没有列出结构体中定义的方法
struct objc_object {
private:
    isa_t isa;
};

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
#elif __x86_64__
    // Definition for x86_64, not listed, check source code in objc-private.h.
# else
#   error unknown architecture for packed isa
#endif

Note: 上面对源码进行了简化,源码中有多个条件编译指令。为了解除阅读源码时对上述简化代码的疑惑,这里简单介绍下源码中几个条件编译宏(如果未阅读源码,可不必关心下面的解释):

  1. SUPPORT_PACKED_ISA:表示平台是否支持在 isa 指针中插入除 Class 之外的信息。如果支持就会将 Class 信息放入 isa_t 定义的 struct 内,并附上一些其他信息,例如上面的 nonpointer 等等;如果不支持,那么不会使用 isa_t 内定义的 struct,这时 isa_t 只使用 cls(Class 指针)。在 iOS 以及 MacOSX 上,SUPPORT_PACKED_ISA 定义为 1。
  2. __arm64__、__x86_64__ 表示 CPU 架构,例如电脑一般是 __x86_64__ 架构,手机一般是 arm 结构,这里 64 代表是 64 位 CPU。上面只列出了 __arm64__ 架构的定义。
  3. 对于 SUPPORT_PACKED_ISA(见第一点)的 isa 指针,SUPPORT_INDEXED_ISA 表示 isa_t 中存放的 Class 信息是 Class 的地址,还是一个索引(根据该索引可在类信息表中查找该类结构地址)。经测试,iOS 设备上 SUPPORT_INDEXED_ISA 是 0。
  4. 除了上述的条件编译宏,这里提一下 Union 结构,如果对 C 语言 union 不了解的可以参考 C/C++中 union 用法总结Why we need C Unions?

本节通过源码查看了 Objective-C 语言中的类、对象以及相关数据结构的定义,可以看出 isa_t 结构非常关键,下面来分析一下 isa_t 结构。

二、深入理解 isa_t

上面已经给出了 isa_t 在 __arm64__ 架构下的定义,这里继续以 __arm64__ 架构下的定义分析(__x86_64__架构下非常类似)。下图给出了 __arm64__ 架构下 isa_t 结构的内存布局:

Screen Shot 2017-03-23 at 21.58.11.png

isa_t 结构中的 struct 的成员变量都绘制在了上图中,下面逐个分析各个字段的含义:

nonpointer: 表示是否对 isa 指针开启指针优化

在说明 nonpointer 意义前,先简单介绍一下苹果为 64 位设备提出的节省内存和提高执行效率的一种优化方案:Tagged Pointer。

设想在 32 位和 64 位设备上分别存储一个 NSNumber 对象,其值是一个 NSInteger 整数。

首先,分析一下内存占用情况:

  1. 读写 NSNumber 对象的指针。在 32 位设备上,一个指针需要 4byte。在 64 位设备上,一个指针需要 8byte。

  2. 存储 NSNumber 对象值的内存。在 32 位设备上,NSInteger 占用 4byte。在 64 位设备上,NSInteger 占用 8byte。

  3. Objective-C 内存管理采用引用计数的方式,我们需要使用额外的空间来存储引用计数,如果引用计数使用 NSInteger,那么 64 位设备会比 32 位设备多用 4byte。

此外,从效率上讲,引用计数、生命周期标识等存储在其他地方,也有不少处理逻辑(例如为引用计数动态分配内存等)。

一般来说,32 位已经足够存储我们通常遇到的整数和指针地址了,那么在 64 位设备上,就有 32 位地址空间浪费掉了,存储一个值为 NSInteger 的 NSNumber 对象就浪费了 8byte 的空间(4byte 指针和 4byte value)。

为了节省内存以及提高程序执行效率,苹果提出了 Tagged Pointer,Tagged Pointer 简单来说就是使用存储指针的内存空间存储实际的数据。

例如,NSNumber 指针在 64 位设备上占用 8byte 内存空间,指针优化可以将 NSNumber 的值通过某种规则放入到存储 NSNumber 指针地址的 8byte 中,这样就减少了 NSInteger 所需的 8byte 内存空间,从而节省了内存。

另外,Tagged Pointer 已经不再是对象指针,它里面存放着实际数据,只是一个普通变量,所以它的内存无需在堆上 calloc/free,从而提高了内存读取效率。但是由于 Tagged Pointer 不是合法的对象指针,所以我们无法通过 Tagged Pointer 获取 isa 信息。关于 Tagged Pointer 更详细的介绍可参考:深入理解 Tagged Pointer-唐巧,这里不做深入介绍。

了解 Tagged Pointer 的概念后,再来看 nonpointer 变量。nonpointer 变量占用 1bit 内存空间,可以有两个值:0 和 1,分别代表不同的 isa_t 的类型:

  1. 0 表示 isa_t 没有开启指针优化,不使用 isa_t 中定义的结构体。访问 objc_object 的 isa 会直接返回 isa_t 结构中的 cls 变量,cls 变量会指向对象所属的类的结构,在 64 位设备上会占用 8byte。

  2. 1 表示 isa_t 开启了指针优化,不能直接访问 objc_object 的 isa 成员变量(因为 isa 已经不是一个合法的内存指针了,见 Tagged Pointer 的介绍),从其名字 nonpointer 也可获知这个 isa 已经不是一个指针了。但是 isa 中包含了类信息、对象的引用计数等信息,在 64 位设备上充分利用了内存空间。

对于 nonpointer 为 1 的 isa_t 的结构就是 isa_t 内部定义的结构体,该结构体中包含了对象的所属类信息、引用计数等。从这里也可以看出,指针优化减少了内存使用,并且引用计数等对象关联信息都存放在 isa_t 中,也减少了很多获取对象信息的逻辑,提高了执行效率。

shiftcls:

存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。

其余变量

其他几个变量很容易理解,这里不再做太多介绍。

  1. has_assoc
    该变量与对象的关联引用有关,当对象有关联引用时,释放对象时需要做额外的逻辑。关联引用就是我们通常用 objc_setAssociatedObject 方法设置给对象的,这里对于关联引用不做过多分析,如果后续有时间写关联引用实现时再深入分析关联引用有关的代码。

  2. has_cxx_dtor
    表示该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。

  3. magic
    用于判断对象是否已经完成了初始化,在 arm64 中 0x16 是调试器判断当前对象是真的对象还是没有初始化的空间(在 x86_64 中该值为 0x3b)。

  4. weakly_referenced
    标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。

  5. deallocating
    标志对象是否正在释放内存。

  6. extra_rc
    表示该对象的引用计数值,实际上是引用计数值减 1,例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10,则需要使用到下面的 has_sidetable_rc。

  7. has_sidetable_rc
    当对象引用技术大于 10 时,则需要借用该变量存储进位(类似于加减法运算中的进位借位)。

  8. ISA_MAGIC_MASK
    通过掩码方式获取 magic 值。

  9. ISA_MASK
    通过掩码方式获取 isa 的类指针值。

  10. RC_ONE 和 RC_HALF 用于引用计数的相关计算。

struct objc_object 中的方法

通过源码查看 struct objc_object,我们可以看到其中定义了很多方法。例如 isa_t 中的与类指针相关的两个方法:

Class ISA();
Class getIsa();

为什么需要定义方法来操作 isa 指针?这里只因为对 isa_t 的变量操作封装了方法是因为前面介绍了开启了指针优化的 isa 已经不是一个合法的指针了,我们无法直接操作对象的 isa 指针,只有通过方法来进行相应操作。

三、对象、类、元类

第一部分讨论了 NSObject、objc_object、objc_class 的定义,可以看出 Class 其实就是 C 语言定义的 objc_class 结构体,而 objc_class 继承自 objc_object,所以 Objective-C 中 Class 也是一个对象。第二部分深入解析了 objc_object 中唯一的成员变量的类型:isa_t,这部分讨论了 isa_t 中存放着对象所属的类的指针。第三部分我们来讨论在 Objective-C 的对象、类和元类(meta-class,后面会介绍)的关系。

刚刚提到,objc_class 继承自 objc_object,所以 objc_class 也是有 isa_t 类型的 isa 成员变量的,那么 objc_class 的 isa_t 中的 shiftcls 表示了什么意思呢?这里引入一个新的概念:元类。objc_class 的 isa_t 中的 shiftcls 就指向了 objc_object 的元类。什么是元类?

先来看一下 objc_class 除了继承自 objc_object 的成员变量 isa_t 外的三个成员变量:

  1. Class superclass:该变量指向父类的 objc_class;
  2. cache_t cache:该变量存放着实例方法的缓存,为了提高每次执行;
  3. class_data_bits_t bits:存放着实例的所有方法。

关于 Objective-C 的 Runtime 的方法查找这里不进深入讨论。不过,通过上面三个属性的解释,我们可以窥探出一个对象可以调用的方法列表是存储在对象的类结构中的。其实不存储在对象中的原因也很好理解,如果方法列表存放在对象结构中,那每创建一个对象,就要增加一个实例方法列表,资源消耗过大,所以存储在了类结构中。但是,除了实例方法外,一般还会法就存放在了上面提到的元类里。元类也是一个 objc_class 结构,结构中有 isa 和 superclass 指针,下图是 Objective-C Runtime 讲解中最经典的一张图:

meta_class.png

注意:上图中 isa 的箭头,其实并不是 isa 指针直接指向了相应结构,而是 isa_t 中的 shiftcls 指向了相应结构。

这里根据上述分析以及上图给出几个总结点:

  1. 每个类都有其对应的元类;

  2. 一个类的类结构中存储着该类所有实例方法,对象通过 isa 去类结构中获取实例方法实现,若该类结构中没有所需的实例方法,则通过 superclass 指针去父类结构查找,直到 Root class(class)。

  3. 一个类的元类中存储着该类所有类方法,类对象通过 isa 去元类结构中获取类方法实现,若元类结构中没有所需的类方法,则通过 superclass 指针去父类元类结构查找,直到 Root class(class)。

  4. 在 Objective-C 中,Root class(class)其实就是 NSObject,NSObject 的 superclass 指向 nil。

  5. 在 Objective-C 中,所有的对象(包含 instance、class、meta-class)都可以调用 NSObject 的实例方法。

  6. 在 Objective-C 中,所有的 class 以及 meta-class 都可以调用 NSObject 的类方法。

如果对于元类(meta-class)还不理解,推荐阅读 what is meta class in objective-c?,此文对 meta-class 的解释通俗易懂,建议阅读。

四、对象初始化过程

第四部分将从源码角度解析一个 NSObject 对象创建的过程。

我们知道创建一个 NSObject 对象的代码为:[[NSObject alloc] init];(还有一种方式是使用 [NSObject new],查看源码可以看到内部其实与第一种方式完全相同)。

+ (id)alloc {
    return _objc_rootAlloc(self);
}

_objc_rootAlloc(Class cls) {
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
    // means if (checkNil && !cls)) return nil;
    // besides, if checkNil && !cls probably to be false, "return nil" is optimized.
    if (slowpath(checkNil && !cls)) return nil; // 检查 cls 信息是否为 nil,如果为 nil,则无法创建新对象,返回 nil。

#if __OBJC2__ // If Objective-C 2.0 or later.
    if (fastpath(!cls->ISA()->hasCustomAWZ())) { // 检查类是否有默认的 alloc/allocWithZone 实现
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) { // 是否可以快速分配内存(这里跟踪源码可看到返回了 false)
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0); // 这里是创建对象的关键函数调用
            if (slowpath(!obj)) return callBadAllocHandler(cls); // 检查新建的对象是否合法
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil]; // 这里 cls 的 allocWithZone 方法里也是调用了 class_createInstance。
    return [cls alloc];
}

可以看到,新建一个对象主要是 callAlloc 函数。在该函数中有一个 slowpath,我们来看下定义:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

其中 __builtin_expect(EXP, N) 表示 EXP == N 编译器优化的 gcc 內建函数。通过这种方式,编译器在编译过程中会把可能性更大的 if 分支代码紧跟前面的代码,从而减少指令跳转带来的性能的下降。所以从逻辑上讲 if(slowpath(x)) 与 if(x) 的含义相同,只不过是多了编译器优化的内容。

上述代码中已经对一些语句进行了注释,该方法中大部分语句都是处理新建对象使用的 zone。我们这里重点分析 class_createInstance 函数,下面是源码中 class_createInstance 的实现:

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; // 检查 cls 是否合法

    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(); // 是否使用原始 isa 格式(见第二部分对 isa 的介绍)

    size_t size = cls->instanceSize(extraBytes); // 需要分配的空间大小,打开 instanceSize 实现可以知道对象是按照 16bytes 对齐的
    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;
}

上述函数实现中,也有了关键位置的注释,这里我们直接分析最重要部分: obj->initInstanceIsa(cls, hasCxxDtor); 该部分代码可以根据第二部分进行简化,简化后如下:

inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) {
    assert(!cls->instancesRequireRawIsa());
    assert(hasCxxDtor == cls->hasCxxDtor());
    initIsa(cls, true, hasCxxDtor);
}

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
    assert(!isTaggedPointer());
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
    }
}

上述代码中,newisa.bits = ISA_MAGIC_VALUE; 是为了对 isa 结构赋值一个初始值,ISA_MAGIC_VALUE 的值为 0x001d800000000001ULL,通过第二部分对 isa_t 的结构分析,我们可以知道此次赋值只是对 nonpointer 和 magic 部分进行了赋值。

newisa.shiftcls = (uintptr_t)cls >> 3; 是将类的地址存储在对象的 isa 结构中,
这里右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。关于类指针对齐的详细解析可参考:从 NSObject 的初始化了解 isa

初始化 isa 之后,[NSObject alloc] 的工作算是做完了,下面就是 init 相关逻辑:

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj) {
    return obj;
}

可以看到 init 其实只是返回了新建的对象指针,没有其他多余逻辑。

到这里新建一个对象的所有逻辑就结束了。

五、参考资料

1. 神经病院 Objective-C Runtime 入院第一天— isa 和 Class

2.从 NSObject 的初始化了解 isa

3.深入理解 Tagged Pointer

4.ObjC runtime 源码 阅读笔记(一)

5.What is a meta-class in Objective-C?

Note: 文中内容不代表权威,有任何问题都可以进行交流。转载请注明原文地址。

上一篇下一篇

猜你喜欢

热点阅读