该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
第7章 运行时系统
终于到了运行时这一章,让我们来一步一步揭开它神秘的面纱吧;
OC拥有相当多的动态特性,这些特性在运行程序时发挥作用,而不是在编译或链接代码时发挥作用;
OC运行时系统实现了这些特性;这些功能也为OC语言提供了非常强大的功能和灵活性;
开发人员使用它们能够以实时方式促进程序的开发和更新,而无需重新编译和部署;对软件影响也比较低;
本章内容:
了解OC运行时系统的工作方式;
介绍OC语言的动态功能以及在程序中它们的使用方式;
7.1 动态功能
在运行时,OC语言会执行其他语言在程序编译或链接时会执行的许多常规操作,如确定类型和方法解析;
这些操作还可以提供API,使你编写的程序能够执行额外的运行时操作;
如对象内省和以动态方式创建和加载代码;
OC运行时系统的结构和实现方式生成了这些动态特性;
接下来介绍这些特性以及在开发程序时使用它们的方式;
7.2 对象消息
OOP术语;
消息传递:指一种在对象之间发送和接收消息的通信方式;
OC中,消息传递(如对象消息传递)用于调用类和实例(对象)的方法
语法举例如下:
接收器 消息
[加法器 加数1:25 加数2:25]
||
选择器
加数1:加数2
分析:
以加法器作为接收器,接收消息(即向加法器发送消息);
在上边的这个消息传递表达式中,接收器(加法器)是消息的目的地(即对象或类);
消息本身(加数1:25 加数2:25)则是由选择器和相应的输入参数构成;
对象消息传递是以动态方式实现的特性;
接收器的类型 和 相应的调用方法是在运行时决定的;
示例如下:
消息
[接收器的performComputation方法]
|
——>OC运行时系统:
1)决定消息接收器的类型(决定的是动态类型,静态类型在声明时就已经决定)
2)决定实现的方法(动态绑定:关键的是选择器 和 方法签名)
3)调用方法;(调用接收器的方法-performComputation)
上述过程描述了OC运行时系统通过动态类型和动态绑定,将消息与方法对应起来的方式;
使用对象消息传递的动态编程特性可以获得极大的灵活性;
可以简化接口,还可以在执行程序时,开发可修改或更新的模块化应用;
因为OC程序是在运行时处理方法调用的,所以会存在与动态绑定有关的额外开销;
OC运行时系统通过缓存的方法调用,节省用于向方法发送消息所消耗的内存,从而减少额外的开销;
运行时系统处理方法调用的另一种方式是:
接收器不保证一定会对消息做出回应;
如果接收器没有消息做出回应,他就会抛出一个运行时异常;
OC提供了多个用于处理这种情况的特性(如对象内省和消息转发);
OC对象消息传递中具有下列关键元素:
1)消息:向对象/类发送的名称(选择器)和一系列参数;
2)方法:OC中的类或实例方法,其声明中含有名称、输入参数、返回值和方法签名(即输入参数和返回值的数据类型);
3)方法绑定:接收向指定接收器发送的消息并寻找和执行适当方法的处理过程;
OC运行时在调用方法时,会以动态绑定方式处理消息;
接下来介绍选择器和方法签名(method signature);
7.2.1 选择器
选择器:在OC的对象消息传递中,选择器是一种字符串,用于指明调用对象或类中的哪个(些)方法;
选择器是一种分为多个段的文本字符串,每个段以冒号结尾并且后跟参数;
例如:分段1:分段2:分段3:
这个选择器有3个分段,每个分段都带有一个冒号,因而表明相应的消息有三个输入参数;
消息的参数与选择器的分段一一对应;
OC运行时系统使用选择器,为目标对象/类提供正确的方法实现代码;
在OC源代码中,消息的选择器直接与一个或多个类/实例方法声明对应;
我们举一个例子:
类A的实例a,定义接口-(int)addNum1:(NSInteger)n1 num2:(NSInteger)n2;
A类实例方法的选择器为addNum1:num2:
调用该实例方法需要使用接收器对象a后跟带输入参数的选择器:[a addNum1:1 num2:2];
这个例子展示了选择器在对象消息传递中的作用;
这些事物的运作方式为:
当源代码被编译时,编译器(运行时系统的组成部分)会创建数据结构和函数调用语句,使用它们以动态方式将接收器(类/对象)和消息选择器与方法的实现代码对应起来;
在执行程序时,运行时库(运行时系统的另一个组成部分)利用这些信息找到并调用适当的方法;
稍后会介绍运行时系统的各个组成部分;
1.空选择器分段:
举个例子,有这样一个选择器 sumAddend1::
在这个选择器中,第一个分段具有一个文本字符串,而第二个分段没有文本字符串;
实际上,拥有一个以上分段的选择器都可以拥有 空选择器分段(即没有文本字符串的分段);
空参数(空选择器分段)并不常见,因为出错时会难以对错误进行定位;
2.SEL类型:
我们已经知道在消息中使用文本字符串定义选择器的方式;接下来看看选择器的类型;
选择器类型(SEL):是一种特殊的OC数据类型,是用于在编译源代码时替换选择器值的唯一标识符;
所有具有相同选择器值的方法都拥有相同的SEL标识符;
OC运行时系统会确保每一个选择器标识符的唯一性;
可以使用关键字@selector创建SEL类型的变量
如下示例:
-(NSNumber *)testSELTypeMethodAddNum1:(NSNumber *)a Num2:(NSNumber *)b{
NSNumber * c = [NSNumber numberWithFloat: [a floatValue] + [b floatValue]];
NSLog(@"a + b = %@",c);
return c;
}
SEL myMethod = @selector(testSELTypeMethodAddNum1:Num2:);
之所以使用SEL类型是因为:
OC运行时系统(在NSObject中)含有许多将SEL型变量用作参数的动态方法;
除了获取对象和类的信息,NSObject类还有很多方法,它们用于使用选择器参数调用对象的方法;
如下示例:
[self performSelector:@selector(testSELTypeMethodAddNum1:Num2:) withObject:@1 withObject:@2];
log:
2017-11-28 10:33:51.926567+0800 精通Objective-C[641:12164711] a + b = 3
这个NSObject的实例方法performSelector:withObject:withObject: ,调用了选择器变量指定的方法;
其中的@selector指令会在编译时创建一个选择器变量;
也可以使用Foundation框架中的NSSelectorFromString函数在运行时创建选择器;
因此上述示例你也可以这样写:
SEL addMethod = NSSelectorFromString(@"testSELTypeMethodAddNum1:Num2:");
[self performSelector:addMethod withObject:@3 withObject:@2];
log:
2017-11-28 10:37:08.824019+0800 精通Objective-C[717:12171435] a + b = 5
7.2.2 方法签名
前面我们已经介绍了消息接收器、选择器和SEL类型,接下来介绍方法特征及其在对象消息传递中的作用;
方法签名(method signature):
定义了方法输入参数的数据类型和方法返回值(如果存在);
在了解方法签名的作用之前,我们先来研究下运行时系统实现对象消息传递的方式;
(第八章将详细介绍运行时系统和实现动态行为(如对象消息传递)的方式;本章着重介绍运行时系统的体系结构,而不是细节)
编译器会将[接收器 消息]形式的对象消息,转换为声明中含有方法签名的(ANSI)C函数调用语句;
因此,为了生成正确的对象消息传递代码,编译器需要获得选择器值和方法签名;
编译器可以从对象消息表达式中轻松提取选择器;获取方法签名的过程则要麻烦些;
获取方法签名:
由于接收器(和相应的方法)是在程序运行时确定的,所以编译器无法知道使用怎样的数据类型才能与要调用的方法对应起来;
为了获取方法签名,编译器会根据已解析的方法声明进行猜测;
如果找不到方法签名,或者从声明获得的方法签名和运行时实际执行的方法匹配不上,就会出现方法签名不匹配的情况,从而导致从编译器警告到运行时错误的各种问题;
7.2.3 使用对象消息
进行一个选择器与SEL类型的练习;
我们定义类C7Calculator可以计算整形加法;
方法实现中使用了NSStringFromSelector(_cmd)获取被调用方法的选择器文本字符串;
NSStringFromSelector函数需要一个SEL类型的变量;_cmd是一个隐式参数(即无需声明就存在于所有OC方法中的参数),它含有被发送信息中的选择器;
#import <Foundation/Foundation.h>
@interface C7Calculator : NSObject
-(NSNumber *)sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2;
-(NSNumber *)sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2;//这个方法的第二个参数使用空参数
@end
#import "C7Calculator.h"
@implementation C7Calculator
-(NSNumber *)sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2{
NSLog(@"Invoking method on %@ object with selector %@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
return [NSNumber numberWithInteger:([adder1 integerValue]+[adder2 integerValue])];
}
-(NSNumber *)sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2{//这个方法的第二个参数使用空参数
NSLog(@"Invoking method on %@ object with selector %@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
return [NSNumber numberWithInteger:([adder1 integerValue]+[adder2 integerValue])];
}
@end
编码测试下:
C7Calculator * cal = [[C7Calculator alloc]init];
[cal sumAddend1:@2 addend2:@3];
log:
2017-11-28 15:09:57.628257+0800 精通Objective-C[4697:12909535] Invoking method on C7Calculator object with selector sumAddend1:addend2:
接下来编写调用对象中动态方法的代码;使用NSObject的performSelector:withObject:withObject:方法,该方法需要制定选择器和参数;
[cal performSelector:@selector(sumAddend1:addend2:) withObject:@2 withObject:@5];
[cal performSelector:NSSelectorFromString(@"sumAddend1::") withObject:@1 withObject:@3];
@selector指令创建的是在编译时;-静态的方式
NSSelectorFromString函数创建的则是在运行时;-动态的方式
看起来没什么问题 但是编译器会给出一个警告:PerformSelector may cause a leak because its selector is unknown
出现这个警告的原因是,如果找不到与该选择器匹配的方法,那么方法就会抛出异常,因而可能导致内存泄漏;
可以通过添加pragma指令去除这个警告;
如下述代码:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[cal performSelector:NSSelectorFromString(@"sumAddend1::") withObject:@1 withObject:@3];
#pragma clang diagnostic pop
使用pragma指令 clang diagnostic ignored "功能诊断的名字" 可以禁止指定的编译器警告功能;
使用#pragma clang diagnostic push和#pragma clang diagnostic pop,可以保存和恢复编译器当前的诊断设置;
这可以确保编译器在编译源文件的其余部分时,继续执行正常的编译器选项;
辅助理解:
运行时包括较多的组成部分,编译器和运行时库都是其中的一部分;一般所说的静态处理是指编译器可判定的,而动态生成/调用则完全是由运行时其他部分决定的;
OC提供了很多可以在运行时动态调用的方法;
有@selector创建的SEL类型是静态创建的,但是使用的时候确是在运行时动态调用的;
接下来再看一下运行时系统的其他特性;
7.3 动态类型
运行时系统通过动态类型(dynamic type)功能,可以在运行程序时决定对象的类型,因而可以使运行时因素能够在程序中指定使用那些对象;
在事先无法知道变量分配哪种类型的对象的情况下,就特别有用;
OC既支持静态类型 也支持动态类型:
使用静态类型设置变量的类型时,变量的类型就由它的声明决定;
动态类型与静态类型:
对于静态类型,编译器能在编译时检查类型,执行程序前可以检查出类型错误;
对于动态类型,类型检查操作是在运行时执行的;
OC通过id数据类型支持动态类型;(id是OC独有的一种可以存储任何数据类型的OC对象的数据类型)
动态类型变量的类型放到运行时确定,从而决定对象之间的关系,而不会强制它们使用静态编码;
这样可以使编写一个可以处理任何类实例的方法变得容易的多,而无需在应用程序中为处理不同类编写多个不同种类的方法;
使用动态类型可以简化接口;
使用动态类型还可以提供很大的灵活性,可以在程序的执行过程中改进程序使用的数据类型,并在不重新编译和重新部署的情况下引入新的数据类型;
在方法中还可以使用不同等级的类型信息,比如使用遵循某协议的对象作为参数,(id<xxxprotocal>);
如:
-(void)testMethod:(id<protocal>)para;
-(void)testMethod:(NSNumber<protocal> *)para;
OC还为运行时对象的内省提供了API(如检查动态设置类型的匿名对象属于哪个类),解决了部分静态类型检查问题;
内省使运行时系统能够检查对象的类型,因而能够查明对象是否适用于特定的操作;
我们休息一下,稍后进行后续介绍;