IOS设计架构模式:代理模式和消息转发机制

       本篇我将带大家了解3个知识点:一是代理模式,代理模式有什么用,应用场景是什么;二是消息转发机制,给对象发送消息,假如对象没有这个方法,在崩溃之前会走哪些方法,可以怎么补救,主要涉及到runtime技术;三是了解除了NSObject这个基类的另外一个基类--NSProxy,以及这个NSProxy基类有什么用处,应用场景在哪里,其实这个类用到的技术主要是代理和消息转发机制的结合,那又是如何结合使用的呢,让我们带着这些疑问开启我们的知识之旅吧。

一、代理模式

       原理:在iOS中代理的本质就是代理对象内存的传递和操作,我们在委托类设置代理对象后,实际上只是用一个id类型的指针将代理对象进行了一个弱引用。委托方让代理方执行操作,实际上是在委托类中向这个id类型指针指向的对象发送消息,而这个id类型指针指向的对象,就是代理对象。

IOS设计架构模式:代理模式和消息转发机制_第1张图片

       通过上面这张图我们发现,其实委托方的代理属性本质上就是代理对象自身,设置委托代理就是代理属性指针指向代理对象,相当于代理对象只是在委托方中调用自己的方法,如果方法没有实现就会导致崩溃。从崩溃的信息上来看,就可以看出来是代理方没有实现协议中的方法导致的崩溃。

       而协议只是一种语法,是声明委托方中的代理属性可以调用协议中声明的方法,而协议中方法的实现还是有代理方完成,而协议方和委托方都不知道代理方有没有完成,也不需要知道怎么完成。

       代理内存管理:为什么我们设置代理属性都使用weak呢?

       我们定义的指针默认都是__strong类型的,而属性本质上也是一个成员变量和setget方法构成的,strong类型的指针会造成强引用,必定会影响一个对象的生命周期,这也就会形成循环引用。IOS设计架构模式:代理模式和消息转发机制_第2张图片          

       上图中,由于代理对象使用强引用指针,引用创建的委托方LoginVC对象,并且成为LoginVC的代理。这就会导致LoginVCdelegate属性强引用代理对象,导致循环引用的问题,最终两个对象都无法正常释放。   

IOS设计架构模式:代理模式和消息转发机制_第3张图片

       我们将LoginVC对象的delegate属性,设置为弱引用属性。这样在代理对象生命周期存在时,可以正常为我们工作,如果代理对象被释放,委托方和代理对象都不会因为内存释放导致的Crash

       但是,这样还有点问题,真的不会崩溃吗?

       下面两种方式都是弱引用代理对象,但是第一种在代理对象被释放后不会导致崩溃,而第二种会导致崩溃。

@property (nonatomic, weak) id delegate;
@property (nonatomic, assign) id delegate;

   weakassign是一种“非拥有关系”的指针,通过这两种修饰符修饰的指针变量,都不会改变被引用对象的引用计数。但是在一个对象被释放后,weak会自动将指针指向nil,而assign则不会。在iOS中,向nil发送消息时不会导致崩溃的,所以assign就会导致野指针的错误unrecognized selector sent to instance。所以我们如果修饰代理属性,还是用weak修饰吧,比较安全。

       基本使用:代理是一种通用的设计模式,在iOS中对代理设计模式支持的很好,有特定的语法来实现代理模式,OC语言可以通过@Protocol实现协议。

       组成部分

  • 协议:用来指定代理双方可以做什么,必须做什么。
  • 代理:根据指定的协议,完成委托方需要实现的功能。
  • 委托:根据指定的协议,指定代理去完成什么功能。

       这里用一张图来阐述一下三方之间的关系:

IOS设计架构模式:代理模式和消息转发机制_第4张图片

Protocol-协议的概念

       从上图中我们可以看到三方之间的关系,在实际应用中通过协议来规定代理双方的行为,协议中的内容一般都是方法列表,当然也可以定义属性,我会在后续文章中顺带讲一下协议中定义属性。

       协议是公共的定义,如果只是某个类使用,我们常做的就是写在某个类中。如果是多个类都是用同一个协议,建议创建一个Protocol文件,在这个文件中定义协议。遵循的协议可以被继承,例如我们常用的UITableView,由于继承自UIScrollView的缘故,所以也将UIScrollViewDelegate继承了过来,我们可以通过代理方法获取UITableView偏移量等状态参数。

       协议只能定义公用的一套接口,类似于一个约束代理双方的作用。但不能提供具体的实现方法,实现方法需要代理对象去实现。协议可以继承其他协议,并且可以继承多个协议,在iOS中对象是不支持多继承的,而协议可以多继承。

// 当前协议继承了三个协议,这样其他三个协议中的方法列表都会被继承过来
@protocol LoginProtocol 
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;
@end

       协议有两个修饰符@optional@required,创建一个协议如果没有声明,默认是@required状态的。这两个修饰符只是约定代理是否强制需要遵守协议,如果@required状态的方法代理没有遵守,会报一个黄色的警告,只是起一个约束的作用,没有其他功能。

       无论是@optional还是@required,在委托方调用代理方法时都需要做一个判断,判断代理是否实现当前方法,否则会导致崩溃。

       示例:

// 判断代理对象是否实现这个方法,没有实现会导致崩溃
if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
    [self.delegate userLoginWithUsername:self.username.text password:self.password.text];
}

       示例:假设我在公司正在敲代码,敲的正开心呢,突然口渴了,想喝一瓶红茶。这时我就可以拿起手机去外卖app上定一个红茶,然后外卖app就会下单给店铺并让店铺给我送过来。

       这个过程中,外卖app就是我的代理,我就是委托方,我买了一瓶红茶并付给外卖app钱,这就是购买协议。我只需要从外卖app上购买就可以,具体的操作都由外卖app去处理,我只需要最后接收这瓶红茶就可以。我付的钱就是参数,最后送过来的红茶就是处理结果。

       但是我买红茶的同时,我还想吃一份必胜客披萨,我需要另外向必胜客app去订餐,上面的外卖app并没有这个功能。我又向必胜客购买了一份披萨,必胜客当做我的代理去为我做这份披萨,并最后送到我手里。这就是多个代理对象,我就是委托方。IOS设计架构模式:代理模式和消息转发机制_第5张图片

       在iOS中一个代理可以有多个委托方,而一个委托方也可以有多个代理。我指定了外卖app和必胜客两个代理,也可以再指定麦当劳等多个代理,委托方也可以为多个代理服务。

       代理对象在很多情况下其实是可以复用的,可以创建多个代理对象为多个委托方服务,在下面将会通过一个小例子介绍一下控制器代理的复用。

下面是一个简单的代理:

       首先定义一个协议类,来定义公共协议

#import 
@protocol LoginProtocol 
@optional
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;
@end

         定义委托类,这里简单实现了一个用户登录功能,将用户登录后的账号密码传递出去,有代理来处理具体登录细节。

#import 
#import "LoginProtocol.h"
/**
 *  当前类是委托类。用户登录后,让代理对象去实现登录的具体细节,委托类不需要知道其中实现的具体细节。
 */
@interface LoginViewController : UIViewController
// 通过属性来设置代理对象
@property (nonatomic, weak) id delegate;
@end

实现部分:

@implementation LoginViewController
- (void)loginButtonClick:(UIButton *)button {
  // 判断代理对象是否实现这个方法,没有实现会导致崩溃
  if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
      // 调用代理对象的登录方法,代理对象去实现登录方法
      [self.delegate userLoginWithUsername:self.username.text password:self.password.text];
  }
}

 

       代理方,实现具体的登录流程,委托方不需要知道实现细节。

// 遵守登录协议
@interface ViewController ()  
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    LoginViewController *loginVC = [[LoginViewController alloc] init];
    loginVC.delegate = self;
    [self.navigationController pushViewController:loginVC animated:YES];
}

/**
 *  代理方实现具体登录细节
 */
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password {
    NSLog(@"username : %@, password : %@", username, password);
}

 

二、消息转发机制

       众所周知OC的一个对象在发送消息的时候首先在该类的struct objc_method_list列表中去搜索,如果找到则直接调用相关方法的实现,如果没有找到就会通过super_class指针沿着继承树向上去搜索,如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到。那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这样就会报unrecognized selector 错误。其实在调用doesNotRecognizeSelector:方法之前还会进行消息转发---还有三次机会来补救。也就是常说的OC消息转发的三次补救措施。

IOS设计架构模式:代理模式和消息转发机制_第6张图片

     如上图,总的来说一个OC消息的发送会经历四个阶段(该四个阶段都是搜索到NSObject再进入下阶段)

      1)先在本类中搜索改方法的实现,如果有则直接调用若果没有则去父类中搜索直到NSObject,如果NSObject没有则进入消息转发(类的动态方法解析、备用接受者对象、完整的消息转发)。

      2)类的动态方法解析:

     首先创建SonPerson类,在ViewController 里面写

 id person = [[SonPerson alloc]init];

 [person appendString:@""];

     注意这里要用id 不然编译报错。

     在该类和父类中没有找到该方法的实现则会执行 +(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法。在+(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法 中利用黑魔法runtime 动态添加方法实现。

void dynamicAdditionMethodIMP(id self,SEL _cmd){

    NSLog(@"dynamicAdditionMethodIMP");

}

+(BOOL)resolveClassMethod:(SEL)sel{

    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));

    if(sel ==@selector(appendString:)) {

        class_addMethod([self class], sel, (IMP)dynamicAdditionMethodIMP,"v@:");

        returnYES;

    }

    return[superresolveClassMethod:sel];

}

+(BOOL)resolveInstanceMethod:(SEL)sel{

    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));

    if(sel ==@selector(appendString:)) {

        class_addMethod([self class], sel, (IMP)dynamicAdditionMethodIMP,"v@:");

        returnYES;

    }

    return[super resolveInstanceMethod:sel];

}

        BOOL class_addMethod(Class cls, SEL name, IMP imp,constchar*types);

        第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。

控制台输出:

resolveInstanceMethod: appendString:

dynamicAdditionMethodIMP

        3)备用接受者: 在+(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法返回NO的时候进入备用接受者阶段 。

    创建一个备用接受者类ForwardPerson 实现appendString:方法

 -(void)appendString:(NSString*)str{

        NSLog(@"%@===%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));

 }

    在SonPerson类中实现- (id)forwardingTargetForSelector:(SEL)aSelector 方法 并返回一个备用接受者对象 

- (id)forwardingTargetForSelector:(SEL)aSelector{

    NSLog(@"forwardingTargetForSelector");

    return [ForwardPerson new];

}

控制台输出:

forwardingTargetForSelector

ForwardPerson===appendString:

       4)完整的消息转发:当-(void)forwardInvocation:(NSInvocation*)anInvocation 方法方法nil的时候则进入消息转发的最后阶段,完整的消息转发。也需要创建一个转发对象ForwardInvocation。

#import "ForwardInvocation.h"

@implementationForwardInvocation

-(void)appendString:(NSString*)str{

    NSLog(@"%@===%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));

}

@end

 在SonPerson中实现-(void)forwardInvocation:(NSInvocation*)anInvocation和- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector方法

-(void)forwardInvocation:(NSInvocation*)anInvocation{

    NSLog(@"forwardInvocation");

    if ([ForwardInvocation instancesRespondToSelector:anInvocation.selector]) {

        [anInvocation invokeWithTarget:self.invocation];

    }

}

/*必须实现这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象 返回nil上面方法不执行*/

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{

    NSMethodSignature*signature = [super methodSignatureForSelector:aSelector];

    if(!signature){

        if ([ForwardInvocation instancesRespondToSelector:aSelector]){

            signature = [ForwardInvocation instanceMethodSignatureForSelector:aSelector];

        }

    }

    returnsignature;

}

控制台输出:

forwardInvocation

ForwardInvocation===appendString:

最后附Demo:https://github.com/SionChen/OBJC_SendMsg  并附消息转发一张图:

IOS设计架构模式:代理模式和消息转发机制_第7张图片

三、NSProxy基类

       众所周知,NSObject类是Objective-C中大部分类的基类。但不是很多人知道除了NSObject之外的另一个基类——NSProxy。

         这个奇怪的类是干嘛的?请允许我做一个黑人问号脸,马上查了一下Apple的官方文档:NSProxy

NSProxy is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.

        总的来说,NSProxy是一个虚类,你可以通过继承它,并重写这两个方法以实现消息转发到另一个实例。

- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

       现在NSProxy的真面目终于浮出水面:负责将消息转发到真正的target的代理类。举个例子,你想要卖一件二手物品,但是你并不想直接跟卖家接触(直接向target发消息),这时你去找了一个第三方,你告诉这个第三方你要买什么、出多少钱买、什么时候要等(向代理发消息),第三方再去跟卖家接触并把这些信息转告卖家(转发消息给真实的target),最后通过第三方去完成这个交易。

       了解完NSProxy是是什么以后,那么它究竟能帮我们干些什么呢?

       多继承在编程中可以说是比较有用的特性。举个例子,原本有两个相互独立的类A和类B,它们各自继承各自的父类,项目进行地好好的,突然有一天产品经理过来告诉你,我要在下个版本加一个xxxxx的特性,非常紧急。一脸懵逼的你发现如果要实现这个特性,你需要对类A以及其父类作很大的修改,代价非常之高。突然你意识到原来类B的父类已经有类似的功能,你只需要让类A继承于类B的父类并重写其某些方法就能实现,这样做高效且低风险,于是你屁颠屁颠地撸起了代码。

        可是,Objective-C却不支持这样一个强大的特性。不过NSProxy可以帮我们在某种程度上(这只是一个模拟的多继承,并不是完全的多继承)解决这个问题。

        现在假设我们想要去买书,但是我懒癌犯了,不想直接去书店(供应商)买,如果有一个跑腿的人(经销商)帮我去书店买完,我再跟他买。同时,我买完书又想买件衣服,我又可以很轻松地在他那里买到一件衣服(多继承)。

        首先,我们定义BookProvider类与ClothesProvider类作为基类。

//  TDBookProvider.h
 
#import 
 
@protocol TDBookProviderProtocol 
 
- (void)purchaseBookWithTitle:(NSString *)bookTitle;
 
@end
 
@interface TDBookProvider : NSObject
 
@end
//  TDClothesProvider.h
 
#import 
 
typedef NS_ENUM (NSInteger, TDClothesSize){
    TDClothesSizeSmall = 0,
    TDClothesSizeMedium,
    TDClothesSizeLarge
};
 
@protocol TDClothesProviderProtocol 
 
- (void)purchaseClothesWithSize:(TDClothesSize )size;
 
@end
 
@interface TDClothesProvider : NSObject
 
@end

       这里要注意:一定要通过protocol来声明接口,而不是直接在类的@interfere中定义。因为通过protocol来声明接口,然后让proxy类遵循此协议,可以骗过编译器防止编译器提示proxy类未声明接口的错误。这个问题下面可以看到。

       然后是这两个类的实现

//  TDBookProvider.m
 
#import "TDBookProvider.h"
 
@interface TDBookProvider () 
 
@end
@implementation TDBookProvider
 
- (void)purchaseBookWithTitle:(NSString *)bookTitle{
    NSLog(@"You've bought \"%@\"",bookTitle);
}
 
@end
//  TDClothesProvider.m
 
#import "TDClothesProvider.h"
 
@interface TDClothesProvider () 
 
@end
 
@implementation TDClothesProvider
 
- (void)purchaseClothesWithSize:(TDClothesSize )size{
    NSString *sizeStr;
    switch (size) {
        case TDClothesSizeLarge:
            sizeStr = @"large size";
            break;
        case TDClothesSizeMedium:
            sizeStr = @"medium size";
            break;
        case TDClothesSizeSmall:
            sizeStr = @"small size";
            break;
        default:
            break;
    }
    NSLog(@"You've bought some clothes of %@",sizeStr);
}
 
@end

       现在两个Provider的类写完,我们可以直接向供应商买东西了,但这跟我们的需求还有很大差异,我们需要一个中间的经销商。

//  TDDealerProxy.h
 
#import 
#import "TDBookProvider.h"
#import "TDClothesProvider.h"
 
@interface TDDealerProxy : NSProxy 
 
+ (instancetype )dealerProxy;
 
@end

       这里有两个要注意的问题:
1、TDDealerProxy这个子类必须要遵循之前定义的两个协议TDBookProviderProtocol与TDClothesProviderProtocol,目的是骗过编译器,让编译器认为这个类实现了上面两个协议
2、NSProxy类是没有init方法的,也就是说如果我们要获得一个NSProxy的实例,代码只需要这样:

NSProxy *proxyInstance = [NSProxy alloc];

        接下来看实现文件

//  TDDealerProxy.m
 
#import "TDDealerProxy.h"
#import 
 
@interface TDDealerProxy () {
    TDBookProvider          *_bookProvider;
    TDClothesProvider       *_clothesProvider;
    NSMutableDictionary     *_methodsMap;
}
@end
 
@implementation TDDealerProxy
 
#pragma mark - class method
+ (instancetype)dealerProxy{
    return [[TDDealerProxy alloc] init];
}
 
#pragma mark - init
- (instancetype)init{
    _methodsMap = [NSMutableDictionary dictionary];
    _bookProvider = [[TDBookProvider alloc] init];
    _clothesProvider = [[TDClothesProvider alloc] init];
 
    //映射target及其对应方法名
    [self _registerMethodsWithTarget:_bookProvider];
    [self _registerMethodsWithTarget:_clothesProvider];
 
    return self;
}
 
#pragma mark - private method
- (void)_registerMethodsWithTarget:(id )target{
 
    unsigned int numberOfMethods = 0;
 
    //获取target方法列表
    Method *method_list = class_copyMethodList([target class], &numberOfMethods);
 
    for (int i = 0; i < numberOfMethods; i ++) {
        //获取方法名并存入字典
        Method temp_method = method_list[i];
        SEL temp_sel = method_getName(temp_method);
        const char *temp_method_name = sel_getName(temp_sel);
        [_methodsMap setObject:target forKey:[NSString stringWithUTF8String:temp_method_name]];
    }
 
    free(method_list);
}
#pragma mark - NSProxy override methods
- (void)forwardInvocation:(NSInvocation *)invocation{
    //获取当前选择子
    SEL sel = invocation.selector;
 
    //获取选择子方法名
    NSString *methodName = NSStringFromSelector(sel);
 
    //在字典中查找对应的target
    id target = _methodsMap[methodName];
 
    //检查target
    if (target && [target respondsToSelector:sel]) {
        [invocation invokeWithTarget:target];
    } else {
        [super forwardInvocation:invocation];
    }
}
 
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    //获取选择子方法名
    NSString *methodName = NSStringFromSelector(sel);
 
    //在字典中查找对应的target
    id target = _methodsMap[methodName];
 
    //检查target
    if (target && [target respondsToSelector:sel]) {
        return [target methodSignatureForSelector:sel];
    } else {
        return [super methodSignatureForSelector:sel];
    }
}
 
@end

       大功告成,现在我们的经销商也有了,最后要做的就是告诉经销商我们要买什么书跟什么衣服了(发消息)。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
 
    TDDealerProxy *dealerProxy = [TDDealerProxy dealerProxy];
    [dealerProxy purchaseBookWithTitle:@"Swift 100 Tips"];
    [dealerProxy purchaseClothesWithSize:TDClothesSizeMedium];
 
    // Override point for customization after application launch.
    return YES;
}

运行看看log:

2016-08-06 01:10:27.095 TDProxyDemo[37732:924470] You've bought "Swift 100 Tips"

2016-08-06 01:10:27.095 TDProxyDemo[37732:924470] You've bought some clothes of medium size

demo 地址:https://github.com/jojotov/TDProxyDemo

 

参考链接: https://www.jianshu.com/p/2113ffe54b30

                   https://www.aliyun.com/jiaocheng/367798.html

                   http://ios.jobbole.com/87856/

 

你可能感兴趣的:(IOS架构设计)