iOS深入学习 - Runtime

SmallTalk 与 C 的融合–Objective-C

三十几年前,Brad Cox 和 Tom Love在主流且高效的C语言基础上,借鉴Smalltalk的面向对象与消息机制,想要搞出一个易用且轻量的C语言扩展,但C和Smalltalk的思想和语法格格不入,比如在Smalltalk中一切皆对象,一切调用都是消息:

1
233 log

再比如用一个工厂方法来实例化一个对象:

1
p := Person name: 'sunnyxx' age: 26

在当时来看,一个具有面向对象功能的C语言真的是非常有吸引力,但必须得解决消息语法的转换,于是乎他们开发了一个Preprocessor(预编译程序),去解析Smalltalk风格的语法,再转换成C语言的代码,进而和其他C代码一起编译。想法很美好,但Smalltalk语法里又是空格、又是冒号的,万一遇到个什么复杂嵌套调用,语法解析多难写呀,于是乎他们想,把消息两边加个中括号吧,这样Parser写起来简单多了:

1
[Person name: "sunnyxx" age: 26];

这就造就了Objective-C奇怪的中括号、冒号四不像语法,这怎么看都是个临时的方案,但当时可能是唯一的方法,借用已有的C的编译器比重造一个成本低多了,而且完全兼容C语言。随着这几年Apple开发的火热,Objective-C越来越成为Apple不爽的地方,先是恨透了在GCC上给Objective-C加支持,自己重建了个Clang,后是干脆重新发明了Swift来彻底代替,用了30年的时间终于还完了技术债。

虽然有了个Preprocessor,但只能做到把Smalltalk风格的代码分析并转译成C,还需要解决两个问题:

  1. C语言上实现一个OOP对象模型
  2. Smalltalk风格的Message机制转换成C函数调用

对象模型的设计倒很省事,直接搬照Smalltalk的就好了:如Class/Meta Class/Instance Method/Class Method这些概念,还有一些关键字如self/super/nil等全都是Smalltalk的。这步转换在Preprocessing过程中就可以完成,因为重写后的Class就是原原本本的C语言的Struct,只需要按Smalltalk中“类-元类”的模型设置好即可,无需额外的支持。
消息机制就不一样了,要实现向一个target(class/instance)发送消息名(selector)动态寻找到函数实现地址(IMP)并调用的过程,还要处理消息向父类传递、消息转发(Smallltalk中叫“Message-Not-Understood”)等,这些行为无法在PreprocessingBuild Time实现,需要提供若干运行时的C函数进行支持,所有这些函数打个包,便形成了最原始的Runtime。

所以最初的Objective-C = C + Preprocessor + Runtime

注:GCC中一开始用预处理器来支持Objective-C,之后作为一个编译器模块,再后来都交给了Clang实现。

作为单纯的C语言扩展,Runtime中只要实现几个最基础的函数(如objc_msgSend)即可,但为了构建整套Objective-C面向对象的基础库(如Foundation),Runtime还需要提供像NSObject这样的Root Class作为面向对象的起点、提供运行时反射机制以及运行时对Class结构修改的API等。再后来,即便是Objective-C语言本身的不断发展,新语言特性的加入,也不在乎是扩展Clang和扩展Runtime,比如:

  • ARC:编译器分析对象引用关系,在合适的位置插入内存管理的函数,并需要把这些函数打包加到Runtime中,如 ==objc_storeStrong==,==objc_storeWeak==等。同时还要处理dealloc函数,自动加入对super的调用等,具体可以看这篇文章
  • Lightweight Generics:叫做“轻量泛型”是因为只增加了编译器检查支持,而泛型信息并未影响到运行时,所以Runtime库无需改动。
  • Syntax Sugars:比如Boxed Expr(@123)、Array Literal(@[...])、Dictionary Literal(@{...})和轻量泛型一样,只是把如@123在编译rewrite成[NSNumber numberWithInt: 123]而已,无需改动Runtime。
  • Non Fragile Ivars: 类实例变量的动态调整技术,用于实现Objective-C Binary的兼容性,随着Objective-C 2.0出现,需要编译器和Runtime的共同配合,感兴趣的可以看这篇文章

因此,Runtime的精髓并非在于平日里很少接触的那些所谓的“黑魔法”Runtime API、也并非各种Swizzle大法,而是Objective-C语言层面如何处理Type、处理Value、如何设计OOP数据结构和消息机制、如何设计ABI等,去了解这么一个小而精美的C语言运行时扩展是怎么设计出来的。

相关的文章:
https://zh.wikipedia.org/wiki/Objective-C
http://web.cecs.pdx.edu/~harry/musings/SmalltalkOverview.html

Runtime简介

作为一门动态语言,Objective-C会尽可能的将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C总是使用动态的方式来解决问题。这意味着Objective-C语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码。运行时系统(runtime)扮演的角色类似于Objective-C语言的操作系统,Objective-C基于该系统来工作的。因此,runtime好比Objective-C的灵魂,很多东西都是在这个基础上出现的。所以它是值得你花功夫去理解的。

与静态语言编译后的区别

1、静态语言

一个静态语言程序,如下所示的C程序:

1
2
3
4
5
#include <stdio.h>
int main(int argc, const char **argv[]) {
printf("Hello World");
return 0;
}

会经过编译器的语法分析,优化然后将你最佳化的代码编译成汇编语言,然后完全按照你设计的逻辑和你的代码自上而下执行。

2、Objective-C 动态语言

很常见的一个消息发送语句:

[receiver message]

会被编译器转化成

objc_msgSend(receiver, selector)

如果有参数则为

objc_msgSend(receiver, selector, arg1, arg2, …)

消息只有到运行时才会和函数实现绑定起来,而不是按照编译好的逻辑一成不变的执行。按照作者的理解,编译阶段只是确定了要去向receiver对象发送message消息,但是却没有发送,真正发送是等到运行的时候进行。因此,编译阶段完全不知道message方法的具体实现,甚至,该方法到底有没有被实现也不知道。这就有可能导致运行时奔溃问题。

Objective-C Runtime的几点说明

1、runtime是开源的

目前Apple公司和GNU公司各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。其中Apple的版本可以在工程中引用
#import <objc/runtime.h> 点击右键jump to definition,进去查看

2、runtime是由C语言实现的

runtime作为Objective-C最核心的部分,几乎全部由C语言实现。这里的“几乎”所指的例外就包含有的方法(比如下面要说到的objc_msgSend方法)甚至是用汇编实现的

3、runtime的两个版本

Objective-C运行时系统有两个已知版本:早期版本(Legacy)和现行版本(Modern)。
在现行版本中,最显著的新特性就是实例变量是“健壮”(non-fragile)的:

在早期版本中,如果你改变类中实例变量的布局,你必须重新编译该类的所有子类。
在现行版本中,如果你改变类中实例变量的布局,你无需重新编译该类的任何子类。
此外,现行版本支持声明property的synthesis属性器。

和Runtime system交互的三种方式

1、通过Objective-C源代码

大部分情况下,运行时系统在后台自动运行,我们只需要编写和编译Objective-C源代码。
当编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据结构包含类定义和协议定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论的类的对象协议类的对象,方法选标,实例变量模板,以及其他来自于源代码的信息。运行时系统的主要功能就是根据源代码中的表达式发送消息。

2、通过类NSObject的方法

Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承了NSObject的行为(NSProxy类是个例外)。然而,某些情况下,NSObject类仅仅定义了完成某件事情的模板,而没有提供所有需要的代码。

例如,NSObject类定义了description方法,返回该类内容的字符串表示。这主要是用来调试程序–GDB中的print-object方法就是直接打印出该方法返回的字符串。NSObject类中该方法的实现并不知道子类中的内容,所以它只是返回类的名字和对象的地址。NSObject的子类可以重新实现该方法以提供更多的信息。

某些NSObject的方法只是简单的从运行时系统中获取信息,从而允许对象进行一定程度的自我检查。

例如,class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址

3、通过运行时系统的函数

运行时系统是一个有公开接口的动态库,由一些数据接口和函数的集合组成,这些数据结构和函数的声明头文件在/usr/include/objc中。这些函数支持用纯C的函数来实现和Objective-C同样的功能。还有一些函数构成了NSObject类方法的基础。这些函数使得访问运行时系统接口和提供开发工具成为可能。尽管大部分情况下它们在Objective-C程序不是必须的,但是有时候对于Objective-C程序来说某些函数是非常有用的。这些函数的文档参见Objective-C 2.0运行时系统参考库。

Runtime的几个概念

SEL

SEL又叫方法选择器,这到底是个什么玩意呢?在objc.h中是这样定义的:

typedef struct objc_selector *SEL;

这个SEL表示什么?首先,说白了,方法选择器仅仅是一个char*指针,仅仅表示它所代表的方法名字罢了。Objective-C在编译的时候,会根据方法的名字,生成一个用来区分这个方法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字相同,那么它们的ID都是相同的。就是说,不管是超类还是子类,不管有没有超类和子类的关系,只要名字相同那么ID就是一样的。

而这也就导致了Objective-C在处理有相同函数名和参数个数但参数类型不同的函数的能力非常的弱,比如当你想在程序中实现下面两个方法:

1
2
-(void)setWidth: (int)width;
-(void)setWidth: (double)width;

这样的函数则被认为是一种编译错误,而这最终导致了一个非常非常奇怪的Objective-C特色的函数命名:

1
2
-(void)setWidthIntValue: (int)width;
-(void)setWidthDoubleValue: (double)width;

可能有人会问,runtime费了老半天劲,究竟想做什么?

刚才我们说道,编译器会根据每个方法的方法名为那个方法生成唯一的SEL,这些SEL组成一个Set集合,这个Set简单的说就是一个经过了优化过的hash表。而Set的特点就是唯一,也就是SEL是唯一的,因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,犀利,速度上无与伦比。但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。

到这里,我们明白了,本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。

通过下面三种方法可以获取SEL:

  1. sel_registerName函数
  2. Objective-C 编译器提供的@selector()
  3. NSSelectorFromString()方法

IMP,方法实现的指针

IMP在objc.h中是如此定义的:

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

第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),这个比SEL要好理解多了,熟悉C语言的同学都知道,这其实是一个函数指针。
第二个参数:是方法选择器(selector)

接下来的参数:方法的参数列表

前面介绍过的SEL就是为了查找方法的最终实现IMP的,由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确的获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们可以像调用普通的C语言函数一样来使用这个函数指针了。

下面的例子,介绍了取得函数指针,即函数指针的用法:

1
2
3
void(* performMessage)(id, SEL);//定义一个IMP(函数指针)
performMessage = (void)(*)(id, SEL)[self methodForSelector: @selector(message)];//通过methodForSelector方法根据SEL获取对应的函数指针
performMessage(self, @selector(message));//通过取到的IMP(函数指针)跳过runtime消息传递机制,直接执行message方法

用IMP的方式,省去了runtime消息传递过程中所做的一系列动作,比直接向对象发送消息效率高效一些。

Method

Method用于表示类定义中的方法,则定义如下:

1
2
3
4
5
typedef struct objc_method *Methodstructobjc_method {
SEL method_name OBJC2_UNAVAILABLE; //方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; //方法实现
}

我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调节方法的实现代码

元类(Meta Class)

meta-class是一个类对象的类(注意是类对象)。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
既然是对象,那么它也是一个objc_object指针,它包含了一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?
为了调用类方法,这个类的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类的一个继承体系,如下图:

Category

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

1
2
3
4
5
6
7
typedef struct objc_category* Category {
char *category_name OBJC2_UNAVAILABLE; //分类名char *cla
ss_name OBJC2_UNAVAILABLE; //分类所属的类名structobjc
_method_list *instance_methods OBJC2_UNAVAILABLE; //实例方法列表struc
tobjc_method_list *class_methods OBJC2_UNAVAILABLE; //类方法列表str
uctobjc_protocol_list *protocols OBJC2_UNAVAILABLE; //分类所实现的协议列表
}

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性

1
struct objc_ivar_list *ivars  OBJC2_UNAVAILABLE; //该类的成员变量链表

objc_class

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

1
typedef struct object_class *Class

它的定义如下:

查看objc/runtime.h中objc_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 *protocol OBJC2_UNAVAILABLE; //协议链表
#endif
} OBJC2_UNAVAILABLE;

objc_object

objc_object是表示一个类的实例的结构体

它的定义如下(objc/objc.h):
struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};
typedef struct objc_object *id;

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

消息调用流程

传递消息所用的几个runtime方法

前面我们说过,下面的方法:

[receiver message]
objc_msgSend(receiver, selector)

实际上,同objc_msgSend方法类似的还有几个:

1
2
3
4
objc_msgSend_stret (返回值是结构体)
objc_msgSend_fpret (返回值是浮点型)
objc_msgSendSuper (调用父类方法)
objc_msgSendSuper_stret (调用父类方法,返回值是结构体)

它们的作用都是类似的,为了简单起见,后续介绍消息和消息传递机制都以objc_msgSend方法为例。

消息调用

一切还是从消息表达式[receiver message]开始,在被转换成objc_msgSend(receiver, SEL)后,在运行时,runtime system会做以下事情:

  1. 检查忽略的Selector,比如当我们运行在有垃圾回收机制的环境中,将会忽略retain和release消息。
  2. 检查receiver是否为nil。不像其他语言,nil在Objective-C中是完全合法的,并且这里有很多原因你也愿意这样,比如,至少我们省去了给一个对象发送消息前检查对象是否为空的操作。如果receiver为空,则会将selector也设置为空,并且直接返回到消息调用的地方。如果对象非空,就继续下一步。
  3. 接下来会根据SEL到当前类中查找对应的IMP,首先会在cache中检索它,如果找到了就根据函数指针跳转到这个函数执行,否则进行下一步。
  4. 检索当前类对象中的方法表(method list),如果找到了,加入cache中,并且跳转到这个函数执行,否则进行下一步。
  5. 从父类中寻找,直到根类:NSObject类。找到了就将方法加入对应类的cache表中,如果仍未找到,则要进入后文介绍的内容:动态方法决议
  6. 如果动态方法决议仍不能解决问题,只能进行最后一次尝试,进入消息转发流程
  7. 如果还不行,会奔溃。

这里的调用可以分成两部分
1、调用的方法可以找到(执行步骤1-4)
下面的图部分展示了这个调用过程:
当消息发送给一个对象时,首先从运行时系统缓存使用过的方法中寻找。如果找到,执行方法,如果没有找到继续执行下面的步骤。objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector, objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现,并将该方法添加进入缓存中,如果最后没有定位到selector,则会走动态解析流程。

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

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

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

不过,我们这边想讨论下不使用respondsToSelector: 判断的情况。
当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)”机制,通过这一机制,我们可以告诉对象如何处理位置消息。默认情况下,对象接收到未知的消息,会导致程序奔溃,通过控制台,我么可以看到以下异常信息:
这段异常信息实际上是由NSObject的“doesNotRecongnizeSelector”方法抛出的。不过,我们可以采取一些措施,在程序奔溃前执行特定的逻辑,而避免程序奔溃。
消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

消息的转发流程图:

动态方法解析
对象在接收到未知消息时,首先会调用所属类的类方法

1
2
+ resolveInstanceMethod: (实例方法) 或者
+ resolveClassMethod: (类方法)

让我们可以在程序运行时动态的为一个selector提供实现,如果我们添加了函数的实现,并返回YES,运行时系统会重启一次消息的放松过程,调用动态添加的方法。例如,下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
+ (BOOL)resolveInstanceMethod: (SEL)sel {
if (sel == @selector(foo)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "V@:");
return YES;
}

return [super resolveInstanceMethod: sel];
}

void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"%s", __PRETTY_FUNCTION__);
}

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

这种方案更多的是为了实现@dynamic属性。注:@dynamic 关键字就是告诉编译器不要做这些事,同时在使用了存储方法时也不要报错,即让编译器相信存储方法会在运行时找到。

备用接收者

1
- (id)forwardingTargetForSelector: (SEL)aSelector

如果在上一步无法处理消息,则runtime会继续调用以下方法:
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制。我们首先要通过,指定方法签名,若返回nil,则表示不处理。
如下代码:

1
2
3
4
5
6
7
- (NSMethodSignature *)methodSignatureForSelector: (SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString: @"testInstanceMethod"]) {
return [NSMethodSignature signatureWithObjcTypes: "v@:"];
}

return [super methodSignatureForSelector];
}

若返回方法签名,则会进入下一步调用以下方法,对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在Invocation中,包括selector,目标(target)和参数。
我们可以在forwardInvocation方法中选择将消息转发给其它对象。我们可以通过Invocation做很多处理,比如修改实现方法,修改响应对象等。

1
2
3
4
5
- (void)forwardInvaocation: (NSInvocation)anInvocation {
[anInvocation invokeWithTarget: _helper];
[anInvocation setSelector: @selector(run)];
[anInvocation invokeWithTarget: self];
}

函数检索优化措施

通过SEL进行IMP匹配

先来看看类对象中保存的方法列表和方法的数据结构

1
2
3
4
5
6
7
8
9
10
11
typedef struct method_list_t {
uint32_t entsize_NEVER_USE;
uint32_t count;
struct method_t first;
} method_list_t;

typedef struct method_t {
SEL name;
const char *types; //参数类型和返回值类型
IMP imp;
} method_t;

在前面介绍SEL的时候,我们已经说过了苹果在通过SEL检索IMP时做的努力,这里不再累述。

cache缓存

cache的原则就是缓存那些可能要执行的函数地址,那么下次调用的时候,速度就可以快速很多。这个和CPU的各种缓存原理相同。说了这么多,再来认识几个名词:

1
2
3
4
5
6
7
8
9
10
11
struct objc_cache {
uintptr_tmask;
uintptr_toccupied;
cache_entry *buckets[1];
};

typedef struct {
SEL name;
void *unused;
IMP imp;
} cache_entry;

看这个结构,还是hash table。
objc_msgSend 首先在cache list中找SEL,没有找到就在class method中找,super class method中找(当然super class 也有cache list)。而cache的缓存机制则非常复杂了,由于Objective-C是动态语言。所以,这里面还有很多的多线程同步问题,而这些锁又是效率的大敌,相关的内容已经远远超过文本讨论的范围。
如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点。如果程序运行了足够长的时间,几乎每个消息都能在缓存中找到方法实现。程序运行时,缓存也讲随着新的消息的增加而增加。据牛人说(没有亲测过),苹果通过这些优化,在消息传递和直接的函数调用上的差距已经相当的小了。

方法调用中的隐藏参数

在进行面向对象编程的时候,在实例方法中都是用过self关键字,可是你有没有想过,为什么在一个实例方法中,通过self关键字就能调取到当前方法的对象呢?这就要归功于runtime system消息的隐藏参数了。
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

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

这些参数帮助方法实现获得了消息表达式的信息。它们被认为是“隐藏”的,是因为它们并没有在在定义方法的源码中声明,而是在代码编译时是插入方法的实现中的。尽管这些参数没有被显示声明,但在源码中仍然可以引用它们(就像可以引用消息接收者对象的实例变量一样)。在方法中可以通过self来引用消息接收者,通过标选_cmd来引用方法本身。下面的例子很好的说明了这个问题:

1
2
3
4
5
6
- (void)message {
self.name = @"James"; //通过self关键字给当前对象的属性赋值
SEL currentSel = _cmd; //通过_cmd关键字取到当前函数对应的SEL
NSLog(@"currentSel is: %s", (char *)currentSel);
}
打印结果:ObjcRuntime[693:403] currentSel is: message

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

方法交换Swizzling

使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
方式一:继承系统的类,重写方法。
方式二:使用runtime,交换方法。
在Objective-C中调用一个方法,其实是向一个对象发送消息,而查找消息的唯一依据是selector的名字。所以,我们可以利用Objective-C的runtime机制,实现在运行时交换selector对应的方法实现以达到我们的目的。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。我们先看看SEL与IMP之间的关系图:

从上图可以看出来,每一个SEL与一个IMP一一对应,正常情况下通过SEL可以查找到对应消息的IMP实现。但是,现在我们要做的就是把链接线解开,然后链接到我们自定义的函数的IMP上。当然,交换了两个SEL的IMP,还是可以再次交换回来了。交换后变成这样的,如下图

从图中可以看出,我们通过swizzling特性,将selectorC的方法实现IMPc与selectorN的方法实现IMPn交换了,当我们调用selectorC,也就是给对象发送selectorC消息时,所查找到的对应的方法实现就是IMPn而不是IMPc了。

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
#import "UIViewController+swizzling.h"
@implementation UIViewController(swizzling)
//load方法会在类第一次加载的时候被调用,调用的时间比较靠前,适合在这个方法里做方法交换
+ (void)load {
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获得viewController的生命周期方法的selector
SEL systemSel = @selector(viewWillAppear:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(swiz_viewWillAppear:);
//两个方法的Method
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, swizzMethod, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));

if (isAdd) {
//如果成功,说明类中不存在这个方法的实现
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzMethod, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
} else {
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}
});
}

- (void)swiz_viewWillApper: (BOOL)animated {
//这时候调用自己,看起来像死循环
//但是其实自己的实现已经被替换了
[self swiz_ViewWillAppear: animated];
NSLog(@"swizzle");
}

@end

在一个自己定义的viewController中重写viewWillAppear

1
2
3
4
- (void)viewWillAppear: (BOOL)animated {
[super viewWillAppear: animated];
NSLog(@"viewWillAppear");
}

设置关联值

使用场景:现在你准备一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。给一个类声明属性,其实本质就是给这个类添加关联,并不会直接把这个值的内存空间添加到类存储空间。分类只能添加方法。
设置关联值
这种情况的一般解决办法就是继承。但是只增加一个属性,就去继承一个类,总觉得太麻烦。这个时候,runtime的关联属性就发挥它的作用了。
添加关联对象

1
2
3
4
5
6
7
- (void)addAssociatedObject: (id)object {
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
//获取关联对象
- (id)getAssociatedObject {
return objc_getAssociatedObject(self, _cmd);
}

注意,这里面我们把getAssociatedObject方法的地址作为唯一的key,_cmd代表当前调用方法的地址。
参数说明:
object:与谁关联,通常是传self
key:唯一键,在获取值时通过该键获取,通常是使用static const void * 来声明
value:关联所设置的值
policy:内存管理策略,比如使用copy

1
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

获取关联值
参数说明:
object:与谁关联,通常是传self,在设置关联时所指定的与哪个对象关联的那个对象
key:唯一键,在设置关联时所指定的键

1
id objc_getAssociatedObject(id object, const void *key)

取消关联

1
void objc_removeAssociatedObjects(id object)

关联策略

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
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, //表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401, //表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403 //表示关联对象copy,不是线程安全的
};

@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//给系统NSObject类动态添加属性name
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"%@", objc.name);
}
@end

//定义关联的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

动态添加方法

使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类添加方法解决。

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
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init]; //默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
//动态添加方法就不会报错
[p performSelector: @selector(eat)];
}
@end

@implementation Person

void(*)()
//默认方法都有两个隐式参数
void eat(id self, SEL sel) {
NSLog(@"%@ %@", self, NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会吧对应的方法列表传过来
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法

+ (BOOL)resolveInstanceMethod: (SEL)sel {
if (sel == @selector(eat)) {
//动态添加eat方法
//第一个参数:给哪个类添加方法
//第二个参数:添加方法的方法编号
//第三个参数:添加方法的函数实现(函数地址)
//第四个参数:函数的类型(返回值+参数类型)v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}

return [super resolveInstanceMethod];
}
@end

字典转模型

设计模式

模型属性,通常需要跟字典中的key一一对应
问题:一个一个的生成模型属性,很慢?
需求:能不能自动根据一个字典,生成对应的属性。
解决:提供一个分类,专门根据字典生成对应的属性字符串

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
@implementation NSObject(Log)
//自动打印属性字符串
+ (void)resolveDict: (NSDictionary *)dict {
//拼接属性字符串代码
NSMutableString *strM = [NSMutableString string];
//1、遍历字典,把字典中的所有key取出来,生成对应的属性代码
[dict enumerateKeysAndObjectsUsingBlock:^(id_Nonnull key, id_Nonnull obj, BOOL_Nonnull stop){
//类型经常变,抽出来
NSString *type;
if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
type = @"NSString";
} else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]) {
type = @"NSArray";
} else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]) {
type = @"NSNumber";
} else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]) {
type = @"NSDictionary";
}

//属性字符串
NSString *str;
if ([type containsString: @"NS"]) {
str = [NSString stringWithFormat: @"@property(nonatomic, strong) %@ *%@;", type, key];
} else {
str = [NSString stringWithFormat: @"@property(nonatomic, assign) %@ %@;", type, key];
}
//每生成属性字符串,就自动换行。
[strM appendFormat: @"\n%@\n", str];

}];

//把拼接好的字符串打印出来
NSLog(@"%@", strM);
}
@end

字典转模型的方式一:KVC

1
2
3
4
5
6
7
@implementation Status
+ (instancetype)statusWithDict: (NSDictionary *)dict {
Status *status = [[self alloc] init];
[status setValuesForKeysWithDictionary: dict];
return status;
}
@end

KVC字典转模型的弊端:必须保证,模型中的属性和字典中的key一一对应。如果不一致,就会调用[setValue:forUndefinedKey:],报key找不到的错。
分析:模型中的属性和字典的ke不一一对应,系统就会调用[setValue:forUndefinedKey:]报错。
解决:重写对象的[setValue:forUndefinedKey:],把系统方法覆盖,就能继续使用KVC,字典转模型了。

1
- (void)setValue: (id)Value forUndefinedKey:(NSString *)key{}

字典转模型的方式二:Runtime
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
步骤:提供一个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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//解析plist文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
NSDictionary *statusDic = [NSDictionary dictionaryWithContentsOfFile:filePath];
//获取字典数组
NSArray *dictArr = statusDict[@"statuses"];
//自动生成模型的属性字符串
[NSObject resolveDict:dictArr[0][@"user"]];
_statuses = [NSMutableArray array];
//遍历字典数组
for(NSDictionary *dict in dictArr) {
Status *status = [Status modelWithDict: dict];
[_statuses addObject:status];
}
}
@end

@implementation NSObject(Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict {
// 思路:遍历模型中所有属性 ->使用运行时
// 0、创建对应的对象
id objc = [[self alloc] init];
// 1、利用runtime给对象中成员属性赋值
// class_copyIvarList: 获取类中的所有成员属性
// Ivar: 成员属性的意思
// 第一个参数:表示获取哪个类中的成员属性
// 第二个参数:表示这个类有多少成员属性,传入一个int变量地址,会自动给这个变量赋值
// 返回值Ivar *:指的是一个ivar数组,会把所有的成员属性放在一个数组中,通过返回的数组就能全部获取到。
/*
Ivar ivar;
Ivar ivar1;
Ivar ivar2;
//定义一个ivar的数组a
Ivar a[] = {ivar, ivar1, ivar2};
//用一个Ivar *指针指向数组的第一个元素
Ivar *ivarList = a;
//根据指针访问数组的第一个元素
ivarList[0];
*/

unsigned int count;
//获取类中的所有成员属性
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
//根据角标,从数组取出对应的成员属性
Ivar ivar = ivarList[i];
//获取成员属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
//处理成员属性名->字典中的key
//从第一个角标开始截取
NSString *key = [name substringFromIndex: 1];
//根据成员属性名去字典中查找对应的value
id value = dict[key];
//二级转换:如果字典中还有字典,也需要把对应的字典转成模型
//判断下value是否是字典
if ([value isKindOfClass: [NSDictionary class]]) {
//字典转模型
//获取模型的类对象,调用modelWithDict
//模型的类名已知,就是成员属性的类型
//获取成员属性类型
NSString *type = [NSString stringWithUTF8String: ivar_getTypeEncoding(ivar)];
//生成的是这种"@\"User\""类型 -> @"User" 在OC字符串中\" -> \是转义的意思,不占用字符串
//裁剪类型字符串
NSRanger ranger = [type rangeOfString:@"\""];
type = [type substringFromIndex:ranger.location + ranger.length];
range = [type rangeOfString:@"\""];
//裁剪到哪个角标,不包括当前角标
type = [type substringToIndex:range.location];
//根据字符串类名生成类对象
Class modelClass = NSClassFromString(type);
if (modelClass) {
//有对应的模型才需要转
//把字典转模型
value = [modelClass modelWithDict:value];
}
}
//三级转换:NSArray中也是字典,把数组中的字典转换成模型
//判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
//判断对应类有没有实现字典数组转模型数组的协议
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
//转换成id类型,就能调用任何对象的方法id
id Self = self;
//获取数组中字典对应的模型
NSString *type = [Self arrayContainModelClass][key];
//生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
//遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
//字典转模型
id model = [classModel modelWithDict: dict];
[arrM addObject: model];
}
//把模型数组赋值给value
value = arrM;
}
}

if (value) {
//有值,才需要给模型的属性赋值
//利用KVC给模型中的属性赋值
[objc setValue:value forKey:key];
}

}
return objc;
}

@end

参考文章

http://www.jianshu.com/p/e071206103a4
http://www.jianshu.com/p/adf0d566c887
http://www.jianshu.com/p/927c8384855a
http://chun.tips/2014/11/05/objc-runtime-1/#more
http://blog.sunnyxx.com/2016/08/13/reunderstanding-runtime-0/
http://blog.csdn.net/wzzvictory/article/details/8624057
http://www.cocoachina.com/ios/20151208/14595.html
http://www.jianshu.com/p/46dd81402f63

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 KNOWLEDGE IS POWER All Rights Reserved.

访客数 : | 访问量 :