Objective-C 如何用 Blocks 代替 Target-Action

代码放在GitHub
ELAutoSelector
CocoaPods 可用

pod 'ELAutoSelector', '~> 1.0.2'

要解决的问题

Objective-C 开发中经常会遇到带有 target 和 action 两个参数的方法,例如

- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

本文中统称这类方法为 Target-Action 方法。

Target-Action 方法调用时必须提供一个明确的接受者 target 和调用的方法 action,常常给开发工作带来不便,比如:

  • UIControl
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

此方法用于注册 UIControlEvent 事件回调。

一般使用此方法的情景是:先调用 UIControl 的 Target-Action 方法,发现需要在本类实现一个 action 方法;去类实现里写一个带有空实现的 action 方法;有了 action 会后再回来补全 Target-Action 方法的参数;之后去实现并调试 action 方法的实现。整个过程灭什么逻辑可言,使用体验很差

如果当前类不适合接受 action,(比如在类方法中创建的 UIControl 对象),需要找到合适的 target 和 action,而且这种写法会造成不不要的逻辑耦合。

action 方法只接受 0~2 个固定的参数,不能自定义参数,如果需要获取其他变量的值,必须将需要获取的值作为属性保存下来,不如 blocks 的自动捕获更加灵活。

这个方法不会产生对 target 的强引用,因此需要开发者自己维护 target 的生命周期。

  • NSTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

NSTimer 的这个方法通过 Target 和 Action 注册一个在 ti 时间后的回调。注册后 NSTimer 对象会强引用 target ,容易导致引用循环。因此需要仔细斟酌 NSTimer 的释放时机,防止产生内存泄漏,不过手动释放 NSTimer 让开发者仿佛回到了 MRC 时代。

尚不能落地的 Apple 的解决方案

苹果提供了一系列带有 block 的回调方法,但是要求 iOS 10.0+ 才可以使用,短期内还是用不了。

现实的需求

既然苹果的新方法用不上,就只自力更生了。最好的情况是能不用寻找合适的 Target;不用写 Action 方法,用 block 代替;不需要管理 Target 的生命周期;接口简单易用,不需要对原有方法做修改。

解决的思路

构造一个单例 TargetSelectorHelper 来负责作为 target 并保存要执行的 block,运行时将 block 转化为方法,并将方法对应的 SEL 作为 action。显式指定 block 的生命周期依赖者,管理 block 生命周期。

关键问题

  • block 怎样存储?
  • 如何将 block 转化为方法?
  • 怎样保证接口简洁易用?
  • block 回收机制怎样设计?

block 的存储

将传入的 block 连同相关信息放在一个对象内,将对象的内存地址与辅助字符拼装后作为储存 block 的 key。

调用 block 的方法签名也与 key 绑定。通过 NSSelectorFromString()函数将 key 转化成 SEL。

SEL 会被返回,用于作为 action 参数注册。

如果 key 出现碰撞,说明较早存储的 block 已经被释放,因此可以将新的 block 直接替换掉旧的,碰撞不会导致异常。

block 转化为方法

利用 Objective-C 强大的动态特性,使用objc/runtime.h中的函数 imp_implementationWithBlock将 block 转化为 IMP。

将 block 存储对应的key作为方法签名。在首次方法调用时通过+ (BOOL)resolveInstanceMethod:(SEL)sel拦截 SEL 并动态的将 block 转化成的 IMP 与方法绑定

接口简洁易用

block 的语法较复杂,并且需要写入到其他方法的参数内部,因此通过宏定义和函数等手段使项目接口易于调用。

  • Target:
#define ELTarget [ELAutoSelectorHelder shared]
  • Action:
SEL ELAction(ELAutoSelectorAction action, id _Nullable dependency)

block 回收机制的设计

如果开发者在项目中使用了 ELAutoSelectorELAutoSelector 会在每次调用的时候动态创建新方法,同一行代码被调用两次会创建两个不同的新方法作为 Action 的返回值。

block 捕获到的 __strong 类型的对象都会被 block 持有,这将导致这些对象无法被释放。如果不对 block 的生命周期做控制, block 捕获对象的生命周期会影响代码逻辑,使用 ELAutoSelector 有可能产生开发者所不期望的结果。

让 block 捕获声明为 __weak 类型的变量,可以解决这个问题。

除此之外,ELAutoSelector 还有生命周期依赖机制来控制 block 的回收。

SEL ELAction(ELAutoSelectorAction action, id _Nullable dependency)
是通过 block 创建动态方法的接口。其中的第一个参数 action 是希望被执行的 block, 第二个参数 dependency 是 block 的生命周期依赖者。每个 action 会和对应的 dependency 绑定。

如果 dependency 传入的是 nil,对应的 action 将不会被释放。

ELAutoSelector 不会强引用 dependency,它会每 0.5 秒左右轮询一遍所有的 block 对应的 dependency,如果 dependency 被释放了,ELAutoSelector 会用一个空实现替代掉 block,并释放 block。block 通过强引用捕获的变量也会被释放掉。如果在 block 释放后调用这个 block,不会执行任何动作(block 被空实现所替代),也不会引起崩溃,仅会调用 NSLog()打印一行警告信息通知开发者。

如果需要为一个 UIButton 添加 event 的响应事件,比较好的方法是调用 SEL ELAction(ELAutoSelectorAction action, id _Nullable dependency) 时将 dependency 指定为这个 button。需要注意的是: 如果 block 通过强引用捕获了 dependency 会导致内存泄漏。

关注微信订阅号:iOS开发笔记本

http://weixin.qq.com/r/Kji_plrEqwfUrR7F9204

关注知乎专栏:iOS开发学记笔记

https://zhuanlan.zhihu.com/iOSDevNote

你可能感兴趣的:(Objective-C 如何用 Blocks 代替 Target-Action)