iOS编程基础-OC(九)-专家级技巧:使用运行时系统API(续)

来源:互联网 发布:js全局数组变量 编辑:程序博客网 时间:2024/06/03 20:41

该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!


     第九章 专家级技巧:使用运行时系统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框架的介绍。

     

   


阅读全文
0 0