Objective-C Runtime 之二 理解类和对象


理解常用数据结构

观察 NSObject 类的结构:

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

可以看到,NSObject 类遵守同名的 NSObject 协议,其类的内部拥有唯一的成员变量 isaisaClass 类型。

objc_class

// Object.mm

typedef struct objc_class *Class;

Class 就是 ObjC 中类的实现,本质上是一个结构体指针,这个结构体是 objc_class

// objc-runtime-new.h

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();
    } // 把类信息中的可读写部分暴露出来
    
    // ...
};

上述代码总结如下内容:

  • objc_class 继承自 objc_object,也就是说,类本质上也是一个对象,称为类对象。
  • Class superclass 类对象拥有继承体系。
  • cache_t cache 为方法缓存,用于快速查找。
  • class_data_bits_t 存储的类的信息。

cache_t

cache_t 为方法缓存,用于快速查找方法的执行函数。常规的方法调用,需要通过 isa 先去其类对象的方法列表中查找,找不到就去查找父类,依次查找到 NSObject 类,如果一些方法经常被调用,这样的方式效率较低,所以就设置一个方法缓存,当某个方法被调用过,就将其放入方法缓存列表,之后就优先查找方法缓存。

cache_t 是一个可增量扩展的哈希表结构,使用哈希表实现可以提高查找效率。

cache_t 可以简单理解为由一组 bucket_t 结构组成,后者的主要构成是 SELIMP

// objc-runtime-new.h

struct cache_t {

    // ...
public:
    static bucket_t *emptyBuckets();
    
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
        
    // ...
};

bucket_t

// objc-runtime-new.h

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp; // 无类型的函数指针,对应具体的函数实现
    explicit_atomic<SEL> _sel; // 方法选择器
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
    
    // ...
};

通过 SEL 可以找到对应的 IMP

class_data_bits_t

// objc-runtime-new.h

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
    
    // ...

public:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    } // data,存储可动态改变的部分,内部还包含只读的部分
    
    // ...
    
    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro;
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    } // 将只读部分暴露出来
    
    // ...
};

class_data_bits_t 主要是对 class_rw_t 的封装。

class_rw_t

class_rw_t 存储类相关的读写信息,还包含 class_ro_t

我们通常为类动态添加的方法、属性、协议都是在这里进行实现。

// objc-runtime-new.h

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t version;
    uint16_t witness;

    const class_ro_t *ro; // 编译就确定,不能动态修改

    method_array_t methods; // 方法列表
    property_array_t properties; // 属性列表
    protocol_array_t protocols; // 协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

    // ...
};

主要包含:

  • const class_ro_t *ro
  • method_array_t methods,方法列表
  • property_array_t properties,属性列表
  • protocol_array_t protocols,协议列表

其中 method_array_tproperty_array_tprotocol_array_t 都是二维数组结构。

如果一个分类,分类中有若干方法,这一组方法就组成一个数组,然后这个数组作为 methods 的其中一个元素。

所以 methods 最终的组成元素就是一个个具体的方法,是 method_t 结构,属性和协议也类似。

class_ro_t

class_ro_t 代表了类的相关只读信息,这些内容在编译时就确定。

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

主要包含:

  • const char * name,类名
  • method_list_t * baseMethodList,方法列表
  • protocol_list_t * baseProtocols,协议列表
  • const ivar_list_t * ivars,变量列表
  • property_list_t *baseProperties,属性列表

这里的列表都是一维数组,代表本类中最初设置的方法、属性等。

method_t

// objc-runtime-new.h

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 中主要包含:

  • SEL name,包含方法名和参数个数信息
  • const char *types,包含函数返回值和参数类型
  • MethodListIMP imp,方法实现地址

关于 types,需要参考 Type Encodings,一个方法的返回值以及参数类型,苹果定义了不同的字符来进行表示,其中第一个是返回值类型,后续依次接参数。

-(void)aMethod 方法,types 存储应该是 v@:,通过查表,三个字符分布代表 voididSEL,也就是说这个方法的返回值是 void,第一个参数是消息接受者,第二个参数是 SEL

protocol_t

// objc-runtime-new.h

struct protocol_t : objc_object { // 协议也是一个对象
    const char *mangledName; // 协议名
    struct protocol_list_t *protocols; // 协议列表
    method_list_t *instanceMethods; // 实例方法
    method_list_t *classMethods; // 类方法
    method_list_t *optionalInstanceMethods; // 可选实例方法
    method_list_t *optionalClassMethods; // 可选类方法
    property_list_t *instanceProperties; // 实例属性
    
    // ...
};

ivar_t

// objc-runtime-new.h

struct ivar_t {
#if __x86_64__
    // *offset was originally 64-bit on some x86_64 platforms.
    // We read and write only 32 bits of it.
    // Some metadata provides all 64 bits. This is harmless for unsigned 
    // little-endian values.
    // Some code uses all 64 bits. class_addIvar() over-allocates the 
    // offset for their benefit.
#endif
    int32_t *offset; // 地址的偏移
    const char *name; // 变量名
    const char *type; // 变量类型
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

property_t

// objc-runtime-new.h

struct property_t {
    const char *name; // 属性名
    const char *attributes; // 属性修饰符
};

objc_object

ObjC 中的 id 类型本质上就是一个 objc_object 的结构体指针,objc_object 结构体其实就是 ObjC 中对对象的实现。objc_class 继承自 objc_object,就是说,类也是对象。

Object.mm

typedef struct objc_object *id;
// objc-private.h

struct objc_object {
private:
    isa_t isa;

    // 关于 isa 的一些相关操作,如通过 isa 获取类对象、元类对象
    // 弱引用相关方法
    // 关联对象相关方法
    // 内存管理相关方法
    // ...
};

isa

objc_object 中存在一个成员变量 isa,拥有 isa 在 ObjC 中就是对象。

// objc-private.h

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa 是一个联合体,在 64 位机器上这个共用体就是 64 位的(32 位下就是 32 位的)。

isa 可能是指针型的,也可能是非指针型的。 指针型就是说这个联合体的内容存储的就是一个指针,比如一个对象的 isa 指针找到对象的类对象。 非指针型的是说,这个联合体中的部分位代表地址,其他位表示一些其他信息。因为 64 位用于存储地址是多余的,只需要部分位已经能满足寻址需要,那么其他剩余的位置就可以存储一些别的。

isa 不总是指向实例所属的类,比如 KVO 就是将被观察对象的 isa 指向一个中间类。确定实例所属的类应该使用 class 方法。

// isa.h

#   define ISA_BITFIELD                                                      \
      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

对象模型

除了一个特殊类 NSProxyNSObject 类是所有类最终的父类(包括 NSObject 元类),NSObject 元类是所有类的元类(包括它自己)。

对象方法是存储在类对象的方法列表中,类方法是存储在元类对象的方法列表中。

图 1 - 对象模型

NSLog(@"Person实例地址:%@", self);

Class personClass = objc_getClass("Person");
NSLog(@"Person类对象地址:%p", personClass);

Class personMetaClass = objc_getMetaClass("Person");
NSLog(@"Person元类对象地址:%p", personMetaClass);

Class personSuperClass = class_getSuperclass(personClass);
NSLog(@"Person父类类对象地址:%p", personSuperClass);

NSObject *obj = [NSObject new];
NSLog(@"NSObject实例地址:%@", obj);

Class objectClass = objc_getClass("NSObject");
NSLog(@"NSObject类对象地址:%p", objectClass);

Class objectMetaClass = objc_getMetaClass("NSObject");
NSLog(@"NSObject元类对象地址:%p", objectMetaClass);

Class objectSuperClass = class_getSuperclass(objectClass);
NSLog(@"NSObject父类类对象地址:%p", objectSuperClass);

NSLog(@"Person实例对象的isa:%p", object_getClass(self));
NSLog(@"Person类对象的isa:%p", object_getClass(personClass));
NSLog(@"Person元类对象的isa:%p", object_getClass(personMetaClass));

NSLog(@"NSObject实例对象的isa:%p", object_getClass(obj));
NSLog(@"NSObject类对象的isa:%p", object_getClass(objectClass));
NSLog(@"NSObject元类对象的isa:%p", object_getClass(objectMetaClass));

/*
 Person实例地址:<Person: 0x1005b5ef0>
 Person类对象地址:0x100003208
 Person元类对象地址:0x1000031e0
 Person父类类对象地址:0x7fff970c7118
 
 NSObject实例地址:<NSObject: 0x103804920>
 NSObject类对象地址:0x7fff970c7118
 NSObject元类对象地址:0x7fff970c70f0
 NSObject父类类对象地址:0x0
 
 Person实例对象的isa:0x100003208
 Person类对象的isa:0x1000031e0
 Person元类对象的isa:0x7fff970c70f0
 
 NSObject实例对象的isa:0x7fff970c7118
 NSObject类对象的isa:0x7fff970c70f0
 NSObject元类对象的isa:0x7fff970c70f0
 */

关于对象模型的若干知识点

调用类方法最终却调用了实例方法

由于 NSObject 元类对象父类是 NSObject 类对象,所以如果调用 NSObject 类方法没有对应实现,但是 NSObject 存在同名的对象方法时,会最终调用到该对象方法。

super 关键字的实质

super 不是一个指针,而是一个关键字,使用 super 的含义就是查找方法时,不在本类的类对象方法列表中查找,而是跳过他,直接从父类的类对象方法列表中开始进行查找。但是消息接受者都是实例本身。

如下面试题:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Person

@end

@interface Student : Person

@end

@implementation Student

- (instancetype)init {
    self = [super init];
    
    if (self) {
        
        NSLog(@"%@", NSStringFromClass([self class])); // Student
        NSLog(@"%@", NSStringFromClass([super class])); // Student
    }
    
    return self;
}

@end

最终两个打印结果都是 Student,原因是两者的方法接受者都是同一个实例,只不过查找方法时,一个是从本类的类对象开始,一个是从父类的类对象开始,但由于 class 方法是 NSObject 中的方法,最终都是寻找到根类然后执行,所以两者其实不管是调用者还是最终调用到的函数都是相同的。

[self class] 本质上是调用 objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)[super class] 本质上是调用 objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)。其中 objc_super 结构如下:


/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver; // 还是调用者本身

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

其中的 receiver 就是调用者本身。