Objective-C有大量的动态(dynamic)特点,这些功能和行为是在运行时而非编译或链接时表现出来的。这些特点是由Objective-C”运行时”系统实现的,理解Objective-C”运行时”的工作方式有助于你理解这门编程语言,以及你的程序是如何运行的。
1.动态特点(Dynamic Features):
Objective-C在”运行时”与其它编程语言在编译和链接期间一样,执行类型确定和方法解析,但是它也提供了让你执行额外的”运行时”操作的API,例如,对象自省(object introspection)、代码的动态创建和加载。
2.对象消息(Object Messaging)
如图,接收者(receiver,即使adder)是消息的target;而消息(addend1:25 addend2:10)是由一个selector(选择器)和一些对应的输入参数组成的。
消息接收者的实际类型和接收者上被激活的相应方法是在”运行时”确定的。
Objective-C”运行时”是通过动态类型(dynamic typing)和动态绑定(dynamic binding)来将消息(message)映射到方法(method)上的。
3.选择器(Selector)
在Objective-C对象消息中,selector是一个代表方法的字符串,可以被发送给对象或类。Objective-C”运行时”可以通过selector来获取正确的方法。selector是一个由” : “分开的几个segment(部分)和” : “组成的字符串,如:nameSegment1:nameSegment2:nameSegment3:
。这个selector由三个segment组成,每部分跟随一个” : “。当然,可以selector可以有空的segment,如addEnd1::
,对应的方法也要有空的方法标签名。
4.” SEL “类型
selector的类型是SEL
,它是一种特殊的数据类型,代表了一种独特的标识符。SEL
类型的变量可以通过@selector
关键字来创建。
SEL myMothod=@selector(myMethod);
5.方法签名(Method Signature)
方法签名定义了输入参数的数据类型
和返回值类型
(注意:没有方法名)。我们来讨论一下”运行时”系统如何实现对象消息的:编译器将[receiver message]
形式的对象消息转化为C函数(声明需要方法签名)调用。为了产生正确的代码,编译器需要同时知道selector和方法签名。现在selector很容易从对象消息表达式中抽取出来,但是编译器如何确定方法签名呢?毕竟,消息可能包含输入参数,但是你不知如何将这些类型映射到实际的方法中因为接收者以及对应的方法知道”运行时”才确定。
6.使用对象消息
Calculator.h文件代码:
#import <Foundation/Foundation.h>
@interface Calculator : NSObject
-(NSNumber*) sumAddend1:(NSNumber*)adder1 addend2:(NSNumber*)adder2;
-(NSNumber*) sumAddend1:(NSNumber*)adder1 :(NSNumber*)adder2;
@end
Calculator.m文件代码:
#import "Calculator.h"
@implementation Calculator
-(NSNumber *)sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2{
NSLog(@"Invoking method on %@ object with selector %@",[self className],NSStringFromSelector(_cmd));
return [NSNumber numberWithInteger:[adder1 integerValue]+[adder2 integerValue]];
}
-(NSNumber *)sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2{
NSLog(@"Invoking method on %@ object with selector %@",[self className],NSStringFromSelector(_cmd));
return [NSNumber numberWithInteger:[adder1 integerValue]+[adder2 integerValue]];
}
@end
main.m文件代码:
#import <Foundation/Foundation.h>
#import "Calculator.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Calculator* calc=[[Calculator alloc] init];
NSNumber* addend1=[NSNumber numberWithInteger:25];
NSNumber* addend2=[NSNumber numberWithInteger:10];
NSNumber* addend3=[NSNumber numberWithInteger:15];
NSLog(@"Sum of %@+%@=%@",addend1,addend2,[calc sumAddend1:addend1 addend2:addend2]);
NSLog(@"Sum of %@+%@=%@",addend1,addend3,[calc sumAddend1:addend1 :addend3]);
}
return 0;
}
运行结果:
讲解:
上述NSStringFromSelector(_cmd)
中,_cmd
参数的类型是SEL,代表被发送的消息(方法)的selector,可以在每个方法的实现部分获得。
修改下main.m代码来调用对象的动态方法,你将使用NSObject实例的performSelector: withObject: withObject:
方法,需要传入一个selector参数:
#import <Foundation/Foundation.h>
#import "Calculator.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Calculator* calc=[[Calculator alloc] init];
NSNumber* addend1=[NSNumber numberWithInteger:25];
NSNumber* addend2=[NSNumber numberWithInteger:10];
NSNumber* addend3=[NSNumber numberWithInteger:15];
SEL selector1=@selector(sumAddend1:addend2:);
id sum1=[calc performSelector:selector1 withObject:addend1 withObject:addend2];
NSLog(@"sum of %@ + %@ = %@",addend1,addend2,sum1);
SEL selector2=NSSelectorFromString(@"sumAddend1::");
id sum2=[calc performSelector:selector2 withObject:addend1 withObject:addend3];
NSLog(@"sum of %@ + %@ = %@",addend1,addend3,sum2);
}
return 0;
}
讲解:第一个selector是使用@Selector命令创建的,在编译时期;第二个selector是使用NSSelectorFromString创建的,在运行时。虽然,可以正确地运行,但是还是有些警告:
为什么会出现警告呢?因为如果没有方法搭配这个selector的话,方法就会抛出异常。可以通过添加pragma指令来移除这个警告。
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
SEL selector1=@selector(sumAddend1:addend2:);
id sum1=[calc performSelector:selector1 withObject:addend1 withObject:addend2];
NSLog(@"sum of %@ + %@ = %@",addend1,addend2,sum1);
SEL selector2=NSSelectorFromString(@"sumAddend1::");
id sum2=[calc performSelector:selector2 withObject:addend1 withObject:addend3];
NSLog(@"sum of %@ + %@ = %@",addend1,addend3,sum2);
#pragma clang diagnostic pop
其中,pragma指令的clang diagnostic ignored
用来禁止特定的编译器警告,语法是:#pragma clang diagnostic ignored "DiagnosticName"
7.动态类型(Dynamic Typing)
动态类型能让”运行时”在运行时确定对象的类型,这对下面这种情况有用:需要把对象分配给一个变量,但是不能提前知道对象的类型。
Objective-C同时支持静态和动态类型,当一个变量被静态地指定类型,那么变量的类型是明确,如:Atom *myAtom;
,使用静态类型,编译可以在编译期间进行类型检查。而使用动态类型,类型检查是在运行时进行的。Objective-C通过id数据类型提供了对动态类型的支持,如:id myAtom;
,此时,myAtom可能指向任何类型的对象,而真正的类型是在运行时确定的。
利用动态类型很容易创建一个方法来处理任何类的实例,例如:
-(NSInteger) computeValue:(id)parameter;
当然,你可以提供不同等级的类型信息给一个方法声明,例如,下面的参数要遵守NSDecimalNumberBehaviors
协议:
-(NSInteger) computeValue:(id<NSDecimalNumberBehaviors<NSDecimalNumberBehaviors>)parameter;
8.动态绑定(Dynamic Binding)
动态绑定是在运行时将消息映射到方法上的过程,而不是在编译时。 实际上,消息和接收消息的对象直到程序运行、消息被发送才会被设置。因为相同的方法可能被许多对象实现,所以被激活的方法会动态变化。动态绑定使Objective-C拥有了多态,允许新的对象被添加到系统中而不影响现有的代码,从而减少对象之间的耦合。看下面代码:
id atom=[[Hydrogen alloc] initWithNeutrons:1];
[atom logInfo];
代码执行时,运行时确定变量atom的真实类型,然后使用消息的selector来将消息映射到对应的由对象atom实现的实例方法上。在这个例子中,变量atom的类型确定是Hydrogen*,所以运行时会搜寻Hydrogen的实例方法logInfo,如果没发现的话,它就会搜寻它父类的实例方法。
9.动态方法解析(Dynamic Method Resolution)
动态方法解析能让你动态地提供一个方法的实现。Objective-C 包含指令@dynamic,告诉编译器,与一个属性关联的方法会被动态提供。NSObject类包含方法resolveInstanceMethod:
和resolveClassMethod:
来动态地为一个[给定的实例和类方法的]selector提供一个实现。你可以重写这些方法来动态地实现实例和类方法。
10.动态地提供一个方法实现
先看下代码:
Calculator.h的代码与上面的一样,Calculator.m改为下面代码:
#import "Calculator.h"
//这将添加运行时系统API到你的代码中
#import <objc/runtime.h>
@implementation Calculator
-(NSNumber *)sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2{
NSLog(@"Invoking method on %@ object with selector %@",[self className],NSStringFromSelector(_cmd));
return [NSNumber numberWithInteger:[adder1 integerValue]+[adder2 integerValue]];
}
-(NSNumber *)sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2{
NSLog(@"Invoking method on %@ object with selector %@",[self className],NSStringFromSelector(_cmd));
return [NSNumber numberWithInteger:[adder1 integerValue]+[adder2 integerValue]];
}
//重写NSObject类的方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString* method=NSStringFromSelector(sel);
if ([method hasPrefix:@"absoluteValue"]) {
//运行时API:class_addMethod(C函数)
class_addMethod([self class], sel, (IMP)absoluteValue, "@@:@");
NSLog(@"Dynamically added instance method %@ to class %@",method,[self className]);
return YES;
}
return [super resolveInstanceMethod:sel];
}
//C函数:至少提供两个参数self和_cmd;另外的是该函数的真正参数
id absoluteValue(id self,SEL _cmd,id value){
NSInteger intVal=[value integerValue];
if (intVal<0) {
return [NSNumber numberWithInteger:(intVal * -1)];
}
return value;
}
@end
讲解:
首先导入运行时库:#import <objc/runtime.h>
,这样的话,运行时系统API才会加到你的代码中。重写类方法:+(BOOL)resolveInstanceMethod:(SEL)sel
,在该方法中,运行时API函数BOOL class_addMethod(Class cls, SEL name, IMP imp,
用来动态添加一个函数作为实例方法到一个类中。参数分别代表:方法所要添加到的类,新方法的selector,函数的地址,方法参数的类型字符数组。
const char *types)
如上面代码,作为实例方法被添加的函数叫做:absoluteValue(id self,SEL _cmd,id value)
,它的前两个参数必须是id self,SEL _cmd
。
下面,我们来看一下主函数:
#import <Foundation/Foundation.h>
#import "Calculator.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Calculator* calc=[[Calculator alloc] init];
NSNumber* addend1=[NSNumber numberWithInteger:25];
NSNumber* addend2=[NSNumber numberWithInteger:10];
NSNumber* addend3=[NSNumber numberWithInteger:15];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
SEL selector1=@selector(sumAddend1:addend2:);
id sum1=[calc performSelector:selector1 withObject:addend1 withObject:addend2];
NSLog(@"sum of %@ + %@ = %@",addend1,addend2,sum1);
SEL selector2=NSSelectorFromString(@"sumAddend1::");
id sum2=[calc performSelector:selector2 withObject:addend1 withObject:addend3];
NSLog(@"sum of %@ + %@ = %@",addend1,addend3,sum2);
SEL selector3=NSSelectorFromString(@"absoluteValue");
NSLog(@"Invoking instance method %@ on object of class %@",NSStringFromSelector(selector3),[calc className]);
id sum3=[calc performSelector:selector3 withObject:sum2 ];
NSLog(@"Absolute value of %@=%@",sum2,sum3);
#pragma clang diagnostic pop
}
return 0;
}
11.动态加载(Dynamic Loading)
动态加载能够让一个Objective-C程序在它需要的时候才加载代码和资源,而不必在应用一启动的时候加载全部的程序组件。Apple提供了bundle mechanism(捆绑机制)来支持动态加载。
bundle是一个软件的delivery机制,它包含了一个拥有标准层次结构的目录,里面是代码和资源。bundle类型有如下几种:
- Application bundle:管理与进程(例如一个程序)关联的代码和资源
- Framework bundle:管理一个动态分享库和它相关联的资源,例如头文件
- Loadable bundle:也指的是插件,应用可以用来动态加载代码
Foundation框架的NSBundle类可以用来管理bundle。一个NSBundle对象代表文件系统中的位置。
NSBundle* bundle=[NSBundle mainBundle];
NSString* bundlePath=[bundle pathForResource:@"info" ofType:@"plist"];
下面的代码使用NSBundle对象动态地加载一个框架bundle,让后从框架中创建一个类的实例。
NSBundle* testBundle=[NSBundle bundleWithPath:@"/Test.bundle"];
id tester=[[[testBundle classNamed:@"Tester"] alloc] init];
12.自省(Introspection)
Foundation框架的NSObject的API提供了大量的方法来执行对象自省。这些方法动态地查找运行时来寻求下面类型的信息:
- 关于方法的信息
- 测试对象的继承、行为和一致性
对象自省能帮助你避免运行时错误,例如:消息调度错误等。
- isKindOfClass:用来测试消息接收者是否是参数所指定类的对象或者子类对象。
BOOL isCalculator=[myObject isKindOfClass:[Calculator calss]];
- respondsToSelector:测试对象是否对selector有所响应,即对象是否拥有selector所对应的方法。
BOOL responds=[myObject respondsToSelector:@selector(logInfo:)];
- conformsToProtocol:测试对象是否遵守指定的协议
BOOL conforms=[myObject conformsToProtocol:@protocol(MyProtocol)];
- methodSignatureForSelector:包含了selector对应的方法签名
NSMethodSignature* SIGNATURE=[calc methodSignatureForSelector:selector1];