最近在开发过程中总结了一些关于异步回调比较有意思的用法,给大家分享一下。不是什么高级东西,iOS老司机们看完可以给小弟指点指点,但是也不适合新手看,如果你还不太熟悉OC中的block或者Swift中的逃逸闭包,那么这篇分享你看着可能会很迷糊。文中代码部分使用Swift,OC可以照搬,思路是一样的。
在iOS开发中,block或者闭包(后面我就统称为闭包了,毕竟我已经投入了Swift的怀抱)可以说无处不在,在Swift中我们使用闭包可以用来做数组排序类似的功能,把元素大小比较的实现通过闭包的形式抛出来。但更多时候,我们用闭包来实现异步回调,因此我们经常要跟逃逸闭包打交道。
举一个很简单的例子,图片下载,大家可能最熟悉的框架就是SDWebImageDownloader,大家经常会在tableViewCell里去异步下载图片,下载完成后在回调里把图片显示出来。
这个过程看似很简单,无非就是先找本地有没有图片,有就直接回调,没有就创建一个http请求去下载,下载完成后回调,当然你就这样去实现,没有错,而且应用能正常运行,大部分时候也不会有什么问题。但是我们来细想一下这个过程到底有没有毛病。
获取图片的回调,可能是同步的(本地有,直接回调),也可能是异步的(本地没有,要去下载),同步的当然是不会出任何问题的,但是异步的就不一定了。
既然图片本地没有要去下载,那么就有可能有好几个cell需要的图片下载地址一样,或者cell被重用后又下载一次,不做任何处理的话,那么同一张图片可能我们得下载好几次,这样就浪费流量浪费空间了。不过好在像SD这样的第三方库,已经为你做了防止重复下载的事情,只要你下载的地址相同,就只会下载一次,并且不会漏掉你的任何一个回调,你可以放心的使用。
但是,我们还可以做得更极致,你有没有考虑过,网络不好的时候,一张图片要下半分钟的情况?当用户打开你的应用,这么多的图片在转圈,半天都停不下来,是不是难免会暴躁,然后明明网速就很慢下图要下半天还非要手痒痒去上下滑一下你的tabletView,滑一下还不解气,来回滑个几十下,差点没把手机给摔了。
这个时候tableViewCell的重用机制就把你带坑里去了,当你把一个cell滑出屏幕范围再滑回屏幕范围的时候,cell又去执行了一次获取图片的方法,图片还是没下载完,于是乎这个cell又创建了一个回调,当你来回滑了几十下后,一个cell可能就创建了几十个回调等待下载图片完成后好执行。
然后突然一下你的网速恢复了,那些个图片一下子就下好了,cell的几十次回调就能执行了,你不觉得很浪费性能吗?我一个cell明明就显示一张图,就因为网速慢加上我手痒滑来滑去的就得执行几十次绘制图片。而且你怎么知道你哪张图片先下完?除非下载图片用的串行队列,不然很可能你的cell最终显示的图片不是它应该显示的图片。当然你可以在回调的参数中多加一个url,回调的时候判断一下当前需要显示的url和回调中的url是不是相等,如果不相等说明这个回调是cell重用之前创建的,直接忽略掉就行了。这种方法也可以解决问题,但是我还想看看闭包可以怎么玩,从新手开始接手项目,踩过太多异步回调的坑了,下面来说说我最近想到的新的解决办法(其实我也还是个萌新),放心,绝对简单到轻描淡写。
第一步,我们要解决异步任务重复的问题,有人要问了,SD已经有这个机制了啊,废话,我用SD的库我第二步怎么实现,而且自己实现一下也是有好处的嘛,毕竟这个真的不难啊。
稍稍改变下代码,代码不好截图,分两部分截。
如果图片本地不存在,那么就需要下载,下载完成后就需要异步回调,我们可以把url保存在一个集合里表示这个url地址下的图片正在下载,把回调已可变数组的形式保存在一个字典里,以url作为key。每当又有一次下载该url的任务,就可以不用再下载一次,只需要把回调添加到该url对应的回调数组中就行了。当下载完成时,根据url找到对应的回调数组遍历执行一次就可以了。是不是很简单,对啊,就是这个简单啊,没有什么高大上的技术。
第二步,我们要解决cell重用的问题,也就是回调重复多余的问题,这个问题理解起来有点困难,需要你对block或者逃逸闭包很熟悉。
很多时候我们只知道调用一个函数,定义好回调需要执行什么任务,就什么都不管了,等着函数去调用我定义好的回调任务就行了,很少去关注闭包在函数本身中是怎么执行的,包括闭包的生命周期是怎么样的。
对于Swift来说,涉及到异步执行的闭包必须声明为逃逸闭包。普通闭包只能同步执行,他们的生命周期不一样(OC中的block不做这样的区分,可以统一看做逃逸闭包)。普通闭包的生命周期随着函数结束而结束,也就是说你没法写一个异步任务去执行普通闭包,也没法把普通闭包保存到其他地方去想什么时候执行就什么时候执行,说形象点,普通闭包像是一个被剥夺了自由的奴隶,它没法从函数的生命周期中逃逸出来。而逃逸闭包就像是一个上流社会的达官贵人,想去哪里就去哪里,只有当没有任何一个地方需要他的时候,他的生命周期才结束,所以你可以把逃逸闭包保存起来看情况使用。
逃逸闭包有一个缺点就是,如果我把它保存在一个数组中,我就再也不能精确地定位到它,更无法准确地删除它。它像是一个对象又不完全是一个对象,它天生的值拷贝属性加上无法进行哈希匹配,导致我们无法在数组中对其进行定位,最简单的办法就是把逃逸闭包封装在一个类中,通过对象去定位逃逸闭包。
回到刚刚那个问题,当cell消失后等待被重用时,怎么把它创建的逃逸闭包给释放掉?在第一步中,我把逃逸闭包以可变数组的形式保存在字典中,上面我们也分析了,我们没法在数组中去定位一个逃逸闭包把它删掉,因此,我们需要定义一个类,把逃逸闭包封装起来,再存在数组里,这样我们就可以释放指定的逃逸闭包了。最终的代码就是下面这个样子。
总结,很多语言都支持闭包这种表达式,但是OC或者Swift的闭包应该是目前所有语言的闭包实现中最好用的,你可以在闭包中随意操作上下文变量,而不需要去担心上下文变量的生命周期,你只需要注意循环引用的问题就行了,而很多其他语言的闭包是不能捕获上下文变量的。闭包在异步任务中使用非常广泛,你可以把闭包保存起来,这感觉就像我把一串代码实现给保存了起来,待异步任务完成时看情况去调用或者释放闭包,如果你需要定位某一个保存起来的闭包,你需要把闭包封装在类中使用。
第一次写东西,瞎写的,有错误的地方望指正。