Objective-C 中优雅代理的最佳实践
status
category
date
summary
slug
icon
tags
password
必现的 bug 就不是问题。——某同事
在去年暑期实习时,因为犯了一个错误导致线上必现 crash,原因是没有使用
respondsToSelector
检查optional
代理方法的实现,委托者未实现方法从而引起崩溃。虽然检查方法实现的做法保证安全,但冗余的代码太过繁琐,产生了大量的Glue Code
,在整个项目中搜索respondsToSelector
,使用量非常之大,极不优雅。通过查阅他人的博客和资料,发现了一些可执行的改进措施,在此记录。当前的困境
目前的困境在于,调用
delegate
定义的optional
方法时,由于不确定委托者是否实现了该方法,所以需要进行检查和保护,一般情况下定义的delegate
:定义一个类,持有
delegate
,给delegate
提供一个用来调用的方法,并且在调用时针对delegate
是否实现协议的方法optionalMethod
做一个检查:ViewController
实现协议TestObjectDelegate
中定义的方法optionalMethod
,并持有TestObject
,将其delegate
设置为self
,并调用相应的方法:这样做的缺点是,每次调用
optional
方法时,都需要通过respondsToSelector
判断检查,首先可能导致方法名拼写错误,另外反复调用respondsToSelector
可能会对性能有所影响,RunTime
可能也会相应做一些缓存。(第二点有待考证)即使使用@required,编译器也只是发出警告,并不能可靠地解决现有问题。苹果的做法
为了不考虑
RunTime
的可靠性,我们可以像很多官方 SDK 的做法一样,通过定义struct
手动创建缓存:再重载
setDelegate
方法,判断 delegate 是否实现了方法,并存入缓存,同时判断逻辑也作出相应改变:这样的做饭看起来巧妙,将
respondToSeletor
的结果缓存起来,可以保证没有性能上的影响,调用时的检查也方便了很多,但是并没有从根本上解决代码冗余的问题,还在头文件和setDelegate
中添加了大量代码。如果协议中的方法名有所变动,需要修改多处代码,拓展性极差。关于这种做法的讨论,可以参考GitHub Gist。从消息传递说起
我们需要的是这样的代码:
它需要以下优点:
- 不需要每次都手动检查
optional
方法是否实现
- 不需要在其他地方编写更多冗余的代码
- 不牺牲太多的性能
- 具有可扩展性,便于修改
如果有这样一个对象或者类,能够在 delegate 调用方法时自动地进行判断该方法是否实现,就能满足我们的要求了。而告诉 delegate 执行某方法,即消息的传递,是可以被代理控制的,也就是说我们需要控制消息传递的流程。举个相似但不恰当的例子来说,在点击事件传递的过程中,当我们不想让某个控件作出响应时,就在消息传递到它之前进行拦截,让它不知道有这回事。
为了描述方便,我给消息转发的过程画了一张图,因为自己还没有对这方面深入研究过,略去了很多细节,也没有按照方法名作为步骤:

简单归纳一下给对象发送消息经过的步骤:
- 在对象类及其父类的类对象的方法列表中查找该消息,如果找到了,就通过函数指针去执行,消息已处理;如果没找到,就通过
resolveInstanceMethod
或者resolveClassMethod
解析消息
- 解析消息就是判断当前类能否为这个消息动态添加方法,例如为
@property
添加getter
和setter
方法,如果可以,就添加方法,处理消息;如果不能,就进行消息的“快速转发”,即替换接受者
- 这一步中可以通过
forwardingTargetForSelector
把这个消息转发给另一个对象,如果返回新的对象就让新的对象重新走一遍发送消息的流程;如果返回nil
,就进入“完整的消息转发”
- 通过消息生成方法签名,再通过
invokeWithTarget
选择处理对象并转发消息,否则抛出doesNotRecognizeSelector
异常
上面步骤中的1和2,是企图在内部解决问题;而3和4,是在类外部寻找其他适合的类来处理消息,这正是代理要做的工作。
优雅的委托代理(proxy for delegate)
简单来看,一个 DelegateProxy 有以下方法和属性:
初始化时传入委托者 delegate、遵循的协议 protocol 以及默认的返回值 returnValue,然后重写了消息转发的相关方法:
首先,DelegateProxy 本身并没有实现任何业务相关的方法,所以调用者如果需要知道委托人 delegate 是否实现了方法,对应的 respondsToSelector 实际上返回的是 delegate 的 respondsToSelector 返回值。另外当消息转发开始时,也就是上面谈到的第 3 步,如果 delegate 实现了方法,就 return delegate 转发给 delegate,否则 return self 由自己处理。
由自己处理时,需要实现完整的消息转发:
这里只是简单地处理了方法签名的缓存,并实现了转发,同时对方法默认的返回值进行了设置。值得注意的是,DelegateProxy 中通过一个字典(CFDictionaryRef 类型的 _signatures)对协议方法进行了缓存。返回字典的方法利用自旋锁保证了线程安全,并进行了空判断等操作,具体实现如下:
而具体的查找方法并建立缓存的方法实现如下:
可以看到实际上是遍历了协议中声明的方法,并把方法的方法名作为 key,方法签名作为 value 存入了字典中。
总结
一个优雅的委托代理,持有了委托者及其实现的协议,重写了 respondsToSelector 等方法,使得调用者的感受非常接近简单的代理模式。另外通过建立协议方法的方法缓存实现了被委托者方法的快速查找,并以此为依据确定了消息转发对象。基本上实现了我们最初想要的代码:
参考资料:
Loading...