zoukankan      html  css  js  c++  java
  • 【代码优化】调用optional delegates的最佳方法

    【转载请注明出处】http://www.cnblogs.com/lexingyu/p/3932475.html

    本文是以下两篇blog的综合脱水,感谢两位作者为解放码农生产力所做的深入思考=。=
    Smart Proxy Delegation
    Elegant Delegation

    使用delegate的情境通常是这样

    定义class和delegate

    @protocol TestObjectDelegate <NSObject>
    @optional
    - (void)testObjectMethod;
    - (NSString *)testObjectMethodWithReturnValue;
    
    @end
    
    
    @interface TestObject : NSObject
    
    @property (nonatomic, weak) id<TestObjectDelegate> delegate;
    
    - (void)print;
    - (void)printWithLog;
    
    @end
    

    在类的内部调用delegate的方法

    - (void)print
    {
        //call the delegate to do the real work
    }
    

    ###调用的方法通常有以下两种

    普通青年:

    if ([self.delegate respondsToSelector:@selector(testObjectMethod)])
        {
            [self.delegate testObjectMethod];
        }
    

    这个办法的缺点是
    1)引入了大量glue code,每个optional function都需要3行代码。尤其在开启clang的-Warc-repeated-use-of-weak时,多次使用self.delegate(通常情况下,是weak)会被警告;

    所以很可能还得这么写

    - (void)print
    {
        id <TestObjectDelegate> delegate = self.delegate;
        if ([delegate respondsToSelector:@selector(testObjectMethod)])
        {
            [delegate testObjectMethod];
        }
    }
    

    2)调用的方法名需要写两次,很可能写错导致方法未被调用;
    3)对于高频率调用的方法而言,意味着需要反复调用respondToSeletor,性能上有所影响(RunTime可能会对respondToSeletor进行缓存,因此在大部分应用上这一点不需要计入考量)。

    文艺青年
    先添加flag

    @interface TestObject : NSObject
    {
        struct
        {
            unsigned int respond2TestObjectMethod:1;
        }_flags;
    }
    
    @property (nonatomic, weak) id<TestObjectDelegate> delegate;
    
    - (void)print;
    
    @end
    

    再重载setDelegate以设置flag,将respondToSeletor的结果缓存起来

    - (void)setDelegate:(id<TestObjectDelegate>)delegate
    {
        _delegate = delegate;
        
        BOOL respond2TestObjectMethod = [delegate respondsToSelector:@selector(testObjectMethod)];
        _flags.respond2TestObjectMethod = respond2TestObjectMethod ? 1 : 0;
    }
    

    最后在print中直接使用缓存的结果

    - (void)print
    {
        if (_flags.respond2TestObjectMethod)
        {
            [self.delegate testObjectMethod];
        }
    }
    

    这个方法被Apple广泛采用,在SDK中随处可见。
    它的优点是将respondToSeletor的结果手动缓存了起来,不需要做性能上的猜测,同时避开了
    -Warc-repeated-use-of-weak的警告。
    但遗憾的是,代码的冗余并没有被移除,反而更为严重(调用时仍然需要3行glue code,且在头文件和setDelegate中添加了大量代码)。当delegate中的方法名需要变动时,需要同时修改多处代码,真如噩梦一般。

    嗯。。。。。。抱歉这里没有二逼青年

    外国友人的想法

    实际上我们真正想要的是类似于这样的东西

    - (void)print
    {
        [self.delegateProxy testObjectMethod];
    }
    

    把glue code也好,其他额外处理也好,都放到一个统一的地方。在调用的时候,一句话简单明了,解决问题。
    那么具体怎么做呢?
    其实,OC的方法调用,或者准确地说,消息传递,就是这样一种机制。这里上一张自绘的图以便说明

    OC中任何一次方法调用,都会从1开始走这个流程,一个步骤不行就进行下一步。若所有4个步骤走完仍然无法找到对应的impletation,则触发异常,程序crash。简单说一下各个步骤的作用
    1)在类的方法表(methodList)中,根据seletor查找对应的impletation;
    2) resolveInstanceMethod用于集中处理类中一些类似的方法,比如在使用core data时需要指定多个property为@dynamic,它们的setter和getter就可以集中在这个方法里做;
    3)forwardingTargetForSelector,作用是将本对象无法处理的调用信息转给另一个对象处理,但不改变调用信息;
    4)forwardInvocation,作用是根据methodSignatureForSelector和调用参数等信息生成的NSInvocation来指定一个对象处理本次调用,在指定时可以对调用信息做任意的修改,比如增加参数个数。

    3被称为Fast message forwarding,相应地4则是Regular message forwarding,二者合在一起才是完整的Message forwarding

    C语言在调用函数时,需要知道函数的原型,以便将参数放入寄存器或压入栈中,并视情况预留返回值的空间。OC作为C语言的超集,也需要顾及这一点。函数的调用信息在OC中以NSMethodSignature的形式存在,在Regular message forwarding中由methodSignatureForSelector返回。

    从以上说明不难看出,1和2的作用是在类内部寻找impletation,而3和4则是在类外部寻找合适的其他类的实例来处理调用信息。显而易见,3和4正是delegateProxy所需要的。

    铺垫了这么多,终于到了正题。

    用Message forwarding机制,来构建一个delegateProxy

    在这里构建了一个NSProxy的派生类作为delegateProxy,像这样

    @interface CDDelegateProxy : NSProxy
    
    @property (nonatomic, weak, readonly) id delegate;
    @property (nonatomic, strong, readonly) Protocol *protocol;
    @property (nonatomic, strong, readonly) NSValue *defaultReturnValue;
    
    @end
    

    delegateProxy中分别保存了被代理的delegate对象、delegate对应的protocol和方法未找到时提供的默认值。
    在.m文件中,首先将glue code放入,像这样

    //供外部需要时使用
    - (BOOL)respondsToSelector:(SEL)selector
    {
        return [_delegate respondsToSelector:selector];
    }
    
    //Fast message forwarding, 存放glue code
    - (id)forwardingTargetForSelector:(SEL)selector
    {
        id delegate = _delegate;
        return [delegate respondsToSelector:selector] ? delegate : self;
    }
    

    嗯。。。至此似乎就完事了=。=
    大部分情况下确实如此。但当方法不存在又需要一个默认返回值时,比如

    - (void)printWithLog
    {
        //这里已经用上delegateProxy了,哈哈
        NSString *logInfo = [self.delegateProxy testObjectMethodWithReturnValue];
        NSLog(@"%@", logInfo);
    }
    

    就需要用到Regular message forwarding了。具体做法如下

    //Regular message forwarding
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
    {
        id delegate = _delegate;
        NSMethodSignature *signature = [delegate methodSignatureForSelector:selector];
        
        //若delegate未实现对应方法,则从protocol的声明中获取MethodSignature
        if (!signature)
        {
            if (!_signatures) _signatures = [self methodSignaturesForProtocol:_protocol];
            signature = CFDictionaryGetValue(_signatures, selector);
        }
        
        //此处如果return nil, 则不会触发forwardInvocation
        return signature;
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation
    {
        //若默认返回值和invocation中指定的返回值一致,则取默认返回值
        if (_defaultReturnValue
            && strcmp(_defaultReturnValue.objCType, invocation.methodSignature.methodReturnType) == 0)
        {
            char buffer[invocation.methodSignature.methodReturnLength];
            [_defaultReturnValue getValue:buffer];
            [invocation setReturnValue:&buffer];
        }
    }
    
    

    首先由methodSignatureForSelector根据protocol中的方法声明,返回一个signature,再由forwardInvocation判断与默认的返回值是否类型一致,一致则返回预设的默认值(即刚才提到的defaultReturnValue)。

    这样,delegateProxy就构建完毕了。在使用的时候,应注意delegateProxy的作用只是在类内部保持调用的简洁,对于外部代码而言,它应该是透明的。具体来说,首先应该将deleagteProxy定义在class extension中

    //.m文件中
    @interface SomeObject ()<TestObjectDelegate>
    
    @property (nonatomic, strong) id<TestObjectDelegate> delegateProxy;
    
    @end
    

    这里将delegateProxy直接声明为id的形式,目的是使之后编码时仍然能够享有Xcode对protocol中方法的自动提示补全。
    接着override delegate(真正id被定义在头文件中)

    - (void)setDelegate:(id <TestObjectDelegate>)delegate 
    {
      self.delegateProxy = delegate ? (id <TestObjectDelegate>)[[CDDelegateProxy alloc] initWithDelegate:delegate] : nil;
    }
    - (id <TestObjectDelegate>)delegate
     {
      return ((CDDelegateProxy *)self.delegateProxy).delegate;
    }
    

    这个步骤看着有些繁琐,可以通过宏来简化,比如

    #define CD_DELEGATE_PROXY_CUSTOM(protocolname, GETTER, SETTER) 
    - (id<protocolname>)GETTER { return ((PSTDelegateProxy *)self.GETTER##Proxy).delegate; } 
    - (void)SETTER:(id<protocolname>)delegate { self.GETTER##Proxy = delegate ? (id<protocolname>)[[PSTDelegateProxy alloc] initWithDelegate:delegate conformingToProtocol:@protocol(protocolname) defaultReturnValue:nil] : nil; }
    
    #define CD_DELEGATE_PROXY(protocolname) PST_DELEGATE_PROXY_CUSTOM(protocolname, delegate, setDelegate)
    

    在使用的使用可以简单地

    CD_DELEGATE_PROXY(id <PSPDFResizableViewDelegate>)
    

    当然,对于比较个性化的delegate的名称,可以通过扩展这个宏来实现。

    如此一来,外部访问delegate时,获取到的仍然是正确的对象。
    以上,就是调用optional delegates的最佳方法,从起因到原理到解决方案的完整阐述。

    文中为便于说明,使用了我自己写的一个简化版的delegateProxy,这里提供一个原作者Peter steinberger的完整实现,有不少值得学习的点哦。

    终于写完啦!!!!

  • 相关阅读:
    linux脚本练习之将数据导入oracle表
    linux脚本之一个程序调用另一个程序
    使用客户端Navicat连接数据库oracle19c
    centos7安装与卸载oracle19c
    Redis-cluster集群搭建(redis版本5.0.4)
    linux下redis的哨兵模式
    使用POI导入Excel文件
    MySQL8.0搭建MGR集群(MySQL-shell、MySQL-router)
    MySQL Shell用法
    CentOS 7下使用rpm包安装MySQL8.0
  • 原文地址:https://www.cnblogs.com/lexingyu/p/3932475.html
Copyright © 2011-2022 走看看