欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS开发中,block与代理的对比,双方的优缺点及在什么样的环境下,优先使用哪一种更为合适?

程序员文章站 2022-04-27 10:19:01
1.block和代理的对比 2.双方的优缺点 3.什么样的环境下,优先使用哪一种?依据是什么? block 和 delegate 都可以通知外面。block 更轻型,使用...
1.block和代理的对比
2.双方的优缺点
3.什么样的环境下,优先使用哪一种?依据是什么?


block 和 delegate 都可以通知外面。block 更轻型,使用更简单,能够直接访问上下文,这样类中不需要存储临时数据,使用 block 的代码通常会在同一个地方,这样读代码也连贯。delegate 更重一些,需要实现接口,它的方法分离开来,很多时候需要存储一些临时数据,另外相关的代码会被分离到各处,没有 block 好读。

应该优先使用 block。而有两个情况可以考虑 delegate。

1. 有多个相关方法。假如每个方法都设置一个 block, 这样会更麻烦。而 delegate 让多个方法分成一组,只需要设置一次,就可以多次回调。当多于 3 个方法时就应该优先采用 delegate。

比如一个网络类,假如只有成功和失败两种情况,每个方法可以设计成单独 block。但假如存在多个方法,比如有成功、失败、缓存、https 验证,网络进度等等,这种情况下,delegate 就要比 block 要好。

在 swift 中,利用 enum, 多个方法也可以合并成一个 block 接口。swift 中的枚举根据情况不同,可以关联不同数据类型。而在 objc 就不建议这样做,objc 这种情况下,额外数据需要使用 NSObject 或者 字典进行强转,接口就不够安全。

2. 为了避免循环引用,也可以使用 delegate。使用 block 时稍微不注意就形成循环引用,导致对象释放不了。这种循环引用,一旦出现就比较难检查出来。而 delegate 的方法是分离开的,并不会引用上下文,因此会更安全些。

假如写一个库供他人使用,不清楚使用者的水平如何。这时为防止误用,宁愿麻烦一些,笨一些,使用 delegate 来替代 block。

将 block 简单分类,有三种情形。

* 临时性的,只用在栈当中,不会存储起来。
比如数组的 foreach 遍历,这个遍历用到的 block 是临时的,不会存储起来。

* 需要存储起来,但只会调用一次,或者有一个完成时期。
比如一个 UIView 的动画,动画完成之后,需要使用 block 通知外面,一旦调用 block 之后,这个 block 就可以删掉。

* 需要存储起来,可能会调用多次。
比如按钮的点击事件,假如采用 block 实现,这种 block 就需要长期存储,并且会调用多次。调用之后,block 也不可以删除,可能还有下一次按钮的点击。

对于临时性的,只在栈中使用的 block, 没有循环引用问题,block 会自动释放。而只调用一次的 block,需要看内部的实现,正确的实现应该是 block 调用之后,马上赋值为空,这样 block 也会释放,同样不会循环引用。

而多次调用时,block 需要长期存储,就很容易出现循环引用问题。

Cocoa 中的 API 设计也是这样的,临时性的,只会调用一次的,采用 block。而多次调用的,并不会使用 block。比如按钮事件,就使用 target-action。有些库将按钮事件从 target-action 封装成 block 接口, 反而容易出问题。


Block 和 Delegate 两种在 iOS 开发中都有广泛的应用,如果加上 NSNotification 的话就把三大 iOS 回调方式给凑齐了。我这里根据这三者来进行说明。

Delegation (代理)是一种很常见的回调方式,和 Singleton (一样),在大部分编程语言里都有实现方法。
Block 是一种苹果开发的基于 C 的调用方式 [1],在 iOS 和 Mac 开发上都有广泛的应用。Block 从某种程度上来说是一种很新的回调方式,苹果在2009年将其引入 Mac OS X 10.6,之后在2010年将其引入到 iOS 4.0。值得一提的是,同样是在 iOS 4.0 ,苹果引入了 ARC (Automatic Reference Counting),准确的说的话是 ARCLite , iOS 5 才正式加入了 ARC [2]。
NSNotification 这个作为苹果在 2001 年发布 Mac OS X 的时候就集成在 Foundation 库里面的类,一直作为一种全局回调通知的功能而存在。有趣的是,按照苹果官方文档对于 Delegation 的说明,在 Cocoa Framwroks 中,苹果的 Delegation 被表述为是一种单观察者通知的实现。[3]

三种回调方式,能共存于 Objective-C 之中,现在又集体被继承到了 Swift 里面,说明三者肯定是有不同和各自的优劣之处的。
下面我就我个人这些年的 iOS 开发了解,简单说一下三者的优劣。

Delegation 是一种很清晰回调形式,从 Protocol 的建立,到之后的引用,和对于 delegate 声明的变量处理,都非常具有条理。建立完 Delegation 之后,其他方法在调用的时候会很清晰整个回调的流程和处理。在处理一些延迟回调或者触发回掉的时候,声明调用的类里面的回调方法在编写时可以按照很独立的逻辑在制作。在使用 Delegation 的时候,一个回调方法可以对应多个不同的 Delegation ,这不仅简化了编程过程,也让能回调处理更加清晰。
Delegation 不好的地方在于,在类中调用 delegate 方法的时候,需要对 delegate 本体进行一定的验证核对,防止出现方法对象为空的情况,再一个就是受制于 performSelector 这个 selector 方法,回掉返回的参数被限制在了 NS 类的范围内,数量也很有限(当然可以用直接调用方法的形式在绕过,并不推荐;也可以用 Array 套着传, 不过这样需要有文档支持,不然不够清晰,回调方法也需要独立的验证,故也不推荐):

if (self.delegate && [self.delegate respondsToSelector:@selector(ABC:)]){
        [self.delegate performSelector:@selector(ABC:) withObject:@"ABC"];
}
//需要验证代理是否为空,同时验证代理方法是否存在
这个问题在 Swfit 中有了很不错的解决:
delegate?.ABC?(@"ABC")
至于说 Delegation 的另外一个问题,那就是使用了 Delegation 之后,代码阅读变成了一件很困难的事情,你需要在不同的 Class 文件中一次次的跳转来理解整个代码思路,要知道糟糕的 XCode 还经常会跳错方法(Command + 点击跳转,跳到了同名的其他方法),导致代码可读性的下降。要说的话,这一点在 Swift(2.2)版本中有一定的优化,不过 Swift 的 Selector 还在修正中,之后也许会在对于 Delegation 的可读性上做更多的优化。
最后一个比较核心的问题就在于,在一批变量声明了代理之后,在代理回掉被执行时,你是不太好知道这个变量究竟是这一批中的哪一个的。这也就是为什么苹果在制作 delgate 和 datasource 的时候都会在代理方法的返回值中带上声明代理类本身的原因。

Block 是一种很好用的回掉方式,其解决了 Delegation 在确认声明对象上的问题,由于 Block 回调方法一般都跟随在声明之后,所以可以很快确认回调来源,通过 __block 声明的变量也可以很方便的穿越到回调方法中进行使用,代码可读性上,一般来说使用 Block 的代码比使用 Delegation 的代码可读性更强(当然你也可以声明 Block 回调方法对应变量,然后按照和 Delegation 差不多的方法来调用他)。
Block 的缺陷主要就在于使用 ARC 情况下的循环引用。从某种程度上来说, Block 回调的方法内变量实际上是关联在声明 Block 的类里面的,但 Block 回调方法本身是在使用 Block 的类中的,同时使用类的 self 本身是可以在 Block 回调方法中被请求的,这里就会出现使用类 A 引用声明类 B ,B 在关联的 Block 中又引用了使用类 A ,这样一个循环之后引用可以无限循环下去,在这种无限循环的引用下, ARC 就无法知道 A 和 B 的确切弃用时间,所以 A 和 B 会在内存中永远存活下去,直到进程被消灭。所以,在很多时候,我们必须要使用 __weak 来声明一个 weakself ,来帮助 ARC 判断内存回收的时间。
这里要指出的是, Block 的回调方法也是可以复用,创建回调方法这一套东西在 Objective-C 中是继承于 C 样式的,一般来说,这个东西是没人用。[4]
void (^simpleBlock)(id responseObject) = ^(id responseObject){
        NSLog(@"This is a block");
    };
//这里创建了 simpleBlock 之后就可以像 Delegation 回调方法那样在各种回调声明中使用了

NSNotification 本身是一个非常强大的回调,因为他的回调是全局而且一对多的,广播的特性使得很多时候使用 NSNotification 能一次解决大面积的问题。同时,由于 NSNotification 是一套很古老的回调机制,在一些时候,当我们需要对 iOS Framework 做一些特殊处理的时候,会用到一些获取隐藏 NSNotification 的方法,这个在之前权限不完善的时候,企业开发中使用很广泛。记得当年 iOS 7 Tech Talk 上海站,我带着代码去找苹果的人问一个问题的时候,他们就查了之后文档给了我一个隐藏的 NSNotification 名,帮我解决了问题。(这个通知之后开放给开发者了,记得应该是一个键盘的通知)
至于说 NSNotification,就和上面说到的一样,也主要是在于他的全局广播属性和权限机制。
在使用 NSNotification 的时候,注册完回调方法之后,开发者需要自行对注册进行取消操作。比如说你注册一个 A 方法到 B 通知,由于某些原因 A 方法所在的类被 ARC 回收掉了,那么在 B 通知被触发的时候,由于 A 的通知注册信息还在,就会出现调用了非法的内存地址的情况。我曾经遇到了一次,应该是在调用 Jianting Zhu 还是 Jim Liu 的 WeiboSDK 的时候,记得那一版 SDK 特别奇葩,所有回调都是通过 NSNotification 来进行的,一开始用的时候不太懂,各种注册回调方法,然后发现 App 跑起来会莫名的闪退,那时候也不太懂,只能硬着头皮把代码都改成代理的形式。后来在做键盘的时候碰到了类似的报错,花时间研究了之后才发现是通知回调了已经被回收的类里面的方法,这时候才知道要如果通知没有注销会带来调用上的问题。
至于说 NSNotification 的权限问题,对于写类的人来说是比较头疼的,很多时候只能靠文档和调用者的自觉,出于权限考虑,如果不是特别需求全局广播这个特性,一般不太建议使用 NSNotification。
NSNotification 由于自身的限制,在回调可以传递的内容上也存在数量和内容的限制,虽然可以通过 Array 的方法绕过,但这样在代码可读性就会有折扣,对于文档也需要有要求,回调方法中还需要验证。

那么,在何种情况下使用上面三者呢?

对于初级的开发人员来说,关键在于使用习惯。如果你从其他语言转到 Objective-C 或者 Swift ,相信 Delegation 肯定让你觉得更加亲切,那么在初级阶段请使用好这个语法糖,多用,多去理解;如果你用着 AFNetworking 看着其他老前辈的说法用 Block 觉得效率很高很开心,那就开心的用,直到你被循环引用烦到了为止(笑);如果你用 NSNotification ……你还是别用了。然后,在你代码写多了之后,你可以开始尝试接触其他回调方式,去感受这些回调方式的不同。

对于中级的开发人员,关键在于对于回调流程的理解。
你要知道你的回调是一个什么性质的回调,如果这个回调是一个不定期触发,或者会多次触发的,那么 Delegation 应该更适合;如果这个回调是一个一次性的,并且和调用方法是单线性关系的,那么 Block 应该更适合;如果这个回调是广播性质的,需要很多个不同的类都接收到,那么 NSNotification 更适合。
在不同的执行线(不是线程),不同的执行次数、执行数量上的区别,是鉴别使用哪一种回调的最好判断方法。
对于 Block 来说,他的执行线应该是和调用方法、回调方法连续在一起的;对于 Delegation 和 NSNotification 来说,他的执行线可以是连续的,也可以是调用方法和回调方法之间有很长的间隔,或者说回调方法在执行线上会多次出现。

对于高级的开发人员……你们要是不懂上面那些就不要做高级开发了,你们应该去研究线程、 GCD 、 NSOperation 这些玩意。