Date Notes Refer.
2019-03-18 首次提交 objc4-750
2019-08-18 抽离 isa 部分;
重整文章结构
Obj-C 中的 isa 指针 - kingcos
2019-08-29 增加 class_rw_t 等细节 -

0

Preface

Obj-C 中的对象分为实例对象(Instance Object)、类对象(Class Object)、以及元类对象(Meta-class Object)三种,本文将借助源码简单了解一下不同类型对象的真实构造。

1

为了方便下文举例,这里我们定义一个 EDdeviceProtocol 协议,并让继承自 NSObjectComputer 类遵守该协议:

// EDdeviceProtocol 协议
@protocol EDdeviceProtocol
- (void)powerOn;
@end

// Computer 类,继承自 NSObject,遵守 EDdeviceProtocol
@interface Computer : NSObject <EDdeviceProtocol> {
// 成员变量
@public
    int _memorySize;
    int _diskSize;
}

// 属性
@property (copy) NSString *model;

// 实例方法
- (void)print:(NSString *)content;

// 类方法
+ (NSString *)arch;
@end

@implementation Computer
- (void)print:(NSString *)content {
    NSLog(@"Print content: %@.", content);
}

// 协议方法
- (void)powerOn {
    NSLog(@"%s", __func__);
}

+ (NSString *)arch {
    return @"ARM 64";
}
@end

实例对象

// cpt 为指向 Computer 类型实例对象的指针
Computer *cpt = [[Computer alloc] init];

// Direct access to Objective-C's isa is deprecated in favor of object_getClass()
// cpt->isa;

cpt->_memorySize = 16;
cpt->_diskSize = 512;

cpt.model = @"MacBook Pro 13'";

cpt 指向的实例对象中将存放继承自 NSObject 基类的 isa 指针成员变量(但无法直接访问)以及其它成员变量的具体值:

// NSObject.h

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

但我们在类中定义的方法哪里去了?

类对象

由于方法并不需要每个类都单独保存一份,在 Obj-C 中,实例方法存放在类对象中,类方法存储在元类对象中。与实例对象不同,类对象和元类对象并不需要我们手动创建。类对象可以通过 [obj class][Computer class] 或 Runtime API object_getClass 获取:

// 获取类对象的方法:
Class cptClass1 = [cpt class];
Class cptClass2 = [Computer class];
Class cptClass3 = object_getClass(cpt);

// LLDB:
// (lldb) p/x cptClass1
// (Class) $0 = 0x00000001000013d0 Computer
// (lldb) p/x cptClass2
// (Class) $1 = 0x00000001000013d0 Computer
// (lldb) p/x cptClass3
// (Class) $2 = 0x00000001000013d0 Computer

类对象的类型是 Class,本质是指向 struct objc_class 结构体的指针。类对象中存放了 isasuperclass、方法缓存、以及为该类实例对象所公用的一些信息:

// Object.mm

typedef struct objc_class *Class;

// objc-runtime-new.h
struct objc_class : objc_object {
    // Class ISA;
    // ➡️ 父类指针
    Class superclass;
    // ➡️ 方法缓存,详见 Reference
    cache_t cache;             // formerly cache pointer and vtable
    // ➡️ 可读可写表(class_rw_t)等
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    // ...
};

// objc-private.h
struct objc_object {
private:
    // ➡️ isa 指针
    isa_t isa;

// ...
}

class_rw_t

class_data_bits_t bits; 中主要存储着 class_rw_t 和自定义的标志位:

// objc-runtime-new.h

struct objc_class : objc_object {
    // ...

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

    // ...
}

#if !__LP64__
// data pointer
#define FAST_DATA_MASK        0xfffffffcUL
#elif 1
#define FAST_DATA_MASK          0x00007ffffffffff8UL // ✅
#else
#define FAST_DATA_MASK          0x00007ffffffffff8UL
#endif

struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;

    // ...

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

    // ...
}

class_data_bits_t bits; 中只有一个 uintptr_t bits,本质是 unsigned long 类型,占用 8 个字节。但其存储方式很类似位域,即将不同的数据存储在不同的位中:

Bits Expresstion
0 -
1 FAST_IS_SWIFT_LEGACY:标志 Swift 类是否来自于尚不稳定的 Swift ABI
2 FAST_IS_SWIFT_STABLE:标志 Swift 类是否来自于稳定的 Swift ABI
3 FAST_HAS_DEFAULT_RR:类或父类含有默认的持有或引用(Retain/Reference)retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
47 FAST_DATA_MASKclass_rw_t
63 -

class_rw_t 指的是可读可写(Read-Write)表。其中存储了指向 class_ro_t 只读表的指针、方法、属性、协议等信息:

// objc-runtime-new.h

// ➡️ 可读可写表
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    // ➡️ 只读表的指针(const:不可修改指针指向内存空间中的数据)
    const class_ro_t *ro;

    // ➡️ 方法、属性、协议信息
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    // ...
};

我们以 method_array_t methods; 为例看一下其类型:

// objc-runtime-new.h

// method_array_t 继承自 list_array_tt,其中 List 类型为 method_list_t,Element 类型为 method_t
class method_array_t :
    public list_array_tt<method_t, method_list_t>
{
    // ...
};

// list_array_tt 中定义了元素类型为 Element,列表为 List
template <typename Element, typename List>
class list_array_tt {
    // ...
};

// method_t 即是 Obj-C 中方法的结构(详见下文)
struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    // ...
};

// method_list_t 继承自 entsize_list_tt,其中 List 类型为 method_list_t,Element 类型为 method_t
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    // ...
};

// entsize_list_tt 是非脆弱(Non-fragile)结构体数组的一般实现
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
    // ...
};

由于 Obj-C 中的 Class 是一个不透明类型,但我们可以尝试一下将类对象的原始结构桥接到其中,首先需要将以下我整理好的内容放在 .h 引入,需要注意的是引入的实现文件需要支持 C++,即以 .mm 结尾:

-> 点击此处即可查看完整内容 <-

//
//  class_details.h
//  Powered by kingcos.me
//
//  Created by kingcos on 2019/8/31.
//  Copyright © 2019 kingcos. All rights reserved.
//

#ifndef class_details_h
#define class_details_h

#define FAST_DATA_MASK          0x00007ffffffffff8UL

typedef struct v_objc_class *V_Class;
typedef unsigned long v_uintptr_t;
typedef v_uintptr_t v_cache_key_t;
typedef uint32_t v_mask_t;
typedef v_uintptr_t v_protocol_ref_t;

struct v_bucket_t {
    IMP _imp;
    v_cache_key_t _key;
};

struct v_cache_t {
    struct v_bucket_t *_buckets;
    v_mask_t _mask;
    v_mask_t _occupied;
};

union v_isa_t {
    V_Class cls;
    v_uintptr_t bits;
};

struct v_objc_object {
    v_isa_t isa;
};

template <typename Element, typename List, uint32_t FlagMask>
struct v_entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
};

struct v_method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct v_method_list_t : v_entsize_list_tt<v_method_t, v_method_list_t, 0x3> {
};

struct v_protocol_list_t {
    uintptr_t count;
};

struct v_ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;
};

struct v_ivar_list_t : v_entsize_list_tt<v_ivar_t, v_ivar_list_t, 0> {
};

struct v_property_t {
    const char *name;
    const char *attributes;
};

struct v_property_list_t : v_entsize_list_tt<v_property_t, v_property_list_t, 0> {
};

struct v_class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;

    const uint8_t * ivarLayout;

    const char * name;
    v_method_list_t * baseMethodList;
    v_protocol_list_t*** * baseProtocols;
    const v_ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    v_property_list_t *baseProperties;
};

template <typename Element, typename List>
class v_list_array_tt {
    union {
        List* list;
        v_uintptr_t arrayAndFlag;
    };
};

class v_method_array_t :
public v_list_array_tt<v_method_t, v_method_list_t>
{
};

class v_property_array_t :
public v_list_array_tt<v_property_t, v_property_list_t>
{
};

class v_protocol_array_t :
public v_list_array_tt<v_protocol_ref_t, v_protocol_list_t>
{
};

struct v_class_rw_t {
    uint32_t flags;
    uint32_t version;

    const v_class_ro_t *ro;

    v_method_array_t methods;
    v_property_array_t properties;
    v_protocol_array_t protocols;
};

struct v_class_data_bits_t {
    v_uintptr_t bits;

    v_class_rw_t* data() {
        return (v_class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

struct v_objc_class : v_objc_object {
    V_Class superclass;
    v_cache_t cache;
    v_class_data_bits_t bits;

    v_class_rw_t *data() {
        return bits.data();
    }
};

#endif /* class_details_h */

@protocol Bar <NSObject>
@optional
- (void)optional;
@end

@interface Foo : NSObject <Bar>
@property (nonatomic, copy) NSString *fooStr;
@end

@implementation Foo
- (void)foo {
    NSLog(@"%s", __func__);
}
@end

@interface Foo (Baz)
@end

@implementation Foo (Bar)
- (void)bar {
    NSLog(@"%s", __func__);
}
- (void)optional {
    NSLog(@"%s", __func__);
}
@end

这样,我们将类或元类对象转换为其真实结构时就能看到其中的细节了:

V_Class fooClassObject = (__bridge V_Class)[Foo class];
v_class_rw_t *fooRWTable = fooClassObject->data();

4

有心的同学可能会发现,这里的方法列表个数显示为 6,根据上面的 Obj-C 源码,应当是 getter & setter 以及 foobaroptional,那么怎么会多一个呢?其实这是由于定义属性,增加了一个名为 .cxx_destruct 的方法,关于此本文不再涉及,考虑放置在 dealloc 相关的文章中。

class_rw_t 中直接存储的方法、属性和协议都继承自 list_array_tt<Element, List>,其中的值共有三种可能:

  1. 空;即没有定义任何内容
  2. 一个指向单一列表的指针;即只存储了编译时(包括类本身和分类中)确定的内容
  3. 一个指向列表的指针数组(类似二维数组);即存储了编译时确定以及运行时附加的内容

class_ro_t

class_ro_t 存在于 class_rw_t 中,是只读(Read-Only)表:

// objc-runtime-new.h

// ➡️ 只读表
struct class_ro_t {
    // ➡️ 标志位
    uint32_t flags;
    uint32_t instanceStart;
    // ➡️ 实例大小
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    // ➡️ 类名
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    // ➡️ 成员变量
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

而与 class_rw_t 不同的是,class_ro_t 中的方法、属性、成员变量分别直接继承自 method_list_tivar_list_tproperty_list_t, 少了一维。这是因为只读表只存储编译时(包括类本身和分类中)确定的内容,在运行时将不会再次附加其它的数据。

为了证明结论,我们再简单了解一下类在首次初始化中所执行的 realizeClass 方法:

// objc-runtime-new.h

// class is unrealized future class - must never be set by compiler
#define RO_FUTURE             (1<<30)
// class is realized - must never be set by compiler
#define RO_REALIZED           (1<<31)

// class_t->data is class_rw_t, not class_ro_t
#define RW_REALIZED           (1<<31)
// class is unresolved future class
#define RW_FUTURE             (1<<30)

// objc-runtime-new.mm

/***********************************************************************
* realizeClass
* Performs first-time initialization on class cls,
* 在类上执行首次初始化,
* including allocating its read-write data.
* 包括分配读写数据。
* Returns the real class structure for the class.
* 返回类的真实类结构。
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    // 只读表指针
    const class_ro_t *ro;
    // 可读可写表指针
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    // 判空
    if (!cls) return nil;
    // 判断是否已经实化
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    // 实化前,cls->data() 返回指向只读表的指针,因此可以强转
    ro = (const class_ro_t *)cls->data();
    // 判断是否为未来的类(即 RO_FUTURE 标志着读写数据是否已经被分配)
    // 可读可写表和只读表的首个成员均为 flags 且 RO_FUTURE 与 RW_FUTURE 实际值相同
    if (ro->flags & RO_FUTURE) {
        // 2⃣️
        // This was a future class. rw data is already allocated.
        // 将数据赋值给可读可写表
        rw = cls->data();
        // 将可读可写表中的只读表赋值给只读表
        ro = cls->data()->ro;
        // 更新标记位
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 1⃣️
        // Normal class. Allocate writeable class data.
        // 普通类,分配可写入类数据。
        // 分配空间
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        // 将可读可写表的只读表指针指向只读表指针
        rw->ro = ro;
        // 更新标记位
        rw->flags = RW_REALIZED|RW_REALIZING;
        // 将可读可写表写回类中
        cls->setData(rw);
    }

    // ...

    return cls;
}

method_t

method_t 是 Obj-C 中方法的本质类型:

// objc.h

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

// objc-ptrauth.h

#if __has_feature(ptrauth_calls)
// Method lists use process-independent signature for compatibility.
using MethodListIMP = IMP __ptrauth_objc_method_list_imp;
#else
using MethodListIMP = IMP;
#endif

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

method_t 中存储了方法的名称、类型和 IMP

Member Notes
SEL name; 方法名,又称方法选择器(Selector),同名方法即使不同类也拥有相同的 SEL 类型方法名;其底层数据类型为 struct objc_selector 但没有找到相关开源,可近似认为是 C 中 char * 字符串;可以使用 @selector()sel_registerName() 方法传入方法来得到方法名;也可通过 sel_getName()NSStringFromSelector()SEL 类型转换为 char *NSString
const char *types; 方法类型编码(Type Encoding),将方法的返回值、参数等信息编码的字符串;Obj-C 方法默认会有两个参数,即 id self & SEL op,因此默认就会有 @:,首个数字代表所有参数占用的总字节数,后续则代表每个参数的起始字节数;@encode();详细可见 Type Encodings - Objective-C Runtime Programming Guide
MethodListIMP imp; 方法实现,即方法的内存地址,类似 C 中的函数指针,可以通过该地址访问到方法的代码块进而执行

元类对象

元类对象和类对象一样,都是 Class 类型,但元类对象只可以通过 Runtime 的 object_getClass API 传入类对象,获取到元类对象。需要注意的是 [[Computer class] class][[cpt class] class] 都只能返回类对象。

对于 object_getClass,如果参数是实例对象,返回类对象;如果参数是类对象,返回元类对象;如果参数是元类对象,返回 NSObject(基类)的元类对象。class_isMetaClass 可以用来检查 Class 是否为元类对象。

VClass cptMetaVClass = (__bridge VClass)object_getClass([Computer class]);
class_rw_t *rwt = cptMetaVClass->data();

// LLDB:
// Printing description of rwt->methods->first:
// (method_t) first = {
//     name = "arch"
//     types = 0x0000000100000f86 "@16@0:8"
//     imp = 0x0000000100000d40 (DemoOC`::+[Computer arch]() at main.mm:52)
// }

元类对象中除了存储 isasuperclass 指针,还存储了类方法。其存放位置和类对象存放实例方法的存放位置(方法列表)一致。

superclass

3

What

superclass 是仅存在于类对象和元类对象的一个成员。类对象中的 superclass 指向当前类的父类,元类对象中的 superclass 指向该元类对象的父类。基类 NSObjectsuperclass 指向 nil,根元类对象的 superclass 指向基类 NSObject 的类对象。

isa & superclass

isa 将实例对象与类对象、元类对象连接起来;superclass 将子类、父类、基类连接起来。

当向一个实例发送实例方法的消息时(也可称调用实例方法,但因为 Obj-C 是消息结构的语言,方法调用并不严谨),该实例对象会通过 isa 找到其类对象,因为实例方法存储在类对象中;如果该类的类对象中并没有存储该实例方法,其类对象将使用 superclass 找到其父类的类对象,并在其中查找实例方法;直到在 NSObject 基类的类对象也无法找到时,superclass 将指向 nil 停止查找,抛出异常:unrecognized selector sent to instance(注意,我们这里仅讨论消息传递,暂时忽略消息转发)。

@interface Computer : NSObject
- (void)run;
@end

@implementation Computer
@end

@interface Mac : Computer
@end

@implementation Mac
// - (void)run {
//     NSLog(@"RUN");
// }
@end

// '-[Mac run]: unrecognized selector sent to instance 0x10330f6c0'
[[[Mac alloc] init] run];

当向一个类(的类对象)发送类方法的消息时,不会经过实例对象;类对象会通过其 isa 指针找到其元类对象,因为类方法存储在元类对象中;如果该类的元类对象中并没有存储该类方法,其元类对象将使用 superclass 找到其父类的元类对象,并在其中查找类方法;直到查找到 NSObject 基类的元类对象还没有该类方法时,NSObject 元类对象的 superclass 将指回 NSObject 类对象中,在其存储的实例方法列表中查找,若此时还没有找到将停止查找,并抛出异常:unrecognized selector sent to instance,因为 NSObject 类对象的 superclass 将指向 nil

@interface NSObject (Extension)
@end

@implementation NSObject (Extension)
- (void)run {
    NSLog(@"RUN");
}
@end

@interface Computer : NSObject
+ (void)run;
@end

@implementation Computer
@end

@interface Mac : Computer
@end

@implementation Mac
@end

// OUTPUT:
// RUN

通过上一点,其实我们可以知道在 Obj-C 中,方法是通过消息传递的,而传递的消息其实就是方法名的 SEL,因此这也是严谨意义上 Obj-C 不支持重载(Overload)的原因(但支持参数个数不同的重载),具体可参考 Obj-C 中的重载与重写一文。

Reference