Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

关于 objc4 源码的一些说明:

  • objc4 的源码不能直接编译,需要配置相关环境才能运行。可以在这里下载可调式的源码。
  • objc 运行时源码的入口在 void _objc_init(void) 函数。

目录

  • 1.Objective-C 对象是什么?Class 是什么?id 又是什么?
  • 2.isa 是什么?为什么要有 isa?
  • 3.为什么在 Objective-C 中,所以的对象都用一个指针来追踪?
  • 4.Objective-C 对象是如何被创建(alloc)和初始化(init)的?
  • 5.Objective-C 对象的实例变量是什么?为什么不能给 Objective-C 对象动态添加实例变量?
  • 6.Objective-C 对象的属性是什么?属性跟实例变量的区别?
  • 7.Objective-C 对象的方法是什么?Objective-C 对象的方法在内存中的存储结构是什么样的?
  • 8.什么是 IMP?什么是选择器 selector ?
  • 9.消息发送和消息转发
  • 10.Method Swizzling
  • 11.Category
  • 12.Associated Objects 的原理是什么?到底能不能在 Category 中给 Objective-C 类添加属性和实例变量?
  • 13.Objective-C 中的 Protocol 是什么?
  • 14.selfsuper 的本质
  • 15.load 方法和 initialize 方法

1. Objective-C 对象是什么?Class 是什么?id 又是什么?

所有的类都继承 NSObject 或者 NSProxy,先来看看这两个类在各自的公开头文件中的定义:

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

在 objc.h 文件中,对于 Class,id 以及 objc_object 的定义:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

runtime.h 文件中对 objc_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;

在 Objective-C 中,每一个对象是一个结构体,每个对象都有一个 isa 指针,类对象 Class 也是一个对象。所以,我们说,凡是包含 isa 指针的,都可以被认为是 Objective-C 中的对象。运行时可以通过 isa 指针,查找到该对象是属于什么类(Class)。

2. isa 是什么?为什么要有 isa?

在 Runtime 源码中,对于 objc_object 和 objc_class 的定义分别如下:

struct objc_object {
private:
    isa_t isa;  // isa 是一个 union 联合体,其包含这个对象所属类的信息

public:
    Class ISA();     // ISA() assumes this is NOT a tagged pointer object
    Class getIsa();  // getIsa() allows this to be a tagged pointer object
    
	...
};
struct objc_class : objc_object {
    // 这里没写 isa,其实继承了 objc_object 的 isa , 在这里 isa 是一个指向元类的指针
    // Class ISA;
    Class superclass;           // 指向当前类的父类
    cache_t cache;              // formerly cache pointer and vtable
                                // 用于缓存指针和 vtable,加速方法的调用
    class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
                                // 相当于 class_rw_t 指针加上 rr/alloc 的标志
                                // bits 用于存储类的方法、属性、遵循的协议等信息的地方

    // 针对 class_data_bits_t 的 data() 函数的封装,最终返回一个 class_rw_t 类型的结构体变量
    // Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
    class_rw_t *data() { 
        return bits.data();
    }
    
    ...
};

objc_class 继承于 objc_object,所以 objc_class 也是一个 objc_object,objc_object 和 objc_class 都有一个成员变量 isa。isa 变量的类型是 isa_t,这个 isa_t 其实是一个联合体(union),其中包括成员量 cls。也就是说,每个 objc_object 通过自己持有的 isa,都可以查找到自己所属的类,对于 objc_class 来说,就是通过 isa 找到自己所属的元类(meta class)。

#define ISA_MASK        0x00007ffffffffff8ULL
#define ISA_MAGIC_MASK  0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE   (1ULL<<56)
#define RC_HALF  (1ULL<<7)

// isa 的类型是一个 isa_t 联合体
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;			// 所属的类
    uintptr_t bits;

    struct {
        uintptr_t nonpointer        : 1;  // 表示 isa_t 的类型,0 表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。1 表示当前 isa 不是指针,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。
        uintptr_t has_assoc         : 1;  // 对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存
        uintptr_t has_cxx_dtor      : 1;  // 表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存。
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;  // 用于调试器判断当前对象是真的对象还是没有初始化的空间
        uintptr_t weakly_referenced : 1;  // 对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
        uintptr_t deallocating      : 1;  // 对象正在释放内存
        uintptr_t has_sidetable_rc  : 1;  // 对象的引用计数太大了,存不下
        uintptr_t extra_rc          : 8;  // 对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc 的值就为 9
    };
};

而在 Objective-C 中,对象的方法都是存储在类中,而不是对象中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。

// Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;               // 一个指向常量的指针,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议

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

    Class firstSubclass;
    Class nextSiblingClass;
    
    ...
    
};

当一个对象的实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后在这里的 class_data_bits_t 结构体中查找对应方法的实现(每个对象可以通过 cls->data()-> methods 来访问所属类的方法)。同时,每一个 objc_class 也有一个指向自己的父类的指针 super_class 用来查找继承的方法。

因为在 Objective-C 中,类其实也是一个对象,每个类也有一个 isa 指向自己所属的元类。所以无论是类还是对象都能通过相同的机制查找方法的实现。

isa 在方法调用时扮演的角色:

  • 调用一个对象的实例方法时,通过对象的 isa 在类中获取方法的实现
  • 调用一个类的类方法时,通过类的 isa 在元类中获取方法的实现
  • 如果在当前类/元类中没找到,就会通过类/元类的 superclass 在继承链中一级一级往上查找

图 1. 对象,类与元类之间的关系

isa_t 中包含什么:

isa 的类型 isa_t 是一个 union 类型的结构体,也就是说其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间,而 isa 总共会占据 64 位的内存空间(决定于其中的结构体)。其中包含的信息见上面的代码注释。

现在直接访问对象(objc_object)的 isa 已经不会返回类指针了,取而代之的是使用 ISA() 方法来获取类指针。其中 ISA_MASK 是宏定义,这里通过掩码的方式获取类指针。

结论

(1)isa 的作用:用于查找对象(或类对象)所属类(或元类)的信息,比如方法列表。

(2)isa 是什么:isa 的数据结构是一个 isa_t 联合体,其中包含其所属的 Class 的地址,通过访问对象的 isa,就可以获取到指向其所属 Class 的指针(针对 tagged pointer 的情况,也就是 non-pointer isa,有点不一样的是,除了指向 class 的指针,isa 中还会包含对象本身的一些信息,比如对象是否被弱引用)。

3. 为什么在 Objective-C 中,所以的对象都用一个指针来追踪?

内存中的数据类型分为两种:值类型和引用类型。指针就是引用类型,struct 类型就是值类型。

值类型在传值时需要拷贝内容本身,而引用类型在传递时,拷贝的是对象的地址。所以,一方面,值类型的传递占用更多的内存空间,使用引用类型更节省内存开销;另一方面,也是最主要的原因(Really??),很多时候,我们需要把一个对象交给另一个函数或者方法去修改其中的内容(比如说一个 Person 对象的 age 属性),显然如果我们想让修改方获取到这个对象,我们需要的传递的是地址,而不是复制一份。

对于像 int 这样的基本数据类型,拷贝起来更快,而且数据简单,方便修改,所以就不用指针了。

另一方面,对象的内存是分配在堆上的,而值类型是分配到栈上的。所以一般对象的生命周期会比普通的值类型要长,而且创建和销毁对象以及内存管理是要消耗性能的,所以通过指针来引用一个对象,比直接复制和创建对象要更有效率、更节省性能。

参考:

4. Objective-C 对象是如何被创建(alloc)和初始化(init)的?

整个对象的创建过程其实就做了两件事情:为对象分配内存空间,以及初始化 isa(一个联合体)。

(1)创建 NSObject 对象的过程

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


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


static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    id obj = class_createInstance(cls, 0);
    if (slowpath(!obj)) return callBadAllocHandler(cls);
    return obj;
}


id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

最核心的逻辑就在 _class_createInstanceFromZone 函数中:

static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) {

    // 实例变量内存大小,实例大小 instanceSize 会存储在类的 isa_t 结构体中,经过对齐最后返回
    size_t size = cls->instanceSize(extraBytes);  

    // 给对象申请内存空间
    id obj = (id)calloc(1, size);
    if (!obj) return nil;
    
    // 初始化 isa
    obj->initInstanceIsa(cls, hasCxxDtor);

    return obj;
}

获取对象内存空间大小:

size_t instanceSize(size_t extraBytes) {
    // Core Foundation 需要所有的对象的大小至少有 16 字节。
    size_t size = alignedInstanceSize() + extraBytes;
    if (size < 16) size = 16;
    return size;
}

uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

uint32_t unalignedInstanceSize() {
    assert(isRealized());
    return data()->ro->instanceSize;
}

初始化 isa,isa 是一个 isa_t 联合体:

inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) {
    if (!indexed) {
        isa.cls = cls;
    } else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}

(2)NSObject 对象初始化的过程

NSObject 对象的初始化实际上就是返回 +alloc 执行后得到的对象本身:

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

id _objc_rootInit(id obj) {
    return obj;
}

5. Objective-C 对象的实例变量是什么?为什么不能给 Objective-C 对象动态添加实例变量?

(1)两个注意点:

  • Objective-C 的 -> 操作符不是C语言指针操作
  • Objective-C 对象不能简单对应于一个 C struct,访问成员变量不等于访问 C struct 成员

(2)Non Fragile ivars

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。

当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。用旧版OSX SDK 编译的 MyObject 类成员变量布局是这样的,MyObject的成员变量依次排列在基类NSObject 的成员后面。

当苹果发布新版本OSX SDK后,NSObject增加了两个成员变量。如果没有Non Fragile ivars特性,我们的代码将无法正常运行,因为MyObject类成员变量布局在编译时已经确定,有两个成员变量和基类的内存区域重叠了。此时,我们只能重新编译MyObject代码,程序才能在新版本系统上运行。

现在有了 Non Fragile ivars 之后,问题就解决了。在程序启动后,runtime加载MyObject类的时候,通过计算基类的大小,runtime 动态调整了 MyObject 类成员变量布局,把MyObject成员变量的位置向后移动8个字节。于是我们的程序无需编译,就能在新版本系统上运行。

(3)Non Fragile ivars 是如何实现的呢?

当成员变量布局调整后,静态编译的native程序怎么能找到变量的新偏移位置呢?

根据 Runtime 源码可知,一个变量实际上就是一个 ivar_t 结构体。而每个 Objective-C 对象对应于 struct objc_object,后者的 isa 指向类定义,即 struct objc_class:

typedef struct ivar_t *Ivar;

struct objc_object {
private:
    isa_t isa;
    //...
};

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() { 
        return bits.data();
    }
    //...
};


沿着 objc_class 的 data()->ro->ivars 找下去,struct ivar_list_t是类所有成员变量的定义列表。通过 first 变量,可以取得类里任意一个类成员变量的定义。

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

    const class_ro_t *ro;               // 一个指向常量的指针,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议
    //...
    
};

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

struct ivar_list_t {
    uint32_t entsize;
    uint32_t count;
    ivar_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    //...
};

这里的 offset,应该就是用来记录这个成员变量在对象中的偏移位置。也就是说,runtime在发现基类大小变化时,通过修改offset,来更新子类成员变量的偏移值。

假如我们现在有一个继承于 NSError 的类 MyClass,在编译时,LLVM计算出基类 NSError 对象的大小为40字节,然后记录在MyClass的类定义中,如下是对应的C代码。在编译后的可执行程序中,写死了“40”这个魔术数字,记录了在此次编译时MyClass基类的大小。

class_ro_t class_ro_MyClass = {
    .instanceStart = 40,
    .instanceSize = 48,
    //...
}

现在假如苹果发布了OSX 11 SDK,NSError类大小增加到48字节。当我们的程序启动后,runtime加载MyClass类定义的时候,发现基类的真实大小和MyClass的instanceStart不相符,得知基类的大小发生了改变。于是runtime遍历MyClass的所有成员变量定义,将offset指向的值增加8。具体的实现代码在runtime/objc-runtime-new.mm的 moveIvars()函数中。

并且,MyClass类定义的instanceSize也要增加8。这样runtime在创建MyClass对象的时候,能分配出正确大小的内存块。

static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
    uint32_t diff;

    diff = superSize - ro->instanceStart;
    if (ro->ivars) {
        // Find maximum alignment in this class's ivars
        uint32_t maxAlignment = 1;
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t alignment = ivar.alignment();
            if (alignment > maxAlignment) maxAlignment = alignment;
        }

        // Compute a slide value that preserves that alignment
        uint32_t alignMask = maxAlignment - 1;
        diff = (diff + alignMask) & ~alignMask;

        // Slide all of this class's ivars en masse
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t oldOffset = (uint32_t)*ivar.offset;
            uint32_t newOffset = oldOffset + diff;
            *ivar.offset = newOffset;

            if (PrintIvars) {
                _objc_inform("IVARS:    offset %u -> %u for %s "
                             "(size %u, align %u)", 
                             oldOffset, newOffset, ivar.name, 
                             ivar.size, ivar.alignment());
            }
        }
    }

    *(uint32_t *)&ro->instanceStart += diff;
    *(uint32_t *)&ro->instanceSize += diff;
}


(4)为什么Objective-C类不能动态添加成员变量

runtime 提供了一个 class_addIvar() 函数用于给类添加成员变量,但是根据文档中的注释,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用 objc_allocateClassPair 之后,调用 objc_registerClassPair 之前才可以添加成员变量。

这样做会带来严重问题,为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用。那为什么runtime允许动态添加方法和属性,而不会引发问题呢?

因为方法和属性并不“属于”实例,而成员变量“属于”实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响实例的内存布局,已经创建出的实例仍然可正常使用。

美团的技术博客中给出的解释比较简单,其中“破坏破坏类的内部布局”这句话本身也是有些问题的:

extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。

但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

参考:

6. Objective-C 对象的属性是什么?属性跟实例变量的区别?

属性是一个结构体,其中包含属性名和属性本身的属性(attributes)。

我们一般是通过使用 @property 进行属性定义,编译时编译器会自动生成对应的实例变量(默认情况下生成的实例变量名是在对应的属性名前加了下划线“_”),同时还会自动合成对应的 setter 和 getter 方法用于存取属性值。

我们可以验证一下,先定义一个带有属性的类 NyanCat,如下:


@interface NyanCat : NSObject {
    int age;
    NSString *name;
}

@property (nonatomic, copy) NSString *cost;

@end

@implementation NyanCat

@end

然后再通过 clang -rewrite-objc NyanCat.m 将该类重写为 cpp 代码后,得到了下面这些内容:


#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat; // NyanCat 类实际上就是一个 objc_object 结构体
typedef struct {} _objc_exc_NyanCat;
#endif

extern "C" unsigned long OBJC_IVAR_$_NyanCat$_cost;
struct NyanCat_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int age;
	NSString *name;
	NSString *_cost;
};


//...

// 属性 cost 的 setter 和 getter 对应的函数
static NSString * _I_NyanCat_cost(NyanCat * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_NyanCat$_cost)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct NyanCat, _cost), (id)cost, 0, 1); }

// 属性的数据结构
struct _prop_t {
	const char *name;
	const char *attributes;
};

extern "C" unsigned long int OBJC_IVAR_$_NyanCat$age __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, age);
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, name);
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$_cost __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, _cost);


// 实例变量列表
static struct /*_ivar_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count;
	struct _ivar_t ivar_list[3];
} _OBJC_$_INSTANCE_VARIABLES_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_ivar_t),
	3,
	{{(unsigned long int *)&OBJC_IVAR_$_NyanCat$age, "age", "i", 2, 4},
	 {(unsigned long int *)&OBJC_IVAR_$_NyanCat$name, "name", "@\"NSString\"", 3, 8},
	 {(unsigned long int *)&OBJC_IVAR_$_NyanCat$_cost, "_cost", "@\"NSString\"", 3, 8}}
};


// 实例方法列表
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{(struct objc_selector *)"cost", "@16@0:8", (void *)_I_NyanCat_cost},
	{(struct objc_selector *)"setCost:", "v24@0:8@16", (void *)_I_NyanCat_setCost_}}
};

// 属性列表
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"cost","T@\"NSString\",C,N,V_cost"}}
};


从上面 clang 重写的代码中可以看到:

  • 属性列表的数据结构 _prop_list_t 中有属性cost对应的属性名和属性的 attributes(attributes 字符串所代表的含义可以在官方文档上查阅到)。
  • 实例变量列表 _method_list_t 中也有属性 cost 对应的变量信息,变量名为 _cost,类型为 @"NSString"
  • 实例方法列表 _method_list_t 中有属性 cost 对应的 setter 和 getter 方法,这两个方法的实现分别对应的是两个函数—— _I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost) _I_NyanCat_cost(NyanCat * self, SEL _cmd)

以上三条结果正好验证了我们一开始提出的结论。

实际上,在 runtime 源码 objc-runtime-new.h 的实现中,属性就是一个 property_t 类型的结构体,其中包含属性名以及属性自己的属性(attributes)。

typedef struct property_t *objc_property_t;

// 属性的数据结构
struct property_t {
    const char *name;         // property 的名字
    const char *attributes;   // property 的属性
};

在实际使用 runtime 时,通过下面两个函数分别可以获取 property 名字和 attributes 字符串。

// Returns the name of a property.
const char * _Nonnull property_getName(objc_property_t _Nonnull property);

// Returns the attribute string of a property.
const char * _Nullable property_getAttributes(objc_property_t _Nonnull property);

小结:

  • 一个对象的属性实际上包括实例变量以及存取属性值(实际上就是实例变量值)的 setter/getter 方法两部分(这里只讨论类本身定义的属性,category 中的 @property 并没有为我们生成实例变量以及存取方法,而需要我们手动实现)。
  • 属性的实际结构是一个结构体,其中包含属性名和 attributes 两部分。

7. Objective-C 对象的方法是什么?Objective-C 对象的方法在内存中的存储结构是什么样的?

objc_class 有一个 class_data_bits_t 类型的变量 bits,Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中,通过调用 objc_class 的 class_rw_t *data() 方法,可以获取这个 class_rw_t 类型的变量。

// Objective-C 类是一个结构体,继承于 objc_object
struct objc_class : objc_object {
    // 这里没写 isa,其实继承了 objc_object 的 isa , 在这里 isa 是一个指向元类的指针
    // Class ISA;
    Class superclass;           // 指向当前类的父类
    cache_t cache;              // formerly cache pointer and vtable
                                // 用于缓存指针和 vtable,加速方法的调用
    class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
                                // 相当于 class_rw_t 指针加上 rr/alloc 的标志
                                // bits 用于存储类的方法、属性、遵循的协议等信息的地方
                                

    // 针对 class_data_bits_t 的 data() 函数的封装,最终返回一个 class_rw_t 类型的结构体变量
    // Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
    class_rw_t *data() { 
        return bits.data();
    }

	 ...
}

class_rw_t 中还有一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

/ Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;               // 一个指向常量的指针,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议

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

	...

}
// 用于存储一个 Objective-C 类在编译期就已经确定的属性、方法以及遵循的协议
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;
    }
};

加载 ObjC 运行时的过程中在 realizeClass() 方法中:

  1. 从 class_data_bits_t 调用 data 方法,将结果强制转换为 class_ro_t 指针;
  2. 初始化一个 class_rw_t 结构体;
  3. 设置结构体 ro 的值以及 flag。
  4. 最后重新将这个 class_rw_t 设置给 class_data_bits_t 的 data。
...
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
...

在上面这段代码运行之后 class_rw_t 中的方法,属性以及协议列表均为空。这时需要 realizeClass 调用 methodizeClass 方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。

方法的结构,与类和对象一样,方法在内存中也是一个结构体 method_t,其中包括成员变量 name(SEL 类型,实际上就是方法名)、types(一个C字符串方法类型,详见 Type Encodings)、imp(IMP 类型方法实现)。

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

结论:

(1) 在 runtime 初始化之后,realizeClass 之前,从 class_data_bits_t 结构体中获取的 class_rw_t 一直都不是 class_rw_t 结构体,而是class_ro_t。因为类的一些方法、属性和协议都是在编译期决定的(baseMethods 等成员以及类在内存中的位置都是编译期决定的)。

(2) 类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。 类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。

(3) 在 class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods。

(4)一个类(Class)持有一个分发表,在运行期分发消息,表中的每一个实体代表一个方法(Method),它的名字叫做选择子(SEL),对应着一种方法实现(IMP)。

参考:

8. 什么是 IMP?什么是选择器 selector ?

8.1 IMP

IMP 在 runtime 源码 objc.h 中的定义是:

/// A pointer to the function of a method implementation. 
typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法的实现,以达到更好的性能(在 MantleMTLModelAdapter.m 中可以看到这方面的应用)。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址;反之亦然。

8.2 选择器 selector

选择器代表方法在 Runtime 期间的标识符,为 SEL 类型,SEL 与普通字符串的区别在于 SEL 对于选择器来说总是能保证其唯一性。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C 的 Runtime 运行系统。

SEL 在 objc.h 中的定义是:

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

SEL 看上去是一个指向结构体的指针,但是实际上是什么类型呢?objc.h 中提供了运行时向系统注册选择器的函数 sel_registerName()。而在开源的 objc-sel.mm 中提供了sel_registerName() 函数的实现,其中能找到一些蛛丝马迹:

SEL sel_registerName(const char *name) {
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}

static SEL __sel_registerName(const char *name, int lock, int copy) 
{
    SEL result = 0;

    if (lock) selLock.assertUnlocked();
    else selLock.assertWriting();

    // name  为空直接返回 0
    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    if (lock) selLock.read();
    if (namedSelectors) {
        // 到全局的表中去找
        result = (SEL)NXMapGet(namedSelectors, name);
    }
    if (lock) selLock.unlockRead();
    if (result) return result;

    // No match. Insert.

    if (lock) selLock.write();

    if (!namedSelectors) {
        namedSelectors = NXCreateMapTable(NXStrValueMapPrototype, 
                                          (unsigned)SelrefCount);
    }
    if (lock) {
        // Rescan in case it was added while we dropped the lock
        result = (SEL)NXMapGet(namedSelectors, name);
    }
    if (!result) {
        // 创建一个 SEL
        result = sel_alloc(name, copy);
        // fixme choose a better container (hash not map for starters)
        NXMapInsert(namedSelectors, sel_getName(result), result);
    }

    if (lock) selLock.unlockWrite();
    return result;
}

static SEL sel_alloc(const char *name, bool copy)
{
    selLock.assertWriting();
    return (SEL)(copy ? strdupIfMutable(name) : name);   
}

从创建 SEL 的实现来看, SEL 实际上是一个 char * 类型,也就是一个字符串。

(1) 使用 @selector() 生成的选择子不会因为类的不同而改变(即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择子),其内存地址在编译期间就已经确定了。也就是说向不同的类发送相同的消息时,其生成的选择子是完全相同的。

(2) 通过 @selector(方法名) 就可以返回一个选择子,通过 (void *)@selector(方法名), 就可以读取选择器的地址。

(3) 推断出的 selector 的特性:

  • Objective-C 为我们维护了一个巨大的选择子表
  • 在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中。
  • 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。

参考:

9. 消息发送和消息转发

具体过程查看源码中 lookUpImpOrForward() 函数部分的注释

  1. 发送 hello 消息后,编译器会将上面这行 [obj hello]; 代码转成 objc_msgSend()(注:objc_msgSend 是一个私有方法,而且是用汇编实现的,我们没有办法进入它的实现,但是我们可以通过 lookUpImpOrForward 函数断点拦截)
  2. 到当前类的缓存中去查找方法实现,如果找到了直接 done
  3. 如果没找到,就到当前类的方法列表中去查找,如果找到了直接 done
  4. 如果还没找到,就到父类的缓存中去查找方法实现,如果找到了直接 done
  5. 如果没找到,就到父类的方法列表中去查找,如果找到了直接 done
  6. 如果还没找到,就进行方法决议
  7. 最后还没找到的话,就走消息转发

参考:

10. Method Swizzling

  • 什么是 Method Swizzling ?一般用来干什么?
  • Method Swizzling 的原理是什么?
  • Method Swizzling 有什么注意点?最佳实践是什么?
  • Method Swizzling 为什么要在 +load 方法中进行?

参考:

11. Category

  • Category 是什么?
  • Category 的使用注意点
  • Category 中的方法和属性以及协议是怎么存储和加载的?
  • Category 和 Class 的关系

11.1 应用场景

在 Objective-C 2.0 中,提供了 Category 这个语言特性,可以动态地为已有类添加新行为。

  • 给已经存在的类添加方法
  • 将一个类的实现拆分成多个独立的源文件
  • 声明私有的方法

11.2 注意点

  • 不要用 Category 来覆写原类的方法。因为这样可能会导致原类中的方法不再被调用。
  • 同理,不要用 Category 来覆写父类的方法。因为如果原类中也覆盖了父类的这个方法,这样还是会遇到上面的第一个问题。
  • 不要用 Category 来覆写原类其他 Category 中定义的方法。因为哪个 Category 中的方法会最先被调用是不可预知的。

11.3 category VS. extension

美团技术团队的博客总结的很好:

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。

但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

11.4 源码实现

(1)Category 的真实面目

objc-runtime-new.h 中可以找到 Category 在 runtime 层面的定义:

struct category_t {
    const char *name; // 分类名
    classref_t cls;  // 原类
    struct method_list_t *instanceMethods; // 实例方法列表
    struct method_list_t *classMethods;    // 类方法列表
    struct protocol_list_t *protocols;     // 协议列表
    struct property_list_t *instanceProperties;  // 实例属性列表
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;  // 类属性列表

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

定义一个 Category,然后再使用 clang -rewrite-objc MyCategory.m,也可以看到 Category 经过编译器处理后的样子。详见 深入理解Objective-C:Category - 美团技术团队

(2)Category (方法、属性等)的加载过程

Objective-C 的运行是依赖 ObjC 的 runtime 的,而 ObjC 的 runtime 和其他系统库一样,是 macOS 和 iOS 通过 dyld 动态加载的。

ObjC runtime 的加载是从 objc-os.mm 中的 _objc_init 函数开始的,其中加载 Category 大概流程如下:

_objc_init()
  _dyld_objc_notify_register()
    map_images()
      map_images_nolock()
        _read_images()
	  addUnattachedCategoryForClass() // 把 category 和原类做一个关联映射
	  remethodizeClass()              // 把 category 的实例方法/类方法、协议以及属性/类属性添加到原类/元类上
	    attachCategories()

加载 Category 属性和方法的主要逻辑都在 attachCategories 函数中:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

attachLists 函数做的工作就是把类中原有的方法(或者属性和协议)放到了新添加的 category (或者属性和协议)的后面了,代码实现如下:

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        
	// 后面的内容省略了...
    }

结论:

  • category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA
  • category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 category 的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就停止查找,不会再关心后面是不是还有一样名字的方法。

11.5 category 和 +load 方法

(1)我们可以在一个类的 +load 方法中调用 category 中声明的方法么?

可以调用,因为 attatch category 到类的工作是在 map_images 阶段调用的(前面提到过),而 ObjC 类的 +load 方法是在 load_images 阶段调用的,前者比后者要早。这一点通过符号断点也可以验证。

runtime 加载时,_objc_init 函数中会调用到 _dyld_objc_notify_register

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_dyld_objc_notify_register 函数会在适当的时机分别调用 map_images 函数和 load_images 函数:

//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
// dyld will also call the "mapped" function.  Dyld will call the "init" function when dyld would be called
// initializers in that image.  This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

load_images 函数中调用了各个类和 Category 的 +load 方法:

void load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

(2)如果一个类和它的各个 category 中都重写了 +load 方法,那么调用顺序是怎样的呢?

+load 的执行顺序是先调用类的 +load,后调用 category 的,而 category 的 +load 执行顺序是根据类的编译顺序(在 Xcode Build phases 里面可以看到)决定的。

(3)如果一个类中和它的各个 category 中都实现了相同的方法,会发生什么?

对于 Category 覆写的方法,运行时的消息机制会先找到方法列表中第一个匹配的方法,也就是最后一个编译的 category 里的对应方法。因为上面提到过,各个 Category 中的方法是逆序排列的,而且 Category 的方法被插入到了原类中方法的前面。

参考:

12. Associated Objects 的原理是什么?到底能不能在 Category 中给 Objective-C 类添加属性和实例变量?

我们需要知道以下几个问题:

  • Associated Objects 的应用场景
  • Associated Objects 相关函数介绍
  • Associated Objects 的原理是什么?Associated Objects 被存储在什么地方,是不是存放在被关联对象本身的内存中?
  • Associated Objects 的五种 policy 有什么区别,有什么坑?
  • Associated Objects 的内存管理机制是什么?生命周期是怎样的,什么时候被释放,什么时候被移除?

12.1 使用场景

  • 为现有的类添加私有变量以帮助实现细节
  • 为现有的类添加公有属性
  • 为 KVO 创建一个关联的观察者

12.2 相关函数

objc/runtime.h 文件中可以看到:

/// 用于给对象添加关联对象,传入 nil 则可以移除已有的关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); 

/// 用于获取关联对象
id objc_getAssociatedObject(id object, const void *key);

/// 用于移除一个对象的所有关联对象
void objc_removeAssociatedObjects(id object);

12.3 objc_setAssociatedObject 的第二个参数 const void *key

void * 一般被称为通用指针或泛指针,它是C关于“纯粹地址(raw address)”的一种约定。void 指针指向某个对象,但该对象不属于任何类型。请看下例:

    int    *ip;
    void    *p;

在上例中,ip 指向一个整型值,而 p 指向的对象不属于任何类型。 在 C 中,任何时候你都可以用其它类型的指针来代替 void 指针(在 C++ 中同样可以),或者用 void 指针来代替其它类型的指针(在 C++ 中需要进行强制转换),并且不需要进行强制转换。例如,你可以把 char * 类型的指针传递给需要 void 指针的函数。

const 表示这个一个常量。

所以,传入的第二参数必须是一个唯一的对象级别的常量。从 runtime 源码中,可以看到这个 key 最后被用来从 std::map 中查找对应的 value。

/// 比较器的定义
struct ObjectPointerLess {
    bool operator()(const void *p1, const void *p2) const {
        return p1 < p2;
    }
};

/// 最终保存关联对象的 Map
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator>
 
...

/// 访问关联对象的逻辑
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
    old_association = j->second;
    j->second = ObjcAssociation(policy, new_value);
} else {
    (*refs)[key] = ObjcAssociation(policy, new_value);
}
...

一般来说,有以下三种推荐的 key 值:

  • 声明 static char kAssociatedObjectKey; ,使用时 objc_getAssociatedObject(self, &kAssociatedObjectKey);
  • 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用时 objc_getAssociatedObject(self, &kAssociatedObjectKey);
  • 直接使用 getter 的 selector 作为 key,使用时 objc_getAssociatedObject(self, @selector(associatedObjectGetterName));,而且在 getter 中可以直接写成 objc_getAssociatedObject(self, _cmd);

12.4 Associated Objects 的原理是什么?Associated Objects 被存储在什么地方?

我们可以在 objc-references.mm 中的 _object_set_associative_reference 函数中,看到 objc_setAssociatedObject 的核心逻辑:

    void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

最顶层的数据结构 AssociationsManager 的定义如下:

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

核心逻辑其实就是 4 个数据结构:

  • AssociationsManager 是最顶层的对象,其中有一个静态的 map,所以可以看做是一个单例,它维护了一个从 spinlock_t 锁到 AssociationsHashMap 哈希表的单例键值对映射;
  • AssociationsHashMap 是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;
  • ObjectAssociationMap 是一个 C++ 中的 map ,维护了从 keyObjcAssociation 的映射,即关联记录;
  • ObjcAssociation 是一个 C++ 的类,表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。

12.5 Associated Objects 的内存管理机制是什么?

关联策略 等价属性 说明
OBJC_ASSOCIATION_ASSIGN @property (assign)
or @property (unsafe_unretained)
弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (strong, nonatomic) 强引用关联对象,且为非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (copy, nonatomic) 复制关联对象,且为非原子操作
OBJC_ASSOCIATION_RETAIN @property (strong, atomic) 强引用关联对象,且为原子操作
OBJC_ASSOCIATION_COPY @property (copy, atomic) 复制关联对象,且为原子操作

一个 Objective-C 对象的 dealloc 方法的调用顺序是从子类到父类直至 NSObject 的,NSObjectdealloc 会调用 object_dispose() 函数,进而移除 Associated Object。具体的实现如下:

id object_dispose(id obj)
{
    if (!obj) return nil;
	// 销毁对象
    objc_destructInstance(obj);    
  // 释放内存
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        // C++ 析构
        if (cxx) object_cxxDestruct(obj);
        // 移除 Associated Object
        if (assoc) _object_remove_assocations(obj);
        // ARC 下调用实例变量的 release 方法,移除 weak 引用
        obj->clearDeallocating();
    }

    return obj;
}

结论:

  • 关联对象的五种关联策略与属性的限定符非常类似,在绝大多数情况下,我们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这可以保证我们持有关联对象
  • 关联对象的释放时机跟对应的等价属性的释放时机基本一致,都是系统 ARC 自动管理的,在“宿主对象” dealloc 被调用时,先析构“宿主对象”自己,然后再移除关联对象
  • 关联对象的释放时机与移除时机并不总是一致,跟 assginunsafe_unretained 类似,属性用关联策略 OBJC_ASSOCIATION_ASSIGN 进行关联的对象,很早就已经被释放了,但是并没有被移除,而再使用这个关联对象时就会造成 Crash

12.6 如何实现 Weak Associated Object?

两种方式:

  • host --> wrapper(retain) --> obj(weak) :“宿主对象”通过 retain 方式的关联一个 Wrapper 对象,Wrapper 对象再持有一个 weak 属性去保存真正要关联的那个对象
  • host --> obj(retain) --> TempObject.block(retain) --> notify host to nil out obj when dealloc:“宿主对象” 通过 retain 方式的关联目标对象,目标对象再通过 retain 方式的关联一个中间对象 dealloc 回调的 block。当目标对象被释放时,中间对象也被释放了,block 回调通知“宿主对象”清空属性,也就是关联对象的值。

实现代码详见如何实现 Weak Associated Object

参考:

13. Objective-C 中的 Protocol 是什么?

14. selfsuper 的本质

self 和 super 两个是不同的,self 是方法的一个隐藏参数(每个方法都有两个隐藏的参数,self_cmd),每个方法的实现的第一个参数即为 self。而 super 不是一个隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用 [super xxx]方法时,去调用父类的方法,而不是本类中的方法。

我们可以看看 message.h 中提供的发消息给父类的函数:

OBJC_EXPORT void 
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ );

当我们发送消息给 super 时,runtime 时就不是使用 objc_msgSend 方法了,而是 objc_msgSendSuper。函数的第一个参数也不再是 self 了,编译器会生成一个 objc_super 结构体。下面是 message.h 中 objc_super 结构体的定义:


/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained 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 Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

objc_super 包含了两个变量,receiver 是消息的实际接收者,super_class 是指向当前类的父类。

通过 clang -rewrite-objc NyanCat.m 命令将下面定义的 NyanCat 类转成 cpp 代码。

#import <Foundation/Foundation.h>

@interface NyanCat : NSObject
@end

@implementation NyanCat

- (instancetype)init {
    self = [super init];
    
    return self;
}

@end
struct __rw_objc_super { 
	struct objc_object *object; 
	struct objc_object *superClass; 
	__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};


#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat;
typedef struct {} _objc_exc_NyanCat;
#endif

struct NyanCat_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

/* @end */


// @implementation NyanCat


static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
    self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));

    return self;
}

// ...

由此可见,[super xxxx] 在运行时确实被转成了 objc_msgSendSuper 函数。

那么 objc_msgSendSuper 这个函数的内部实现是怎么样的呢?文档 objc_msgSendSuper 函数的注释中对 super 参数的注释是这样写的:

super - A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.

我们可以推断出,objc_msgSendSuper 函数实现实际上就是:objc_super 结构体指向的 objc_super->superClass 的方法列表开始查找调用方法的 selector 对应的实现,找到后以 objc_super->receiver 去调用这个 selector,最后就变成了调用 objc_msgSend 函数给 self 发消息的形式了。

objc_msgSend(objc_super->receiver, @selector(xxx));

这里的 objc_super->receiver 就相当于 self,上面的操作其实就是:

objc_msgSend(self, @selector(xxx));

[self init][super init] 的相同点在于消息接收者实际上都是 self(方法调用源头),区别就在于查找方法的实现时,前者是从 currentClass(self 所属的类)的方法列表中开始往上找,而后者是从 objc_super->spuerClass(也就是调用了 super 的地方的父类,这是在编译时就确定了的)的方法列表中开始往上查找。

需要强调的地方是,[self xxx] 要调用的实现是在运行时动态决定的,而 [super xxx] 要调用的实现是编译时就确定了的(这里有个例子可以测试一下)。从上面转换出来的 cpp 代码中也可以看出来,这其实是因为 objc_msgSendSuper 函数的第一个参数 objc_super 结构体中的 receiver 是通过接收方法中的 self 参数得来的,所以动态决定的,而 objc_super->superClass 是通过 class_getSuperclass(objc_getClass("NyanCat")) 得到的,所以是静态的,在编译时就确定了的。

static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
    self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));

    return self;
}

参考:

15. load 方法和 initialize 方法

  • +load 方法和 +initialize 方法分别在什么时候被调用?,在父类、子类和分类中的调用顺序是什么?
  • 这两个方法是用来干嘛的?
  • ProtocolKit 的实现中为什么要在 main 函数执行前进行 Protocol 方法默认实现的注册?

+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。

15.1 +load 方法

在 runtime 动态库加载的入口函数 _objc_init 设置符号断点,启动程序,可以看到如下堆栈:

#0	0x00007fff201b300d in _objc_init ()
#1	0x00000001002e36db in _os_object_init ()
#2	0x00000001002f4928 in libdispatch_init ()
#3	0x00007fff2a4fe65b in libSystem_initializer ()
#4	0x000000010002d079 in ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) ()
#5	0x000000010002d478 in ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) ()
#6	0x0000000100027d1a in ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#7	0x0000000100027c85 in ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#8	0x0000000100025b82 in ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#9	0x0000000100025c22 in ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) ()
#10	0x00000001000125e9 in dyld::initializeMainExecutable() ()
#11	0x00000001000189a4 in dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) ()
#12	0x000000010001122b in dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) ()
#13	0x0000000100011025 in _dyld_start ()

接着在设置断点map_images,函数堆栈如下:

#0	0x00007fff201c4fe1 in map_images ()
#1	0x0000000100015d2c in dyld::notifyBatchPartial(dyld_image_states, bool, char const* (*)(dyld_image_states, unsigned int, dyld_image_info const*), bool, bool) ()
#2	0x0000000100015ecf in dyld::registerObjCNotifiers(void (*)(unsigned int, char const* const*, mach_header const* const*), void (*)(char const*, mach_header const*), void (*)(char const*, mach_header const*)) ()
#3	0x00007fff203228ff in _dyld_objc_notify_register ()
#4	0x00007fff201b34d9 in _objc_init ()
#5	0x00000001002e36db in _os_object_init ()
#6	0x00000001002f4928 in libdispatch_init ()
#7	0x00007fff2a4fe65b in libSystem_initializer ()
#8	0x000000010002d079 in ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) ()
#9	0x000000010002d478 in ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) ()
#10	0x0000000100027d1a in ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#11	0x0000000100027c85 in ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#12	0x0000000100025b82 in ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#13	0x0000000100025c22 in ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) ()
#14	0x00000001000125e9 in dyld::initializeMainExecutable() ()
#15	0x00000001000189a4 in dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) ()
#16	0x000000010001122b in dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) ()
#17	0x0000000100011025 in _dyld_start ()

最后再在一个类的 +load 方法处设置断点,得到函数堆栈如下:

#0	0x000000010000378c in +[Dog load] at /Users/ShannonChen/Desktop/Playgrounds/ObjCPlayground/ObjCPlayground/Dog.m:15
#1	0x00007fff201b79e9 in load_images ()
#2	0x000000010001228a in dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) ()
#3	0x0000000100027d08 in ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#4	0x0000000100025b82 in ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#5	0x0000000100025c22 in ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) ()
#6	0x000000010001262f in dyld::initializeMainExecutable() ()
#7	0x00000001000189a4 in dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) ()
#8	0x000000010001122b in dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) ()
#9	0x0000000100011025 in _dyld_start ()

大概过程如下:

  • dyld 开始将程序二进制文件初始化
  • 交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
  • 由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
  • runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

根据 runtime 源码,加载 objc runtime 时调用 +load 方法的流程如下:

_objc_init()
  _dyld_objc_notify_register()
    map_images()  // 先映射二进制数据
    load_images() // 然后加载二进制数据
      prepare_load_methods()
        schedule_class_load()
	  schedule_class_load() // 确保父类优先
	  add_class_to_loadable_list()
	    getLoadMethod() // 获取 +load 方法,保存到 loadable_classes 里
	add_category_to_loadable_list()
	  _category_getLoadMethod()  // 获取 +load 方法,保存到 loadable_categories 里
      call_load_methods()
        call_class_loads() 
	  (*load_method)(cls, SEL_load) // 从 loadable_classes 中取出各个类,调用它们的 +load 方法
	call_category_loads()
	  (*load_method)(cls, SEL_load) // 从 loadable_categories 中取出各个 Category,调用它们的 +load 方法
  

结论:

  • 先调用父类的 +load 方法,后调用子类的 +load 方法
  • Category 的 +load 方法会在它的主类的 +load 方法被调用之后才被调用
  • 不同的类之间的 +load 方法的调用顺序根据编译时的顺序(在 Xcode Build Phases 中可以看到)来确定的,靠前的先加载

15.2 +initialize 方法

在处理消息发送的函数 lookUpImpOrForward() 中我们可以看到 initialize class 的逻辑:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

		// 省略部分内容...
		...

    // 在 Objective-C 运行时 初始化的过程中(也就是libSystem 调用 _objc_init 函数时)会对其中的类进行第一次初始化也就是执行 realizeClass 方法,为类分配可读写结构体 class_rw_t 的空间,并返回正确的类结构体
    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    // _class_initialize 方法会调用类的 initialize 方法,在这个类第一次被初始化时就会调用 initialize 方法
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

 		// 省略后面的内容...
 		...
 		
    return imp;
}

通过阅读 runtime 源码,一个 ObjC 类的 +initialize 方法被调用的大概流程如下:

objc_msgSend()
  lookUpImpOrForward()
    _class_initialize()
      _class_initialize(supercls) // initialize 当前类之前确保先 initialize 父类
      callInitialize()
        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); // 调用 initialize 方法

结论:

  • +initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。

  • +load 方法是通过函数指针来直接调用不同,+initialize 方法的调用是通过 objc_msgSend 调用的,与普通方法的调用是一样的,走的都是发送消息的流程

  • 基于第二点,可以得出下面的结论:

    • 如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用
    • 如果一个子类没有实现 +initialize 方法,那么父类的实现会被执行多次
    • 如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖

参考:

延伸

  1. clang 命令的使用(比如 clang -rewrite-objc test.m),clang -rewrite-objc 的作用是什么?clang rewrite 出来的文件跟 objc runtime 源码的实现有什么区别吗?

参考: