Objective-C Runtime

转载  Objective-C Runtime

文章目录
  1. 1. 引言
  2. 2. 简介
  3. 3. 与 Runtime 交互
    1. 3.1. Objective-C 源代码
    2. 3.2. NSObject 的方法
    3. 3.3. Runtime 的函数
  4. 4. Runtime 基础数据结构
    1. 4.1. SEL
    2. 4.2. id
    3. 4.3. Class
      1. 4.3.1. cache_t
      2. 4.3.2. class_data_bits_t
      3. 4.3.3. class_ro_t
      4. 4.3.4. class_rw_t
      5. 4.3.5. realizeClass
    4. 4.4. Category
    5. 4.5. Method
    6. 4.6. Ivar
    7. 4.7. objc_property_t
    8. 4.8. protocol_t
    9. 4.9. IMP
  5. 5. 消息
    1. 5.1. objc_msgSend 函数
    2. 5.2. 方法中的隐藏参数
    3. 5.3. 获取方法地址
  6. 6. 动态方法解析
  7. 7. 消息转发
    1. 7.1. 重定向
    2. 7.2. 转发
    3. 7.3. 转发和多继承
    4. 7.4. 替代者对象(Surrogate Objects)
    5. 7.5. 转发与继承
  8. 8. 健壮的实例变量 (Non Fragile ivars)
  9. 9. Objective-C Associated Objects
  10. 10. Method Swizzling
  11. 11. 总结

本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具有了灵活的动态特性,使这门古老的语言焕发生机。主要内容以下:html

引言

曾经以为Objc特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把 [receiver message] 当成简单的方法调用,而无视了“发送消息”这句话的深入含义。其实 [receiver message] 会被编译器转化为:node

1
objc_msgSend(receiver, selector)

若是消息含有参数,则为:ios

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

若是消息的接收者可以找到对应的 selector,那么就至关于直接执行了接收者这个对象的特定方法;不然,消息要么被转发,或是临时向接收者动态添加这个 selector 对应的实现内容,要么就干脆玩完崩溃掉。git

如今能够看出 [receiver message] 真的不是一个简简单单的方法调用。由于这只是在编译阶段肯定了要向接收者发送 message 这条消息,而 receive 将要如何响应这条消息,那就要看运行时发生的状况来决定了。程序员

Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,可是倒是每一个 Objc 程序员须要了解的。github

简介

由于Objc是一门动态语言,因此它老是想办法把一些决定工做从编译链接推迟到运行时。也就是说只有编译器是不够的,还须要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个 Objc 运行框架的一块基石。objective-c

Runtime其实有两个版本: “modern” 和 “legacy”。咱们如今用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 以后的 64 位程序中。而 maxOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你须要从新编译它的子类,而现行版就不须要。算法

Runtime 基本是用 C 和汇编写的,可见苹果为了动态系统的高效而做出的努力。你能够在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。编程

与 Runtime 交互

Objc 从三种不一样的层级上与 Runtime 系统进行交互,分别是经过 Objective-C 源代码,经过 Foundation 框架的NSObject类定义的方法,经过对 runtime 函数的直接调用。数组

Objective-C 源代码

大部分状况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳做着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而建立的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(好比 objc_msgSend 函数及其参数列表中的 id 和 SEL 都是啥)

NSObject 的方法

Cocoa 中大多数类都继承于 NSObject 类,也就天然继承了它的方法。最特殊的例外是 NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,能够经过继承它来实现一个其余类的替身类或是虚拟出一个不存在的类,说白了就是领导把本身展示给你们风光无限,可是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的做用,好比description方法须要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时得到类的信息,并检查一些特性,好比class返回对象的类;isKindOfClass:isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象可否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime 的函数

Runtime 系统是一个由一系列函数和数据结构组成,具备公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数容许你用纯C代码来重复实现 Objc 中一样的功能。虽然有一些方法构成了NSObject类的基础,可是你在写 Objc 代码时通常不会直接用到这些函数的,除非是写一些 Objc 与其余语言的桥接或是底层的debug工做。在 Objective-C Runtime Reference 中有对 Runtime 函数的详细文档。

Runtime 基础数据结构

还记得引言中的objc_msgSend:方法吧,它的真身是这样的:

1
id objc_msgSend ( id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。熟悉 Objective-C 类的内存模型或看过相关源码的能够直接跳过。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,能够理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

1
typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你能够用 Objc 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来得到一个 SEL 类型的方法选择器。

不一样类中相同名字的方法所对应的方法选择器是相同的,即便方法名字相同而变量类型不一样也会致使它们具备相同的方法选择器,因而 Objc 中方法命名有时会带上参数类型(NSNumber 一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。

id

objc_msgSend 第一个参数类型为id,你们对它都不陌生,它是一个指向类实例的指针:

1
typedef struct objc_object *id;

objc_object又是啥呢,参考 objc-private.h 文件部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();
... 此处省略其余方法声明
}

objc_object 结构体包含一个 isa 指针,类型为 isa_t 联合体。根据 isa 就能够顺藤摸瓜找到对象所属的类。isa 这里还涉及到 tagged pointer 等概念。由于 isa_t 使用 union 实现,因此可能表示多种形态,既能够当成是指针,也能够存储标志位。有关 isa_t 联合体的更多内容能够查看 Objective-C 引用计数原理

PS: isa 指针不老是指向实例对象所属的类,不能依靠它来肯定类型,而是应该用 class 方法来肯定实例对象的类。由于KVO的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫作 isa-swizzling 的技术,详见官方文档

Class

Class 实际上是一个指向 objc_class 结构体的指针:

1
typedef struct objc_class *Class;

而 objc_class 包含不少方法,主要都为围绕它的几个成员作文章:

1
2
3
4
5
6
7
8
9
10
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 继承于 objc_object,也就是说一个 ObjC 类自己同时也是一个对象,为了处理类和对象的关系,runtime 库建立了一种叫作元类 (Meta Class) 的东西,类对象所属类型就叫作元类,它用来表述类对象自己所具有的元数据。类方法就定义于此处,由于这些方法能够理解成类对象的实例方法。每一个类仅有一个类对象,而每一个类对象仅有一个与之相关的元类。当你发出一个相似 [NSObject alloc] 的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。全部的元类最终都指向根元类为其超类。全部的元类的方法列表都有可以响应消息的类方法。因此当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找可以响应消息的方法,若是找到了,而后对这个类对象执行方法调用。

上图实线是 superclass 指针,虚线是isa指针。 有趣的是根元类的超类是 NSObject,而 isa 指向了本身,而 NSObject 的超类为 nil,也就是它没有超类。

能够看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。

cache_t

1
2
3
4
5
6
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
... 省略其余方法
}

_buckets 存储 IMP_mask 和 _occupied 对应 vtable

cache 为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找可以响应消息的方法,由于这样效率过低了,而是优先在 cache 中查找。Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法若是被调用,那么它有可能从此还会被调用),下次查找的时候效率更高。

bucket_t 中存储了指针与 IMP 的键值对:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;

public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }

void set(cache_key_t newKey, IMP newImp);
};

有关缓存的实现细节,能够查看 objc-cache.mm 文件。

class_data_bits_t

objc_class 中最复杂的是 bitsclass_data_bits_t 结构体所包含的信息太多了,主要包含 class_rw_tretain/release/autorelease/retainCount 和 alloc 等信息,不少存取方法也是围绕它展开。查看 objc-runtime-new.h 源码以下:

1
2
3
4
5
6
7
8
9
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);
}
... 省略其余方法
}

注意 objc_class 的 data 方法直接将 class_data_bits_t 的data 方法返回,最终是返回 class_rw_t,保了好几层。

能够看到 class_data_bits_t 里又包了一个 bits,这个指针跟不一样的 FAST_ 前缀的 flag 掩码作按位与操做,就能够获取不一样的数据。bits 在内存中每一个位的含义有三种排列顺序:

32 位:

0 1 2 - 31
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_DATA_MASK

64 位兼容版:

0 1 2 3 - 46 47 - 63
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_REQUIRES_RAW_ISA FAST_DATA_MASK 空闲

64 位不兼容版:

0 1 2 3 - 46 47
FAST_IS_SWIFT FAST_REQUIRES_RAW_ISA FAST_HAS_CXX_DTOR FAST_DATA_MASK FAST_HAS_CXX_CTOR
48 49 50 51 52 - 63
FAST_HAS_DEFAULT_AWZ FAST_HAS_DEFAULT_RR FAST_ALLOC FAST_SHIFTED_SIZE_SHIFT 空闲

其中 64 位不兼容版每一个宏对应的含义以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// class is a Swift class
#define FAST_IS_SWIFT (1UL<<0)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA (1UL<<1)
// class or superclass has .cxx_destruct implementation
// This bit is aligned with isa_t->hasCxxDtor to save an instruction.
#define FAST_HAS_CXX_DTOR (1UL<<2)
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// class or superclass has .cxx_construct implementation
#define FAST_HAS_CXX_CTOR (1UL<<47)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_HAS_DEFAULT_AWZ (1UL<<48)
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<49)
// summary bit for fast alloc path: !hasCxxCtor and
// !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC (1UL<<50)
// instance size in units of 16 bytes
// or 0 if the instance size is too big in this field
// This field must be LAST
#define FAST_SHIFTED_SIZE_SHIFT 51

这里面除了 FAST_DATA_MASK 是用一段空间存储数据外,其余宏都是只用 1 bit 存储 bool 值。class_data_bits_t 提供了三个方法用于位操做:getBit,setBits 和 clearBits,对应到存储 bool 值的掩码也有封装函数,好比:

1
2
3
4
5
6
7
bool isSwift() {
return getBit(FAST_IS_SWIFT);
}

void setIsSwift() {
setBits(FAST_IS_SWIFT);
}

重头戏在于最大的那块存储区域–FAST_DATA_MASK,它其实就存储了指向 class_rw_t 的指针:

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

对这片内存读写处于并发环境,但并不须要加锁,由于会经过对一些状态(realization or construction)判断来决定是否可读写。

class_data_bits_t 甚至还包含了一些对 class_rw_t 中 flags 成员存取的封装函数。

class_ro_t

objc_class 包含了 class_data_bits_tclass_data_bits_t 存储了 class_rw_t 的指针,而 class_rw_t 结构体又包含 class_ro_t 的指针。

class_ro_t 中的 method_list_tivar_list_tproperty_list_t 结构体都继承自 entsize_list_tt<Element, List, FlagMask>。结构为 xxx_list_t 的列表元素结构为 xxx_t,命名很工整。protocol_list_t 与前三个不一样,它存储的是 protocol_t * 指针列表,实现比较简单。

entsize_list_tt 实现了 non-fragile 特性的数组结构。假如苹果在新版本的 SDK 中向 NSObject 类增长了一些内容,NSObject 的占据的内存区域会扩大,开发者之前编译出的二进制中的子类就会与新的 NSObject 内存有重叠部分。因而在编译期会给 instanceStart 和 instanceSize 赋值,肯定好编译时每一个类的所占内存区域起始偏移量和大小,这样只需将子类与基类的这两个变量做对比便可知道子类是否与基类有重叠,若是有,也可知道子类须要挪多少偏移量。更多细节能够参考后面的章节 Non Fragile ivars。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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_ro_t->flags 存储了不少在编译时期就肯定的类的信息,也是 ABI 的一部分。下面这些 RO_ 前缀的宏标记了 flags 一些位置的含义。其中后三个并不须要被编译器赋值,是预留给运行时加载和初始化类的标志位,涉及到与 class_rw_t 的类型强转。运行时会用到它作判断,后面会讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define RO_META (1<<0) // class is a metaclass
#define RO_ROOT (1<<1) // class is a root class
#define RO_HAS_CXX_STRUCTORS (1<<2) // class has .cxx_construct/destruct implementations
// #define RO_HAS_LOAD_METHOD (1<<3) // class has +load implementation
#define RO_HIDDEN (1<<4) // class has visibility=hidden set
#define RO_EXCEPTION (1<<5) // class has attribute(objc_exception): OBJC_EHTYPE_$_ThisClass is non-weak
// #define RO_REUSE_ME (1<<6) // this bit is available for reassignment
#define RO_IS_ARC (1<<7) // class compiled with ARC
#define RO_HAS_CXX_DTOR_ONLY (1<<8) // class has .cxx_destruct but no .cxx_construct (with RO_HAS_CXX_STRUCTORS)
#define RO_HAS_WEAK_WITHOUT_ARC (1<<9) // class is not ARC but has ARC-style weak ivar layout

#define RO_FROM_BUNDLE (1<<29) // class is in an unloadable bundle - must never be set by compiler
#define RO_FUTURE (1<<30) // class is unrealized future class - must never be set by compiler
#define RO_REALIZED (1<<31) // class is realized - must never be set by compiler

class_rw_t

class_rw_t 提供了运行时对类拓展的能力,而 class_ro_t 存储的大可能是类在编译时就已经肯定的信息。两者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不一样。

class_rw_t 中使用的 method_array_tproperty_array_tprotocol_array_t 都继承自 list_array_tt<Element, List>, 它能够不断扩张,由于它能够存储 list 指针,内容有三种:

  1. 一个 entsize_list_tt 指针
  2. entsize_list_tt 指针数组

class_rw_t 的内容是能够在运行时被动态修改的,能够说运行时对类的拓展大都是存储在这里的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

char *demangledName;

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
... 省略操做 flags 的相关方法
}

class_rw_t->flags 存储的值并非编辑器设置的,其中有些值可能未来会做为 ABI 的一部分。下面这些 RW_ 前缀的宏标记了 flags 一些位置的含义。这些 bool 值标记了类的一些状态,涉及到声明周期和内存管理。有些位目前甚至还空着。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define RW_REALIZED (1<<31) // class_t->data is class_rw_t, not class_ro_t
#define RW_FUTURE (1<<30) // class is unresolved future class
#define RW_INITIALIZED (1<<29) // class is initialized
#define RW_INITIALIZING (1<<28) // class is initializing
#define RW_COPIED_RO (1<<27) // class_rw_t->ro is heap copy of class_ro_t
#define RW_CONSTRUCTING (1<<26) // class allocated but not yet registered
#define RW_CONSTRUCTED (1<<25) // class allocated and registered
// #define RW_24 (1<<24) // available for use; was RW_FINALIZE_ON_MAIN_THREAD
#define RW_LOADED (1<<23) // class +load has been called
#if !SUPPORT_NONPOINTER_ISA
#define RW_INSTANCES_HAVE_ASSOCIATED_OBJECTS (1<<22) // class instances may have associative references
#endif
#define RW_HAS_INSTANCE_SPECIFIC_LAYOUT (1 << 21) // class has instance-specific GC layout
// #define RW_20 (1<<20) // available for use
#define RW_REALIZING (1<<19) // class has started realizing but not yet completed it
#define RW_HAS_CXX_CTOR (1<<18) // class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_DTOR (1<<17) // class or superclass has .cxx_destruct implementation
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ (1<<16)
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA (1<<15) // class's instances requires raw isa
#endif

demangledName 是计算机语言用于解决实体名称惟一性的一种方法,作法是向名称中添加一些类型信息,用于从编译器中向连接器传递更多语义信息。

realizeClass

在某个类初始化以前,objc_class->data() 返回的指针指向的实际上是个 class_ro_t 结构体。等到 static Class realizeClass(Class cls) 静态方法在类第一次初始化时被调用,它会开辟 class_rw_t 的空间,并将 class_ro_t 指针赋值给 class_rw_t->ro。这种偷天换日的行为是靠 RO_FUTURE 标志位来记录的:

1
2
3
4
5
6
7
8
9
10
11
12
13
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// 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 {
// 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);
}

注意以前 RO 和 RW flags 宏标记的一个细节:

1
2
3
4
5
#define RO_FUTURE (1<<30)
#define RO_REALIZED (1<<31)

#define RW_REALIZED (1<<31)
#define RW_FUTURE (1<<30)

也就是说 ro = (const class_ro_t *)cls->data(); 这种强转对于接下来的 ro->flags & RO_FUTURE 操做彻底是 OK 的,两种结构体第一个成员都是 flagsRO_FUTURE 与 RW_FUTURE 值同样的。

通过 realizeClass 函数处理的类才是『真正的』类,调用它时不能对类作写操做。

Category

Category 为现有的类提供了拓展性,它是 category_t 结构体的指针。

1
typedef struct category_t *Category;

category_t 存储了类别中能够拓展的实例方法、类方法、协议、实例属性和类属性。类属性是 Objective-C 2016 年新增的特性,沾 Swift 的光。因此 category_t 中有些成员变量是为了兼容 Swift 的特性,Objective-C 暂没提供接口,仅作了底层数据结构上的兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
};

在 App 启动加载镜像文件时,会在 _read_images 函数间接调用到 attachCategories 函数,完成向类中添加 Category 的工做。原理就是向 class_rw_t 中的 method_array_tproperty_array_tprotocol_array_t 数组中分别添加 method_list_tproperty_list_tprotocol_list_t 指针。以前讲过 xxx_array_t 能够存储对应 xxx_list_t 的指针数组。

在调用 attachCategories 函数以前,会先使用 unattachedCategoriesForClass 函数获取类中还未添加的类别列表。这个列表类型为 locstamped_category_list_t,它封装了 category_t 以及对应的 header_infoheader_info 存储了实体在镜像中的加载和初始化状态,以及一些偏移量,在加载 Mach-O 文件相关函数中常常用到。

1
2
3
4
5
6
7
8
9
10
11
12
struct locstamped_category_t {
category_t *cat;
struct header_info *hi;
};

struct locstamped_category_list_t {
uint32_t count;
#if __LP64__
uint32_t reserved;
#endif
locstamped_category_t list[0];
};

因此更具体来讲 attachCategories 作的就是将 locstamped_category_list_t.list 列表中每一个 locstamped_category_t.cat中的那方法、协议和属性分别添加到类的 class_rw_t 对应列表中。header_info 中的信息决定了是不是元类,从而选择应该是添加实例方法仍是类方法、实例属性仍是类属性等。源码在 objc-runtime-new.mm 文件中,很好理解。

Method

Method是一种表明类中的某个方法的类型。

1
typedef struct method_t *Method;

而 objc_method 在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct method_t {
SEL name;
const char *types;
IMP 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; }
};
};
  • 方法名类型为 SEL,前面提到过相同名字的方法即便在不一样类中定义,它们的方法选择器也相同。
  • 方法类型 types 是个char指针,其实存储着方法的参数类型和返回值类型。
  • imp 指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

Ivar

Ivar 是一种表明类中实例变量的类型。

1
typedef struct ivar_t *Ivar;

而 ivar_t 在上面的成员变量列表中也提到过:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct 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;

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

能够根据实例查找其在类中的名字,也就是“反射”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}

class_copyIvarList 函数获取的不只有实例变量,还有属性。但会在本来的属性名前加上一个下划线。

objc_property_t

@property 标记了类中的属性,这个没必要多说你们都很熟悉,它是一个指向objc_property 结构体的指针:

1
typedef struct property_t *objc_property_t;

能够经过 class_copyPropertyList 和 protocol_copyPropertyList 方法来获取类和协议中的属性:

1
2
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回类型为指向指针的指针,哈哈,由于属性列表是个数组,每一个元素内容都是一个 objc_property_t 指针,而这两个函数返回的值是指向这个数组的指针。

举个栗子,先声明一个类:

1
2
3
4
5
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end

你能够用下面的代码获取属性列表:

1
2
3
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你能够用 property_getName 函数来查找属性名称:

1
const char *property_getName(objc_property_t property)

你能够用class_getProperty 和 protocol_getProperty经过给出的名称来在类和协议中获取属性的引用:

1
2
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你能够用property_getAttributes函数来发掘属性的名称和@encode类型字符串:

1
const char *property_getAttributes(objc_property_t property)

把上面的代码放一块儿,你就能从一个类中获取它的属性啦:

1
2
3
4
5
6
7
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

对比下 class_copyIvarList 函数,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。

protocol_t

虽然 Objective-C 的 Category 和 protocol 拓展能力有限,但也得为了将就 Swift 的感觉,充个胖子。

flags 32 位指针最后两位是给加载 Mach-O 的 fix-up 阶段使用的,前 16 位预留给 Swift 用的。

protocol 主要内容实际上是(可选)方法,其次就是继承其余 protocol。Swift 还支持 protocol 多继承,因此须要 protocols 数组来作兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
... 省略一些封装的便捷 get 方法
}

IMP

IMPobjc.h中的定义是:

1
typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息以后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然获得了执行某个实例某个方法的入口,咱们就能够绕开消息传递阶段,直接执行方法,这在后面会提到。

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

消息

前面作了这么多铺垫,如今终于说到了消息了。Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

有关消息发送和消息转发机制的原理,能够查看这篇文章

objc_msgSend 函数

在引言中已经对objc_msgSend进行了一点介绍,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 selector 是否是要忽略的。好比 Mac OS X 开发,有了垃圾回收就不理会 retainrelease 这些函数了。
  2. 检测这个 target 是否是 nil 对象。ObjC 的特性是容许对一个 nil 对象执行任何一个方法不会 Crash,由于会被忽略掉。
  3. 若是上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找获得就跳到对应的函数去执行。
  4. 若是 cache 找不到就找一下方法分发表。
  5. 若是分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
  6. 若是还找不到就要开始进入动态方法解析了,后面会提到。

PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。

其实编译器会根据状况在objc_msgSendobjc_msgSend_stretobjc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。若是消息是传递给超类,那么会调用名字带有”Super”的函数;若是消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。

值得一提的是在 i386 平台处理返回类型为浮点数的消息时,须要用到objc_msgSend_fpret函数来进行处理,这是由于返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend再也不适用,因而objc_msgSend_fpret被派上用场,它会对浮点数寄存器作特殊处理。不过在 PPC 或 PPC64 平台是不须要麻烦它的。

PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别表明“struct”和“return”;“fpret”就是“fp”+“ret”,分别表明“floating-point”和“return”。

方法中的隐藏参数

咱们常常在方法中使用self关键字来引用实例自己,但从没有想过为何self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷的动态传入的。

objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中全部的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)

之因此说它们是隐藏的是由于在源代码方法的定义中并无声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中咱们仍然能够引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法自己的选择器:

1
2
3
4
5
6
7
8
9
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();

if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。

而当方法中的super关键字接收到消息时,编译器会建立一个objc_super结构体:

1
struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self自己,这点须要注意,由于当咱们想经过[super class]获取超类时,编译器只是将指向selfid指针和class的SEL传递给了objc_msgSendSuper函数,由于只有在NSObject类才能找到class方法,而后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,因此咱们获得的永远都是self的类型。

获取方法地址

IMP那节提到过能够避开消息绑定而直接获取方法的地址并调用方法。这种作法不多用,除非是须要持续大量重复调用某方法的极端状况,避开消息发送泛滥而直接调用该方法会更高效。

NSObject类中有个methodForSelector:实例方法,你能够用它来获取某个方法选择器对应的IMP,举个栗子:

1
2
3
4
5
6
7
void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);

当方法被当作函数调用时,上节提到的两个隐藏参数就须要咱们明确给出了。上面的例子调用了1000次函数,你能够试试直接给target发送1000次setFilled:消息会花多久。

PS:methodForSelector:方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。

动态方法解析

你能够动态地提供一个方法的实现。例如咱们能够用@dynamic关键字在类的实现文件中修饰一个属性:

1
@dynamic propertyName;

这代表咱们会为这个属性动态提供存取方法,也就是说编译器不会再默认为咱们生成setPropertyName:propertyName方法,而须要咱们动态提供。咱们能够经过分别重载resolveInstanceMethod:resolveClassMethod:方法分别添加实例方法实现和类方法实现。由于当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:来给程序员一次动态添加方法实现的机会。咱们须要用class_addMethod函数完成向特定类添加特定方法实现的操做:

1
2
3
4
5
6
7
8
9
10
11
12
13
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding

PS:动态方法解析会在消息转发机制浸入前执行。若是 respondsToSelector: 或 instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。若是你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO

评论区有人问如何用 resolveClassMethod: 解析类方法,我将他贴出有问题的代码作了纠正和优化后以下,能够顺便将实例方法和类方法的动态方法解析对比下:

头文件:

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

m 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import "Student.h"
#import <objc/runtime.h>

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end

须要深入理解 [self class] 与 object_getClass(self) 甚至 object_getClass([self class]) 的关系,其实并不难,重点在于 self 的类型:

  1. 当 self 为实例对象时,[self class] 与 object_getClass(self) 等价,由于前者会调用后者。object_getClass([self class]) 获得元类。
  2. 当 self 为类对象时,[self class] 返回值为自身,仍是 selfobject_getClass(self) 与 object_getClass([self class]) 等价。

凡是涉及到类方法时,必定要弄清楚元类、selector、IMP 等概念,这样才能作到触类旁通,随机应变。

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给咱们一次偷梁换柱的机会,即经过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其余对象:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

毕竟消息转发要耗费更多时间,抓住此次机会将消息重定向给别人是个不错的选择,不过千万别返回self,由于那样会死循环。 若是此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);不然将向返回的对象从新发送消息。

若是想替换类方法的接受者,须要覆写 + (id)forwardingTargetForSelector:(SEL)aSelector 方法,并返回类对象

1
2
3
4
5
6
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}

转发

当动态方法解析不做处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,咱们能够重写这个方法来定义咱们的转发逻辑:

1
2
3
4
5
6
7
8
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}

该消息的惟一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。咱们能够实现forwardInvocation:方法来对不能处理的消息作一些默认的处理,也能够将消息转发给其余对象来处理,而不抛出错误。

这里须要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。因此咱们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,不然会抛异常。

当一个对象因为没有相应的方法实现而没法响应某消息时,运行时系统将经过forwardInvocation:消息通知该对象。每一个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。经过实现咱们本身的forwardInvocation:方法,咱们能够在该方法实现中将消息转发给其它对象。

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不一样接收对象。或者它也能够象一个运输站将全部的消息都发送给同一个接收对象。它能够将一个消息翻译成另一个消息,或者简单的”吃掉“某些消息,所以没有响应也没有错误。forwardInvocation:方法也能够对不一样的消息提供一样的响应,这一切都取决于方法的具体实现。该方法所提供是将不一样的对象连接到消息链的能力。

注意: forwardInvocation:方法只有在消息接收对象中没法正常响应消息时才会被调用。 因此,若是咱们但愿一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。不然,forwardInvocation:将不可能会被调用。

转发和多继承

转发和继承类似,能够用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另外一个对象中的方法借过来或是“继承”过来同样。

这使得不一样继承体系分支下的两个类能够“继承”对方的方法,在上图中WarriorDiplomat没有继承关系,可是Warriornegotiate消息转发给了Diplomat后,就好似DiplomatWarrior的超类同样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了由于多继承致使单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,并且转发机制是透明的。

替代者对象(Surrogate Objects)

转发不只能模拟多继承,也能使轻量级对象表明重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来作了。这里有一些适用案例,能够参看官方文档

转发与继承

尽管转发很像继承,可是NSObject类不会将二者混淆。像respondsToSelector: 和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。好比上图中一个Warrior对象若是被问到是否能响应negotiate消息:

1
2
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

结果是NO,尽管它可以接受negotiate消息而不报错,由于它靠转发消息给Diplomat类来响应消息。

若是你为了某些意图偏要“弄虚做假”让别人觉得Warrior继承到了Diplomatnegotiate方法,你得从新实现 respondsToSelector: 和 isKindOfClass:来加入你的转发算法:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

除了respondsToSelector: 和 isKindOfClass:以外,instancesRespondToSelector:中也应该写一份转发算法。若是使用了协议,conformsToProtocol:一样也要加入到这一行列中。相似地,若是一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。好比一个对象能给它的替代者对象转发消息,它须要像下面这样实现methodSignatureForSelector:

1
2
3
4
5
6
7
8
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

健壮的实例变量 (Non Fragile ivars)

在 Runtime 的现行版本中,最大的特色就是健壮的实例变量。当一个类被编译时,实例变量的布局也就造成了,它代表访问类的实例变量的位置。从对象头部开始,实例变量依次根据本身所占空间而产生位移:

上图左边是NSObject类的实例变量布局,右边是咱们写的类的布局,也就是在超类后面加上咱们本身类的实例变量,看起来不错。但试想若是哪天苹果更新了NSObject类,发布新版本的系统的话,那就悲剧了:

咱们自定义的类被划了两道线,那是由于那块区域跟超类重叠了。惟有苹果将超类改成之前的布局才能拯救咱们,但这样也致使它们不能再拓展它们的框架了,由于成员变量布局被死死地固定了。在脆弱的实例变量(Fragile ivars) 环境下咱们须要从新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下会发生什么呢?

在健壮的实例变量下编译器生成的实例变量布局跟之前同样,可是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。

须要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))来代替。

优化 App 的启动时间 讲过加载 Mach-O 文件时有个步骤是经过 fix-up 修改偏移量来解决 fragile base class。

Objective-C Associated Objects

在 OS X 10.6 以后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有如下三个:

1
2
3
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 );

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。有关 Objective-C 引用计数机制的原理,能够查看这篇文章

Method Swizzling

以前所说的消息转发虽然功能强大,但须要咱们了解而且能更改对应类的源代码,由于咱们须要实现本身的转发逻辑。当咱们没法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,可是有时没法达到目的。这里介绍的是 Method Swizzling ,它经过从新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的作法更为隐蔽,甚至有些冒险,也增大了debug的难度。

PS: 对于熟练使用 Method Swizzling 的开发者,能够跳过此章节,看看我另外一篇『稍微深刻』一点的文章 Objective-C Method Swizzling

这里摘抄一个 NSHipster 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import <objc/runtime.h> 

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

@end

上面的代码经过添加一个Tracking类别到UIViewController类中,将UIViewController类的viewWillAppear:方法和Tracking类别中xxx_viewWillAppear:方法的实现相互调换。Swizzling 应该在+load方法中实现,由于+load是在一个类最开始加载时调用。dispatch_once是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操做,线程安全是很重要的。

若是类中不存在要替换的方法,那就先用class_addMethodclass_replaceMethod函数添加和替换两个方法的实现;若是类中已经有了想要替换的方法,那么就调用method_exchangeImplementations函数交换了两个方法的 IMP,这是苹果提供给咱们用于实现 Method Swizzling 的便捷方法。

可能有人注意到了这行:

1
2
3
4
5
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self) 与 [self class] 返回的结果类型都是 Class,但前者为元类,后者为其自己,由于此时 self 为 Class 而不是实例.注意 [NSObject class] 与 [object class] 的区别:

1
2
3
4
5
6
7
+ (Class)class {
return self;
}

- (Class)class {
return object_getClass(self);
}

PS:若是类中没有想被替换实现的原方法时,class_replaceMethod至关于直接调用class_addMethod向类中添加该方法的实现;不然调用method_setImplementation方法,types参数会被忽略。method_exchangeImplementations方法作的事情与以下的原子操做等价:

1
2
3
4
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

最后xxx_viewWillAppear:方法的定义看似是递归调用引起死循环,其实不会的。由于[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被咱们与viewWillAppear:方法实现进行了互换,因此这段代码不只不会死循环,若是你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引起死循环。

看到有人说+load方法自己就是线程安全的,由于它在程序刚开始就被调用,不多会碰到并发问题,因而 stackoverflow 上也有大神给出了另外一个 Method Swizzling 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
NSLog(@"arg1 is %@", arg1);
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}

上面的代码一样要添加在某个类的类别中,相比第一个种实现,只是去掉了dispatch_once 部分。

Method Swizzling 的确是一个值得深刻研究的话题,找了几篇不错的资源推荐给你们:

在用 SpriteKit 写游戏的时候,由于 API 自己有一些缺陷(增删节点时不考虑父节点是否存在啊,很容易崩溃啊有木有!),我在 Swift 上使用 Method Swizzling弥补这个缺陷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
extension SKNode {

class func yxy_swizzleAddChild() {
let cls = SKNode.self
let originalSelector = #selector(SKNode.addChild(_:))
let swizzledSelector = #selector(SKNode.yxy_addChild(_:))
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}

class func yxy_swizzleRemoveFromParent() {
let cls = SKNode.self
let originalSelector = #selector(SKNode.removeFromParent)
let swizzledSelector = #selector(SKNode.yxy_removeFromParent)
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}

@objc func yxy_addChild(_ node: SKNode) {
if node.parent == nil {
self.yxy_addChild(node)
}
else {
print("This node has already a parent!\(String(describing: node.name))")
}
}

@objc func yxy_removeFromParent() {
if parent != nil {
DispatchQueue.main.async(execute: { () -> Void in
self.yxy_removeFromParent()
})
}
else {
print("This node has no parent!\(String(describing: name))")
}
}

}

而后其余地方调用那两个类方法:

1
2
SKNode.yxy_swizzleAddChild()
SKNode.yxy_swizzleRemoveFromParent()

由于 Swift 中的 extension 的特殊性,最好在某个类的load() 方法中调用上面的两个方法.我是在AppDelegate 中调用的,因而保证了应用启动时可以执行上面两个方法.

总结

咱们之因此让本身的类继承 NSObject 不只仅由于苹果帮咱们完成了复杂的内存分配问题,更是由于这使得咱们可以用上 Runtime 系统带来的便利。可能咱们平时写代码时可能不多会考虑一句简单的 [receiver message] 背后发生了什么,而只是当作方法或函数调用。深刻理解 Runtime 系统的细节更有利于咱们利用消息机制写出功能更强大的代码,好比 Method Swizzling 等。

Update 20170820: 使用 objc4-709 源码重写部分章节,更新至 Swift 4 代码示例。

参考连接:

相关文章
相关标签/搜索