iOS Runtime 复习整理

Runtime复习整理

Runtime 是一个用 C 语言和编译语言写的API,称为“运行时”,它的应用使 Objective-C 拥有相当多的动态特性。具体怎么实现的呢。我们从下面几个方面探寻 Runtime 的实现机制。

  • Runtime 介绍
  • Runtime 交互方式
  • Runtime 常用术语的数据结构
  • Runtime 消息传递
  • Runtime 消息转发
  • Runtime 应用

Runtime介绍

本章节包括:

  • Runtime概述
  • 代码到可执行文件过程
  • 源码地址
  • 版本和平台

Runtime概述

Objective-C 是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OCC语言的过渡就是由 runtime 来实现的。在Runtime中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。

对于 C 语言,函数的调用会在编译期就已经决定好,在编译完成后直接顺序执行。但是 OC 是一门动态语言,函数调用变成了消息发送,在编译期不能知道要调用哪个函数。所以 Runtime 无非就是去解决如何在运行时期来动态得创建类和对象、进行消息传递和转发。

理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。

代码到可执行文件过程

这是《深入理解计算机系统(第2版)》里面的一张截图:

img

主要过程我们可以简化成三个:

1
2
3
- 编译
- 链接
- 运行

编译:将代码转换成底层可执行的语言(如汇编),简单来讲,就是把你能看懂的语言,转换成系统底层可以看懂的东西,这中间通常会有优化,先预处理,再编译。

链接:在编译的过程中,如果有调用其他的类的方法等,是不会检查或者报警的,编译的时候会默认你已经实现了。而链接就是去检查调用的方法或者类等是否确实存在。

运行:执行最终的可执行文件

如果是普通的C语言代码,我们使用的是传统的编译运行,那么一个函数的执行内容,在编译阶段其实就确定了,执行的时候只要去执行对应的内存地址的程序就好。

而在runtime中,编译阶段只能确定最终要执行的函数名,但是具体执行的时候,执行的是什么程序,是在运行的时候才能确定,大大增加了程序的灵活性。

源码地址

Runtime 基本是用 C汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。在这里先放上runtime的源码和runtime官方api:

版本和平台

Runtime是有个两个版本的: legacy 、 modern
在Objective-C 1.0使用的是legacy,在2.0使用的是modern。这里简单介绍下区别:

  • 在legacy runtime,如果你改变了实例变量的设计,需要重新编译它的子类。支持 32bit的OS X 程序
  • 在modern runtime,如果你改变了实例变量的设计,不需要重新编译它的子类。支持iphone程序和OS X10.5之后的64bit程序

因为legacy是如此的古老,我们基本可以忽略legacy版本。

Runtime 的交互

Objc 在三种层面上与 Runtime 系统进行交互:

  • Objective-C 源代码
  • NSObject
  • Runtime 库

Objective-C 源代码

多数情况我们只需要编写 OC 代码即可,Runtime 系统自动在幕后搞定一切,编译器会将 OC 代码转换成运行时代码,在运行时确定数据结构和函数。

NSObject

Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类时个例外,它是个抽象超类)

一些情况下,NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 -description 方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。

还有一些 NSObject 的方法可以从 Runtime 系统中获取信息,允许对象进行自我检查。例如:

  • -class方法返回对象的类;
  • -isKindOfClass:-isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
  • -respondsToSelector: 检查对象能否响应指定的消息;
  • -conformsToProtocol:检查对象是否实现了指定协议类的方法;
  • -methodForSelector: 返回指定方法实现的地址。

Runtime 库

Runtime 系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入objc/Runtime.h头文件即可。

许多函数可以让你使用纯 C 代码来实现 Objc 中同样的功能。除非是写一些 Objc 与其他语言的桥接或是底层的 debug 工作,否则你一般不会直接用到这些函数的,在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime 常用术语的数据结构

下面讲讲Runtime用到的一些概念:

  • 类对象(objc_class)
  • 实例(objc_object)
  • 元类(Meta Class)
  • Ivar(objc_ivar)
  • Method(objc_method)
  • SEL(objc_selector)
  • IMP
  • 类缓存(objc_cache)
  • Property(objc_property_t)
  • Category(objc_category)

类对象(objc_class)

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

1
typedef struct objc_class *Class;

查看objc/runtime.hobjc_class结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list *methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
}OBJC2_UNAVAILABLE;

struct objc_class结构体定义了很多变量,通过命名不难发现,
结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,这些数据称为元数据(metadata),该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

实例(objc_object)

objc_object是表示一个类的实例的结构体它的定义如下(objc/objc.h):

1
2
3
4
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。

元类(Meta Class)

类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?

在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念为了调用类方法,meta-class中存储着一个类的所有类方法.所以,调用类方法的这个类对象的isa指针指向的就是meta-class。

当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。

即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下图

img

通过上图我们可以看出整个体系构成了一个自闭环,

实例对象(objc_object) 的isa指针指向类对象(objc_class),类对象的isa指针指向了元类(Meta Class),元类的isa指针指向基类(NSObject)的元类,基类(NSObject)的元类指向它自己。

类对象(objc_class)的super_class指针指向了父类的类对象

元类(Meta Class))的super_class指针指向了父类的元类

Ivar(objc_ivar)

Ivar 是表示成员变量的类型。

1
2
3
4
5
6
7
8
9
10
typedef struct objc_ivar *Ivar;

struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}

其中 ivar_offset 是基地址偏移字节

Method(objc_method)

Method 代表类中某个方法的类型

1
2
3
4
5
6
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;//方法名
char *method_types OBJC2_UNAVAILABLE;//char 指针,存储方法的参数类型和返回值类型
IMP method_imp OBJC2_UNAVAILABLE;//方法的实现,本质是一个函数指针
}

在这个结构体重,我们已经看到了SELIMP,说明SELIMP其实都是Method的属性。

SEL(objc_selector)

先看下定义

1
2
3
@include "Objc.h"
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

首先区分下selectorSEL的的关系,我们先看下selector的定义

1
@property SEL selector;

可以看到SEL 是一种数据结构,selector是它的实例。

selector是方法选择器,可以理解为区分方法的 ID,可以让我们能够快速找到对应的函数。而这个 ID 的数据结构是SEL

在iOS中,runtime会在运行的时候,通过load函数,将所有的methodhash然后map到set中。这样在运行的时候,寻找selector的速度就会非常快,不会因为runtime特性牺牲太多的性能。

1
A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了methodname,没有参数,所以没法区分不同的method

比如:

1
2
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

是会报错的。

我们只能通过命名来区别:

1
2
- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。

IMP

看下IMP的定义

1
2
/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);

就是指向最终实现程序的内存地址的指针。

iOSRuntime中,Method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

Cache(objc_cache)

Cache 定义如下:

1
2
3
4
5
6
7
typedef struct objc_cache *Cache

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。

Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。

Property(objc_property_t)

1
2
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyListprotocol_copyPropertyList 方法获取类和协议中的属性:

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

Category(objc_category)

Category是表示一个指向分类的结构体的指针,其定义如下:

1
2
3
4
5
6
7
8
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;
};
1
2
3
4
5
6
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

Runtime 消息传递(dispatch table)

本章节分为以下三个部分:

  • 消息传递介绍
  • 消息传递流程
  • 方法中的隐藏参数

消息传递介绍

基础 Runtime 术语讲完了,接下来就要说到消息了。体会苹果官方文档中的:

messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。

当你写下面这样的代码时

1
[tableView cellForRowAtIndexPath:indexPath];

编译器实际上把它转换成下面这样的C函数调用

1
objc_msgSend(tableView, @selector(cellForRowAtIndexPath:), indexPath);

先看看苹果官方关于消息发送方法的说明:

When it encounters a method invocation, the compiler might generate a call to any of several functions to perform the actual message dispatch, depending on the receiver, the return value, and the arguments. You can use these functions to dynamically invoke methods from your own plain C code, or to use argument forms not permitted by NSObject’s perform...methods. These functions are declared in /usr/include/objc/objc-runtime.h.

  • objc_msgSend sends a message with a simple return value to an instance of a class.
  • objc_msgSend_stret sends a message with a data-structure return value to an instance of a class.
  • objc_msgSendSuper sends a message with a simple return value to the superclass of an instance of a class.
  • objc_msgSendSuper_stret sends a message with a data-structure return value to the superclass of an instance of a class.

解释下上面的意思:

当遇到方法调用时,编译器可能会生成以下几个函数的调用,以执行实际的消息调度,具体取决于接收者,返回值和参数。 这些函数在/usr/include/objc/objc-runtime.h中声明。具体生成规则如下:

objc_msgSend //将具有简单返回值的消息发送到类的实例。

objc_msgSend_stret //将具有数据结构返回值的消息发送到类的实例。

objc_msgSendSuper //将具有简单返回值的消息发送到类的实例的超类。

objc_msgSendSuper_stret // 将具有数据结构返回值的消息发送到类实例的超类。

举例说明:

1
2
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
CGRect rect = [view frame];

由于返回值是结构体类型,编译器不会把它转换成 msgSend, 而是调用 objc_msgSend_stret,stret就是 struct return的缩写

消息传递流程

下面我们以objc_msgSend为例,详细叙述消息发送的步骤(如下图):

img

  1. 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
  2. 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
  3. 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
  4. 如果 cache 找不到就找类的方法列表中是否有对应的方法。
  5. 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
  6. 如果还找不到,就要开始进入动态方法解析了,后面会提到。

方法中的隐藏参数

疑问:
我们经常用到关键字 self ,但是 self 是如何获取当前方法的对象呢?

其实,这也是 Runtime 系统的作用,self 实在方法运行时被动态传入的。

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

  • 接受消息的对象(self 所指向的内容,当前方法的对象指针)
  • 方法选择器(_cmd 指向的内容,当前方法的 SEL 指针)

因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们时在代码被编译时被插入方法实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。

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

Runtime 消息转发(Message Forwarding)

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform…的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

1
2
3
if([self respondsToSelector:@selector(method)]){
[self performSelector:@selector(method)];
}

不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)”机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

  • 动态方法解析 - resolveInstanceMethod/resolveClassMethod
  • 重定向 - forwardingTargetForSelector
  • 转发 - forwardInvocation

动态方法解析 - resolveInstanceMethod

对象在接收到未知的消息时,首先会调用所属类的类方法
+resolveInstanceMethod:(实例方法)或者
+resolveClassMethod:(类方法)。

在这个方法中,我们有机会为该未知消息新增一个“处理方法”,通过运行时class_addMethod函数动态添加到类里面就可以了。这种方案更多的是为了实现@dynamic属性。

举例:如果我们执行ClassA并不存在的foo方法,如果我们什么都不处理在运行时程序会崩溃。

1
2
ClassA *a = [ClassA new];
[a performSelector:@selector(foo)];

如果想要用 resolveInstanceMethod来补救 ,该怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <objc/runtime.h>
void foo(id self, SEL _cmd) {
NSLog(@"resolveInstanceMethod add method foo ");
}
@implementation ClassA
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(foo)) {
class_addMethod([self class], sel, (IMP)foo, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

这里的return YES 或者 return NO,是告诉系统是否实现了这个方法,如果return YES,但是并没有增加方法,还是会报错,并且不会走到forward,因为系统默认你已经在这一步做了resolveInstanceMethod这个事情。

重定向 - forwardingTargetForSelector

如果上一步骤的resolveInstanceMethod return no,系统会走forwardingTargetForSelector,这一步被称为快速转发,是因为相对下面要介绍的normal fastward,这一步直接转发了消息,而normal fastward生成了NSInvocation,相对直接转发慢一些。

先看下如何实现,比如,我想把消息转发给有能力的classB:

1
2
3
4
5
6
7
8
9
@interface ClassB : NSObject
- (void)foo;
@end

@implementation ClassB
- (void)foo{
NSLog(@"ClassB foo run");
}
@end

A中需要实现forwardingTargetForSelector方法:

1
2
3
4
5
6
7
8
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(foo)){
ClassB *b = [ClassB new];
return b;
}
return [super forwardingTargetForSelector:aSelector];
}

转发 - forwardInvocation

如果你的类没有实现forwardingTargetForSelector方法,系统会调用methodSignatureForSelector方法,如果这个方法返回一个函数的签名,则执行forwardInvocation方法,否则执行doesNotRecognizeSelector

如果希望在这一步补救,如何做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [ClassB instanceMethodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL sel = invocation.selector;
ClassB *b = [ClassB new];

if([b respondsToSelector:sel]) {
[invocation invokeWithTarget:b];
}
else {
[self doesNotRecognizeSelector:sel];
}
}

苹果的文档里,讲述了这一个消息转发的出发点,其实是为了实现类似C多继承的功能。我们知道,在C中如果一个类想要具有多个类的功能,是可以直接继承多个类的。而Objective-C是单继承,如果想实现类似的功能,就用消息转发,将消息转发给有能力处理的类。苹果是这样描述他们的思想的:C的多继承,是加法,在多继承的同时,其实也增加了很多不需要的功能,而苹果通过消息转发,实现了减法的思想,只留有用的方法,而不去增加过多内容。

Runtime应用

Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

  • 遍历结构体中数据(Ivar、objc_property_t、Method、Protocol)
    • 获取对应列表
    • NSCoding的自动归档/解档
    • 模型/字典互转
  • 动态添加属性
  • 动态添加方法与方法转发
  • 黑魔法(Method Swizzling)
  • KVO实现

遍历结构体中数据

获取对应列表

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
1、获取成员变量
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i=0; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
class_copyIvarList
ivar_getName
2、获取属性
unsigned int count;
//获取类的属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
class_copyPropertyList
property_getName
3、方法
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i=0; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
class_copyMethodList
method_getName
4、协议
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i=0; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
class_copyProtocolList
protocol_getName

NSCoding的自动归档/解档

原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。
核心方法:在Model的基类中重写方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}

模型/字典互转(MJExtension)

原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
核心方法:在NSObject的分类中添加方法

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
- (instancetype)initWithDict:(NSDictionary *)dict {

if (self = [self init]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);

//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;

}

动态添加属性

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
//.h文件
#import "NSObject.h"
@interface NSObject (Property)
//@property在分类中只会生成set、get方法的声明 不会生成实现,也不会生成_成员属性
@property (nonatomic,copy)NSString name;
@end
-----------------------------------------------
//.m文件
// 定义关联的key
static const char *key = "name";
@implementation NSObject (Property)
- (NSString *)name
{
// 根据关联的key,获取关联的值。
return objc_getAssociatedObject(self, key);
}
- (void)setName:(NSString *)name
{
/*
第一个参数:给哪个对象添加关联
第二个参数:关联的key,通过这个key获取
第三个参数:关联的value
第四个参数:关联的策略
*/
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

动态添加方法与方法转发

关于消息转发,前面已经讲到过了,消息转发分为三级,我们可以在每级实现替换功能,实现消息转发,从而不会造成崩溃。同学们可以自行前往前面章节查看案例。JSPatch 是它成功使用的案例

JSPatch 是一个 iOS 动态更新框架,不仅能够实现消息转发,还可以实现方法添加、替换能一系列功能。只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。

黑魔法(Method Swizzling)

下面实现一个替换ViewControllerviewDidLoad方法的例子。

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
@implementation ViewController

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(jkviewDidLoad);

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

//judge the method named swizzledMethod is already existed.
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// if swizzledMethod is already existed.
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

- (void)jkviewDidLoad {
NSLog(@"替换的方法");
[self jkviewDidLoad];
}

- (void)viewDidLoad {
NSLog(@"自带的方法");
[super viewDidLoad];
}
@end

swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

KVO实现

全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。

KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPathsetter 方法。setter 方法随后负责通知观察对象属性的改变状况。

Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVONSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

  • NSKVONotifying_A 类剖析
1
2
NSLog(@"self->isa:%@",self->isa);  
NSLog(@"self class:%@",[self class]);

在建立KVO监听前,打印结果为:

1
2
self->isa:A
self class:A

在建立KVO监听之后,打印结果为:

1
2
self->isa:NSKVONotifying_A
self class:A

在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被KVO 机制修改为指向系统新创建的子类NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。

  • 子类setter方法剖析

KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:didChangeValueForKey: ,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;
当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

1
2
3
4
5
- (void)setName:(NSString *)newName { 
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}

总结

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