协议与代理的异形变换

作者: 01_Jack | 来源:发表于2018-01-25 10:40 被阅读262次

    前言

    一个类/对象只要遵守某个协议就可以调用协议方法,从而在某些方面达成共识。如果单纯遵守协议并实现协议方法,在某些场景从外部调用,这篇文章已经没有存在的必要了。当协议与代理配合使用时,可以组成代理模式。在iOS中有这么一句话,“代理是一对一的”,本文围绕这句话展开,并给出不同场景下的多种解决方案及终极解决方案。

    引例

    场景:假设JKScrollView继承自UIScrollView,JKScrollView对外提供的接口需要用到UIScrollViewDelegate,此时需要将JKScrollView对应实例对象的代理设置为自身。这样一来,当外部重设代理时,会导致内部代理失效。怎样在确保内部代理正常的前提下,外部仍然可以获取代理相应的功能?

    写法一:

    最简单的写法大概长这样:

    NS_ASSUME_NONNULL_BEGIN
    
    @class JKScrollView;
    typedef void(^JKScrollViewDidScrollBlock)(JKScrollView *scrollView);
    
    @interface JKScrollView : UIScrollView<UIScrollViewDelegate>
    @property (nullable, nonatomic, copy) JKScrollViewDidScrollBlock didScrollBlock;
    @end
    
    NS_ASSUME_NONNULL_END
    
    - (instancetype)init {
        if (self = [super init]) {
            [super setDelegate:self];
        }
        return self;
    }
    
    - (void)setDelegate:(id<UIScrollViewDelegate>)delegate {
        if (!delegate || self.delegate == delegate) return;
        [super setDelegate:self];
    }
    
    - (void)setDidScrollBlock:(JKScrollViewDidScrollBlock)didScrollBlock {
        if (!didScrollBlock) return;
        _didScrollBlock = [didScrollBlock copy];
    }
    
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        !_didScrollBlock ? : _didScrollBlock(self);
        //  do something
    }
    

    1.在初始化时,将代理设为self
    2.重写代理的setter,强制将代理设为self(以防外部改变delegate)
    3.在代理方法中执行外部传入的block

    这样写确实可以完成相应需求,但是需要手动添加block,如果不想添加block也可以这么写


    写法二:
    NS_ASSUME_NONNULL_BEGIN
    
    @interface JKTableScrollView : UIScrollView
    @property (nullable, nonatomic, weak, readonly) id<UIScrollViewDelegate> fakeDelegate;
    @end
    
    NS_ASSUME_NONNULL_END
    
    - (instancetype)init {
        if (self = [super init]) {
            [super setDelegate:self];
        }
        return self;
    }
    
    - (void)setDelegate:(id<UIScrollViewDelegate>)delegate {
        if (!delegate || self.delegate == delegate) return;
        _fakeDelegate = delegate;
        [super setDelegate:self];
    }
    
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        if (_fakeDelegate && [_fakeDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
            [_fakeDelegate scrollViewDidScroll:scrollView];
        }
        // do something
    }
    

    与第一种方法略有不同,在delegate的setter中,将外部设置的delegate保存,并在内部代理方法调用时判断外部代理是否实现相同方法,如果实现,则手动调用。
    这两种写法思想类似,并且写起来都很麻烦,特别当有多个代理方法要实现,会做很多无用功。

    而且会有这么一种情况:
    内部并不需要用到代理的某个方法,但是外部需要用到,此时在内部还得写相应的代理方法用来适配外部代理调用。

    为了减少这种适配,现在引入第三种写法。
    在介绍第三种写法前,请先确保了解消息转发流程,如果不了解,可以看这篇文章传送门

    除此之外,还会用到runtime中的这个函数

    struct objc_method_description protocol_getMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull aSel, BOOL isRequiredMethod, BOOL isInstanceMethod)
    

    可以看到,objc_method_description结构体的成员变量很简单,只有方法名与参数

    objc_method_description
    写法三:

    说明:这种方法在第二种方法的基础上添加如下代码

    - (BOOL)respondsToSelector:(SEL)aSelector {
        BOOL result = [super respondsToSelector:aSelector];
        if (!result && _fakeDelegate) {
            struct objc_method_description omd = protocol_getMethodDescription(@protocol(UIScrollViewDelegate), aSelector, NO, YES);
            if (omd.name) {
                result = [_fakeDelegate respondsToSelector:aSelector];
            }
        }
        return result;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (_fakeDelegate) {
            struct objc_method_description omd = protocol_getMethodDescription(@protocol(UIScrollViewDelegate), aSelector, NO, YES);
            if (omd.name) {
                return _fakeDelegate;
            }
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    

    重写respondsToSelector :方法,满足代码中条件则认为可响应。若某代理方法内部未实现,而外部代理实现(fakeDelegate),由于真实代理为内部代理(self),正常流程fakeDelegate无法响应。重写respondsToSelector :后会进入消息转发流程,在forwardingTargetForSelector:判断是否满足条件,如果满足则转发给_fakeDelegate,从而使_fakeDelegate可以响应内部未实现的代理方法。

    这种写法相对前两种方法大大简化了实现外部代理的书写流程,但还是很烦,因为内部代理已实现的方法没法进入消息转发流程,所以只要内部代理实现的方法,外部代理想实现就必须在内部做一次判断。

    同时还有这么两种情况:
    如果外部要实现的代理有多个怎么办?
    如果内部并不需要实现代理,外部需要实现多个代理又怎么办?


    接着方法三的思路来,是否能在消息转发流程将要实现的代理方法转发给多个外部代理对象?显然是可以的,因为消息转发的最后一步forwardInvocation:参数为NSInvocation,而NSInvocation可以指定target。如果不熟悉NSInvocation,可以看这篇文章传送门
    在开始终极写法前,还需要解决一个问题:targets由外部传入,如果直接用NSArray保存,而保存的对象又被其他对象持有,此时极易导致循环引用。因此,这里用NSPointerArray代替NSArray以防循环引用

    终极写法:

    直接上代码:

    #import "ViewController.h"
    #import "JKProtocolHelper.h"
    
    @interface Test: NSObject<UIScrollViewDelegate>
    @end
    
    @implementation Test
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        NSLog(@"Test");
    }
    @end
    
    
    @interface ViewController ()<UITableViewDelegate>
    @property (nonatomic, strong) Test *test;
    @property (nonatomic, strong) id helper;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
        [self.view addSubview:tableView];
        
        _test = [Test new];
        _helper = [JKProtocolHelper helperWithProtocol:@protocol(UIScrollViewDelegate) executors:@[self, _test]];
        tableView.delegate = _helper;
    }
    
    - (void)dealloc {
        NSLog(@"%s", __func__);
    }
    
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        NSLog(@"view controller");
    }
    
    
    @end
    
    JKProtocolHelper.gif

    Demo已放到github,自取

    Have fun!

    相关文章

      网友评论

        本文标题:协议与代理的异形变换

        本文链接:https://www.haomeiwen.com/subject/ihtuaxtx.html