该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
第九章 专家级技巧:使用运行时系统API
9.2 使用运行时系统API
接下来仍然是编写一段程序:
该程序会使用运行时系统API以动态的方式创建一个类和一个类实例;
然后以动态方式向该实例添加一个变量;
创建类RunningWidget用于说明;
在类中添加相应代码;(部分内容可参考第7章)
(Code_RunningWidget.m)
#import "C9RunningWidget.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation C9RunningWidget
//用于显示选择器的方法实现函数
static void display(id self,SEL _cmd){
NSLog(@"Invoking method with selector %@ on %@ instance",NSStringFromSelector(_cmd),NSStringFromClass([self class]));
}
-(void)test9_2{
//创建一个类对
Class widgetClass = objc_allocateClassPair([NSObject class], "C9Widget", 0);
//向这个类添加一个方法
const char * types = "v@:";
class_addMethod(widgetClass, @selector(display), (IMP)display, types);
//向类添加一个实例变量
const char * height = "height";
class_addIvar(widgetClass, height, sizeof(id), rint(log2(sizeof(id))), @encode(id));
//注册这个类
objc_registerClassPair(widgetClass);
//创建一个widget实例并设置实例变量的值
id widget = [[widgetClass alloc] init];
id value = [NSNumber numberWithInteger:15];
[widget setValue:value forKey:[NSString stringWithUTF8String:height]];
NSLog(@"W height = %@",[widget valueForKey:[NSString stringWithUTF8String:height]]);
//向widget实例发送一个消息
objc_msgSend(widget, NSSelectorFromString(@"display"));
//Build Setting--> Apple LLVM 6.0 - Preprocessing--> Enable Strict Checking of objc_msgSend Calls 改为 NO
//以动态方式向widget实例添加一个变量(关联对象)
NSNumber * width = [NSNumber numberWithInteger:10];
objc_setAssociatedObject(widget, @"width", width, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//获取该变量的值并显示它
id result = objc_getAssociatedObject(widget, @"width");
NSLog(@"W width = %@",result);
}
@end
C9RunningWidget * test = [[C9RunningWidget alloc] init];
[test test9_2];
log:
2017-12-18 17:07:40.425307+0800 精通Objective-C[53341:5496447] W height = 15
2017-12-18 17:07:40.425491+0800 精通Objective-C[53341:5496447] Invoking method with selector display on C9Widget instance
2017-12-18 17:07:40.425634+0800 精通Objective-C[53341:5496447] W width = 10
从功能上讲,这段代码执行了如下操作:
1)定义了一个方法的实现参数;
2)创建并注册了一个类;
3)创建了一个类实例;
4)以动态方式向该实例添加一个变量;
我来逐个分析下;
9.2.1 定义方法的实现函数
OC的方法就是一个至少接收两个陈述(self和_cmd)的C语言参数;(第8章)
9.2.2 创建并注册类
要使用运行时系统API以动态的方式创建类,必须执行下列步骤:
1)新建一个类及其元类;
2)向这个类添加方法和实例变量(如果有的话);
3)注册新建的类;
运行时系统桉树class_addMethod参数:
应该添加方法的类;
设置被添加方法名称的选择器;
实现该方法的函数;
类型编码(描述方法的参数类型和返回值类型的字符串);
类型编码:
字符v代表void;
字符@代表SEL类型(https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100);
这里附上一张截图Objective-C type encodings:
(Pic9_1)
class_addMethod函数的类型参数中的编码必须以固定顺序排列:
第一个编码用于设置返回值类型;
第二个编码用于设置方法的隐式参数self(类型为id);
第三个编码用于设置方法的隐式参数_cmd(类型为SEL);
其他编码用于设置方法的显式参数的类型;
对应上边的这个表,就好理解各个编码的含义了;
9.2.3 创建类实例
创建一个widget实例并设置实例变量的值,然后调用了实例中的方法;
9.2.4 以动态的方式向类实例添加变量
OC没有提供向对象添加实例变量的功能;
然而,使用运行时系统的关联对象特性,可以高效地模拟这个功能;
关联对象:
指通过关键字引用的 附加到类实例中的对象;
我们可以在,不允许使用是实例变量的OC分类中使用关联对象;
在创建关联对象时,需要设置关联对象的关键字、关联对象的内存管理策略和它的值;
NSNumber * width = [NSNumber numberWithInteger:10];
objc_setAssociatedObject(widget, @"width", width, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
id result = objc_getAssociatedObject(widget, @"width");
运行时系统API含有一个枚举型值,其中含有内存管理策略的所有可能值;
OBJC_ASSOCIATION_RETAIN_NONATOMIC:向关联对象分配非原子性的强引用;(和nonatomic和strong特性的属性类似)
本示例展示了使用运行时系统API以动态的方式创建类、类实例和关联对象的方式;
参阅Apple objective-c runtime programming guide,可以学习更多;
9.3 创建动态代理
本节将实现本章的最后一个示例程序:
如何使用Foundation框架中的NSInvocation和NSProxy类,实现动态消息转发功能;
OC提供了多个消息转发选项(第三章):
使用NSObject类的forwardingTargetForSelector:方法实现的快速转发;
以及使用NSObject类的forwardInvocation:方法实现标准(完全)转发;
标准转发的一大优点是使程序能够对对象的消息、参数和返回值执行额外的处理操作;
将该功能与NSProxy类一起使用,可以在OC中获得不错的面向切面编程(AOP)机制;
AOP是一种编程范式,其目的是通过横切(cross-cutting)将(依赖或影响程序的其他部分)功能与程序中的其他组成部分分隔开;提高程序的模块化程度;
NSProxy是Foundation框架中的一个类,专门用于创建代理,其作用就像真正类的接口;
下面我们通过创建一个NSProxy类的子类并实现forwardInvocation:方法:
从而使消息能够发送到应用了横切功能的真正类;
程序的结构可以是这样的:
(Pic9_2)
接下来分步理解下,先从Invoker协议开始;
9.3.1 创建Invoker协议
我们新建了一个文件夹9.3AspectProxy,用于存放这里的类;
首先创建一个协议和一个遵守该协议的类;
选择OC的协议模板,命名为Invoker;添加必要的代码;
(Code_Invoker.h)
#import <Foundation/Foundation.h>
@protocol Invoker <NSObject>
@required
-(void)preInvoke:(NSInvocation *)inv withTarget:(id)target;
@optional
-(void)postInvoke:(NSInvocation *)inv withTarget:(id)target;
@end
临时对代理/协议有了点理解;
在一个类M中导入一个协议P,然后在这个类的.m中实现该协议方法;
M.h中的接口就被协议P扩展了,但是用来扩展接口的协议P只是提供了接口,其实现则需要由被扩展了的M在其.m中进行实现;
这样通过这种接口扩展的方式,我们可以给不同继承体系的类提供统一的行为;
我们来看一下这个协议中定义的两个接口;
-(void)preInvoke:(NSInvocation *)inv withTarget:(id)target;
这是一个必要方法,所有遵循该协议的类都必须实现这个方法;
能够在调用对象中的方法前执行对功能的横切;
-(void)postInvoke:(NSInvovation *)inv withTarget:(id)target;
是一个可选方法;
能够在调用对象中的方法后执行对功能的横切;
接着我们编写一个遵循该协议的类AuditingInvoker,实现横切功能;
(Code_AuditingInvoker.h,AuditingInvoker.m)
#import <Foundation/Foundation.h>
#import "Invoker.h"
@interface AuditingInvoker : NSObject<Invoker>
@end
#import "AuditingInvoker.h"
@implementation AuditingInvoker
-(void)preInvoke:(NSInvocation *)inv withTarget:(id)target{
NSLog(@"Creating audit log before sending message with selector %@ to %@ object",NSStringFromSelector([inv selector]),NSStringFromClass([target class]));
}
-(void)postInvoke:(id)inv withTarget:(id)target{
NSLog(@"Creating audit log after sending message with selector %@ to %@ object",NSStringFromSelector([inv selector]),NSStringFromClass([target class]));
}
@end
实现了横切功能之后,我们再来处理下NSproxy子类;
9.3.2 编写代理类
下面创建的代理类,它是NSProxy的子类;
它会实现消息转发方法forwardInvocation:和methodSignatureForSelector:;
我们来新建这个类AspectProxy;继承自NSProxy;
(Code_AspectProxy.h,AspectProxy.m)
#import <Foundation/Foundation.h>
#import "Invoker.h"
@interface AspectProxy : NSProxy
@property (nonatomic , strong) id proxyTarget;//该属性是通过NSProxy实例转发消息的真正对象;
@property (nonatomic , strong) id<Invoker> invoker;//该属性是一个能够实现横切功能的类(遵循Invoker协议)的实例;
@property (nonatomic , strong , readonly) NSMutableArray * selectors;//该属性是一个选择器集合,定义了哪些消息会调用横切功能;
-(id)initWithObject:(id)object andInvoker:(id<Invoker>)invoker;
-(id)initWithObject:(id)object selectors:(NSArray *)selectors andInvoker:(id<Invoker>)invoker;
-(void)registerSelector:(SEL)selector;//会向集合中添加一个选择器
@end
#import "AspectProxy.h"
@implementation AspectProxy
-(id)initWithObject:(id)object andInvoker:(id<Invoker>)invoker{
return [self initWithObject:object selectors:@[] andInvoker:invoker];
}
-(id)initWithObject:(id)object selectors:(NSArray *)selectors andInvoker:(id<Invoker>)invoker{
_proxyTarget = object;
_invoker = invoker;
_selectors = [selectors mutableCopy];
return self;
}
-(void)registerSelector:(SEL)selector{
NSValue * selValue = [NSValue valueWithPointer:selector];
[self.selectors addObject:selValue];
}
//该方法会为目标对象中被调用的方法返回一个NSMethodSignature实例
//运行时系统要求在执行标准转发时实现这个方法
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.proxyTarget methodSignatureForSelector:sel];
}
-(void)forwardInvocation:(NSInvocation *)invocation{
//调用目标方法前的横切功能;
if ([self.invoker respondsToSelector:@selector(preInvoke:withTarget:)]) {
if (self.selectors != nil) {
SEL methodSel = [invocation selector];
for (NSValue * selValue in self.selectors) {
if (methodSel == [selValue pointerValue]) {
[[self invoker] preInvoke:invocation withTarget:self.proxyTarget];
break;
}
}
}else{
// [[self invoker] preInvoke:invocation withTarget:self.proxyTarget];
}
}
//调用目标方法
[invocation invokeWithTarget:self.proxyTarget];//invocation 文件提取
//在调用目标方法之后执行横切操作;
if ([self.invoker respondsToSelector:@selector(preInvoke:withTarget:)]) {
if (self.selectors != nil) {
SEL methodSel = [invocation selector];
for (NSValue * selValue in self.selectors) {
if (methodSel == [selValue pointerValue]) {
[[self invoker] postInvoke:invocation withTarget:self.proxyTarget];
break;
}
}
}else{
// [[self invoker] postInvoke:invocation withTarget:self.proxyTarget];
}
}
}
@end
来看一下这段代码:
初始化方法初始化了相应的AspectProxy对象的实例;
注意,因为NSProxy是一个基类,所以这些方法的开头没有调用[super init];
当调用目标方法的选择器与在AspectProxy对象中注册的选择器匹配时,forwardInvocation:方法会调用目标对象中的方法,并根据条件语句的判断结果调用AOP功能;
现在再回看之前的Pic9_2,是不是就清晰了;
最终向AspectProxy代理对象发送原对象可接收的消息,而不是直接向原对象发送消息;
我们来测试下;
9.3.3 测试AspectProsy程序
我们是用第七章的C7Calculator类(还记得动态方法决议不?)来进行测试;
该类声明了下列方法:
-(NSNumber *)sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2;
-(NSNumber *)sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2;//这个方法的第二个参数使用空参数
//创建对象
id calculator = [[C7Calculator alloc] init];
NSNumber * addend1 = [NSNumber numberWithInteger:-1];
NSNumber * addend2 = [NSNumber numberWithInteger:2];
NSNumber * addend3 = [NSNumber numberWithInteger:5];
//为该对象创建代理
NSValue * selValue1 = [NSValue valueWithPointer:@selector(sumAddend1:addend2:)];
NSArray * selValues = @[selValue1];
AuditingInvoker * invoker = [[AuditingInvoker alloc] init];
id calculatorProxy = [[AspectProxy alloc] initWithObject:calculator selectors:selValues andInvoker:invoker];
//使用指定的选择器向该代理发送消息
[calculatorProxy sumAddend1:addend1 addend2:addend2];
//使用没有特殊处理的其他选择器向该代理发送消息
[calculatorProxy sumAddend1:addend2 :addend3];
//为这个代理注册另一个选择器并再次向其发送消息
[calculatorProxy registerSelector:@selector(sumAddend1::)];
[calculatorProxy sumAddend1:addend2 :addend3];
log:
2017-12-21 11:42:53.211276+0800 精通Objective-C[83628:6659416] Creating audit log before sending message with selector sumAddend1:addend2: to C7Calculator object
2017-12-21 11:42:53.211419+0800 精通Objective-C[83628:6659416] Invoking method on C7Calculator object with selector sumAddend1:addend2:
2017-12-21 11:42:53.257332+0800 精通Objective-C[83628:6659416] Creating audit log after sending message with selector sumAddend1:addend2: to C7Calculator object
2017-12-21 11:42:53.257634+0800 精通Objective-C[83628:6659416] Invoking method on C7Calculator object with selector sumAddend1::
2017-12-21 11:42:53.257853+0800 精通Objective-C[83628:6659416] Creating audit log before sending message with selector sumAddend1:: to C7Calculator object
2017-12-21 11:42:53.258002+0800 精通Objective-C[83628:6659416] Invoking method on C7Calculator object with selector sumAddend1::
2017-12-21 11:42:53.258146+0800 精通Objective-C[83628:6659416] Creating audit log after sending message with selector sumAddend1:: to C7Calculator object
分析:
调用C7Calculator对象的代理中的sumAddend1:addend2:方法时;
会调用AuditingInvoker对象中的preInvoker:,postInvoker:方法;
还会调用真正目标(C7Calculator对象)中的sumAddend1:addend2:方法;
方法通过AspectProxy代理进行注册,才会调用AuditingInvoker对象中的AOP方法;
不注册,就不会调用;
9.4 小结
本章介绍了3个程序,我们使用OC运行时及其API完成了这些程序;
还有创建可选包和使用Xcode将代码导入工程的方法;
好好复习一下第二部分的内容,接下来将开始第三部分Foundation框架的介绍。