非逃逸闭包,OC中的block与Swift中的尾随闭包的使

最近在开发过程中总结了一些关于异步回调比较有意思的用法,给大家分享一下。不是什么高级东西,iOS老司机们看完可以给小弟指点指点,但是也不适合新手看,如果你还不太熟悉OC中的block或者Swift中的逃逸闭包,那么这篇分享你看着可能会很迷糊。文中代码部分使用Swift,OC可以照搬,思路是一样的。

何为闭包?

Swift中的闭包和OC中的block很相似(其实也有其他语言有闭包的概念)。所谓闭包,就是可以捕获其当前所在上下文的常量和变量的代码块。

很多刚开始写Swift的同学或许已经把闭包应用在很多地方了,也总是会把闭包跟OC中的block划等号,的确Swift中的的闭包跟OC中的block有很多相似之处,但是Swift毕竟是一门新的语言,出于性能、内存优化等原因的考虑,Swift比起OC而言在很多地方是有区别。以闭包来说,Swift 的闭包分为 逃逸非逃逸 两种。

图片 1

在iOS开发中,block或者闭包(后面我就统称为闭包了,毕竟我已经投入了Swift的怀抱)可以说无处不在,在Swift中我们使用闭包可以用来做数组排序类似的功能,把元素大小比较的实现通过闭包的形式抛出来。但更多时候,我们用闭包来实现异步回调,因此我们经常要跟逃逸闭包打交道。

闭包表达式的语法:

可以看到,首先整个闭包表达式是包裹在花括号内{}的,然后是表明该闭包“类型”的参数列表和闭包返回值类型(是的,这和函数的参数和返回值的类型共同决定其函数的类型一样)。然后以关键字in开启闭包内代码实现部分,为了看起来清爽点,此时一般要换行写闭包内代码。

        { (参数列表) -> 返回值类型 in
            闭包内代码
        }

** 1、闭包的变量/常量:**
那我们来定义一个闭包:定义了一个可以接收两个String类型参数,返回值为Bool类型,名叫sortedBlock的闭包。我们用它来自己定义实现数组排序的规则str1<str2(升序),然后以该闭包为参数传入Swift数组库的sort(by:)方法,sort(by:)方法自动会以该规则为数组排序。
可以看到,我们完全可以把闭包看作一个普通的常量或者变量。

        let sortedBlock: (String, String)->Bool
        sortedBlock = {(str1: String, str2: String)->Bool in
                return str1<str2
        }
        let resultNamesBlcok = names.sorted(by: sortedBlock)
        print("resultNamesBlcok=(resultNamesBlcok)")

2、 内联闭包:
除了可以将闭包定义成变量/常量,还可以省去名字将其嵌入方法中,即“内联闭包”。

图片 2

屏幕快照 2016-12-22 下午5.29.51.png

        let resultNames2 = names.sorted(by: {(s1: String, s2: String) ->Bool in
            return s1 < s2
        })
        print("resultNames2=(resultNames2)")

** 3、可以推断类型:**
因为排序闭包函数是作为sorted(by:) 方法的参数传入的,Swift自己可以推断出参数和返回值类型,因此我们可以不用写。

        let resultNames3 = names.sorted(by: {(s1, s2) in // 括号也可不写
            return s1<s2
        })
        print("resultNames3=(resultNames3)")

** 4、单表达式闭包隐式返回:**
因为该闭包代码只有一个表达式s1<s2,当只有一个表达式时,我们可以省略return关键字。

        let resultNames4 = names.sorted { (s1, s2) in s1<s2}
        print("resultNames4=(resultNames4)")

5、内联闭包参数名称缩写:
Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0,$1,$2 来顺序调用闭包的参数,以此类推。

        let resultNames5 = names.sorted(by: { $0<$1 })
        print("resultNames5=(resultNames5)")

6、尾随闭包:
可以说尾随闭包没什么特殊的功能,它的发明只是为了提高代码可读性。即,当闭包表达式是某个方法的最后一个参数时,可以将闭包从参数列表中移出来放到方法末尾,并省略参数标签(从(param:{})变为(){})。

        // 标准写法
        let resultNames1 = names.sorted(by:{(s1, s2)->Bool in
            return s1<s2
        })      
        // 尾随写法
        let resultNames2 = names.sorted(){(s1, s2)->Bool in
            return s1<s2
        }

** 7、逃逸闭包:**
当闭包作为参数传入方法时,若该闭包是直到该方法返回后才执行闭包,我们就说该闭包从该方法中逃逸了,它是个逃逸闭包。常见的,我们在请求网络时常以闭包为参数用以执行网络请求回调。请求网络的方法马上会执行完,但是请求的结果回调要后面才执行,即在方法的生命周期后。在Swift3中,闭包默认是非逃逸的,如果是逃逸的闭包需要显式地以关键字@escaping标注。

    func testFunction1(testBlock:@escaping ()->Void) {
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() 2){
            testBlock()
        }
        print("----in funtion")
    }

8、自动闭包:

        var stuNames = ["wang1", "wang2", "wang3"]
        let stuRemoveHandle = {()->String in
            stuNames.remove(at: 0)
        }

        let removedName = stuRemoveHandle()
        print(removedName)

9、定义闭包类型别名:

        typealias AddBlock = (Int, Int)->String
        let addBlock: AddBlock = {(num1, num2) in
            return "num1 num2=(num1 num2)"
        }
        let addedNum = addBlock(3,5)
        print(addedNum)
逃逸闭包

概念:一个接受闭包作为参数的函数,逃逸闭包会在函数返回之后才被调用,也就是说闭包逃离了函数的作用域场景举例:网络请求请求结束后才调用的闭包,因为发起请求后过了一段时间后这个闭包才执行,并不一定是在函数作用域内执行

func requestData() -> () { CCNetWorkTool.sharedInstance.request(method: .POST, URLString: API_TEST parameters: nil) {[weak self] (response, isSuccess) in // 请求失败 if isSuccess == false { // 失败处理 return } // 请求成功 // ...... }

girl.png

举一个很简单的例子,图片下载,大家可能最熟悉的框架就是SDWebImageDownloader,大家经常会在tableViewCell里去异步下载图片,下载完成后在回调里把图片显示出来。

闭包的循环强引用:

和OC中block可能会出现循环强引用一样,Swift也会有这个问题。出现循环强引用的原因现在不必多讲。主要可以看看在Swift中是怎样处理循环强引用的:Swift中闭包的简单使用

闭包出现循环强引用一般是因为,首先闭包作为某类实例的属性,此时闭包被实例所引用;而往往在闭包里you调用该实例self,即实例被闭包所引用。两者彼此引用,任何一方释放的前提是对方先释放引用。这样的话两者便陷入了僵局,互不能释放。
解决方法是我们可以将某一方申明为弱引用weak,打破强引用僵局。这个被weak申明为弱引用的一般来说属于生命周期较短的一方。若两者生命周期一样,不能分辨谁的命较短时,可以将闭包内捕获引用的实例用unowned关键字申明为“**无主引用”。

注意:weak修饰的对象如果发现被销毁,那么指向该对象的指针会立即指向nil,因此在Swift中来说,被weak关键字修饰后其对象有可能为nil,即是可选类型了。所以,下面代码中weakSelf后面要加?

        weak var weakSelf = self
        printString { (text) in
            print(text)
            //闭包中铺捕获了self
            weakSelf?.view.backgroundColor = UIColor.red
        }
非逃逸闭包

概念:一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用注意:关于非逃逸的闭包有一个默认规则:除了作为函数的即时参数传入的闭包是非逃逸的,其他类型的都是逃逸的。

场景举例:我们常用的masonry或者snapkit的添加约束的方法就是非逃逸的,创建完子控件,然后添加到父视图,紧接着就是需要把约束设置好,这种情况不需要再等什么条件满足后再来回调,所以就要求闭包马上执行。

class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let orangeView = UIView() orangeView.backgroundColor = UIColor.orange view.addSubview(orangeView) orangeView.snp.makeConstraints {  in make.topMargin.equalTo make.leftMargin.equalTo make.rightMargin.equalTo make.bottomMargin.equalTo make.center.equalTo(view.center) } }}

其实是为了管理内存。闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用(其实当前对象的属性、函数隐形带了一个self参数,一旦调用他们,隐形中就使用了self),这样闭包会持有当前对象,容易导致循环引用。

非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可以保证在函数结束时闭包会释放它捕获的所有对象;使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留和释放的调用;此外非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上。

综上所述,如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包

Swift3.0之前闭包作为即时函数的参数默认为逃逸的(@escaping),但是很多开发者在开发中总是忽略去判断闭包是否为逃逸,这样就都被当做了逃逸闭包处理,对闭包的内存管理优化不太友好。Swift3.0中做出调整,所有作为即时函数的参数的闭包默认是非逃逸( @noescape)。如果开发者想使闭包具有逃逸性,需要用@escaping修饰闭包,@escaping 标识符还有警示开发者的作用,警示开发者这是一个逃逸闭包,注意循环引用问题,比如下面的代码

图片 3逃逸闭包1.png

以下是几个GCD的API用到了逃逸闭包

public func __dispatch_after(_ when: dispatch_time_t, _ queue: DispatchQueue, _ block: @escaping () -> Swift.Void)public func __dispatch_barrier_async(_ queue: DispatchQueue, _ block: @escaping () -> Swift.Void)
注意

可选型的闭包总是逃逸的:更令人惊讶的是,即便闭包被用作参数,但是当闭包被包裹在其他类型(例如元组、枚举的 case 以及可选型)中的时候,闭包仍旧是逃逸的。由于在这种情况下闭包不再是即时的参数,它会自动变成逃逸闭包。

参考资料:

  OC中的block与Swift中的尾随闭包都起到了将参数作为返回值的作用,也就是常说的回调。

图片 4获取图片的简易实现图片 5Cell的简易实现

1. block的使用

  在OC中block主要分为三种,分别是
(1)_NSConcreteGlobalBlock 全局静态,
(2)_NSConcreteStackBlock 保存在栈中,出函数作用域就销毁,
(3)_NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁。
  而我们在使用block时大致分为以下几步:

//1.使用typedef定义一个block
typedef void(^CallBack1)();  //不带参数  
typedef void(^CallBack2)(NSString *test); //带参数

//2.通过属性声明
@property (nonatomic, copy) CallBack callBack;

//3.通过函数方法声明
- (void)functionCallBack:(CallBack)callBack;

  其实呢,如果写的比较熟练了,也可以连着一起定义:

@property (nonatomic, strong) void(^ completed1)();//不带参数
@property (nonatomic, strong) void(^ completed2)(NSString *test);//带参数

- (void) functionCallBack:(void(^)())completed1;//不带参数
- (void) functionCallBack:(void(^)(NSString *test))completed2;//带参数

这个过程看似很简单,无非就是先找本地有没有图片,有就直接回调,没有就创建一个http请求去下载,下载完成后回调,当然你就这样去实现,没有错,而且应用能正常运行,大部分时候也不会有什么问题。但是我们来细想一下这个过程到底有没有毛病。

2. 尾随闭包的使用

  在Swift中,闭包是一段自包含的函数代码块,可以在代码中使用和传递,相当于一个匿名函数。
  那么尾随闭包就是这个函数的最后一个参数是一个闭包,所以规定这个闭包既可以写在函数的参数括号里面,也可以直接放在最后面来使用,就像重新给这个函数定义了一次一样。
  并且,它的写法和block很相似:

//1.使用typealias定义
typealias functionBlock1 = () -> ()//不带参数
typealias functionBlock2 = (String) -> ()//带参数

//2.声明函数体
func blockTest1(complete: (functionBlock1)) -> () {
        complete()
}

func blockTest2(complete: (functionBlock2)) -> () {
        let re: String = "Cookie"
        complete(re)
}

//3.使用函数
blockTest1 {}
blockTest2{ (result) in
         print(result)
}

  同样的,如果写的比较熟练了,也可以连着一起定义:

//1.声明函数体
func blockTest(complete: (_ result: String)->()) -> () {

        let re: String = "Cookie"
        complete(re)
}

//2.使用函数
blockTest { (result) in

            print(result)
}

  除此之外,swift中的闭包还包括逃逸闭包,这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸,多用来做函数回调,与Objective-C里的Block有异曲同工之妙。自动闭包,作为参数传递给函数时,可以将闭包定义为自动闭包(使用关键字@autoclosure),这样传递参数时,可以直接传递一段代码(或者一个变量、表达式),系统会自动将这段代码转化成闭包。

获取图片的回调,可能是同步的,也可能是异步的(本地没有,要去下载),同步的当然是不会出任何问题的,但是异步的就不一定了。

既然图片本地没有要去下载,那么就有可能有好几个cell需要的图片下载地址一样,或者cell被重用后又下载一次,不做任何处理的话,那么同一张图片可能我们得下载好几次,这样就浪费流量浪费空间了。不过好在像SD这样的第三方库,已经为你做了防止重复下载的事情,只要你下载的地址相同,就只会下载一次,并且不会漏掉你的任何一个回调,你可以放心的使用。

但是,我们还可以做得更极致,你有没有考虑过,网络不好的时候,一张图片要下半分钟的情况?当用户打开你的应用,这么多的图片在转圈,半天都停不下来,是不是难免会暴躁,然后明明网速就很慢下图要下半天还非要手痒痒去上下滑一下你的tabletView,滑一下还不解气,来回滑个几十下,差点没把手机给摔了。

这个时候tableViewCell的重用机制就把你带坑里去了,当你把一个cell滑出屏幕范围再滑回屏幕范围的时候,cell又去执行了一次获取图片的方法,图片还是没下载完,于是乎这个cell又创建了一个回调,当你来回滑了几十下后,一个cell可能就创建了几十个回调等待下载图片完成后好执行。

然后突然一下你的网速恢复了,那些个图片一下子就下好了,cell的几十次回调就能执行了,你不觉得很浪费性能吗?我一个cell明明就显示一张图,就因为网速慢加上我手痒滑来滑去的就得执行几十次绘制图片。而且你怎么知道你哪张图片先下完?除非下载图片用的串行队列,不然很可能你的cell最终显示的图片不是它应该显示的图片。当然你可以在回调的参数中多加一个url,回调的时候判断一下当前需要显示的url和回调中的url是不是相等,如果不相等说明这个回调是cell重用之前创建的,直接忽略掉就行了。这种方法也可以解决问题,但是我还想看看闭包可以怎么玩,从新手开始接手项目,踩过太多异步回调的坑了,下面来说说我最近想到的新的解决办法(其实我也还是个萌新),放心,绝对简单到轻描淡写。

第一步,我们要解决异步任务重复的问题,有人要问了,SD已经有这个机制了啊,废话,我用SD的库我第二步怎么实现,而且自己实现一下也是有好处的嘛,毕竟这个真的不难啊。

稍稍改变下代码,代码不好截图,分两部分截。

图片 6第一部分图片 7第二部分

如果图片本地不存在,那么就需要下载,下载完成后就需要异步回调,我们可以把url保存在一个集合里表示这个url地址下的图片正在下载,把回调已可变数组的形式保存在一个字典里,以url作为key。每当又有一次下载该url的任务,就可以不用再下载一次,只需要把回调添加到该url对应的回调数组中就行了。当下载完成时,根据url找到对应的回调数组遍历执行一次就可以了。是不是很简单,对啊,就是这个简单啊,没有什么高大上的技术。

第二步,我们要解决cell重用的问题,也就是回调重复多余的问题,这个问题理解起来有点困难,需要你对block或者逃逸闭包很熟悉。

很多时候我们只知道调用一个函数,定义好回调需要执行什么任务,就什么都不管了,等着函数去调用我定义好的回调任务就行了,很少去关注闭包在函数本身中是怎么执行的,包括闭包的生命周期是怎么样的。

对于Swift来说,涉及到异步执行的闭包必须声明为逃逸闭包。普通闭包只能同步执行,他们的生命周期不一样(OC中的block不做这样的区分,可以统一看做逃逸闭包)。普通闭包的生命周期随着函数结束而结束,也就是说你没法写一个异步任务去执行普通闭包,也没法把普通闭包保存到其他地方去想什么时候执行就什么时候执行,说形象点,普通闭包像是一个被剥夺了自由的奴隶,它没法从函数的生命周期中逃逸出来。而逃逸闭包就像是一个上流社会的达官贵人,想去哪里就去哪里,只有当没有任何一个地方需要他的时候,他的生命周期才结束,所以你可以把逃逸闭包保存起来看情况使用。

逃逸闭包有一个缺点就是,如果我把它保存在一个数组中,我就再也不能精确地定位到它,更无法准确地删除它。它像是一个对象又不完全是一个对象,它天生的值拷贝属性加上无法进行哈希匹配,导致我们无法在数组中对其进行定位,最简单的办法就是把逃逸闭包封装在一个类中,通过对象去定位逃逸闭包。

回到刚刚那个问题,当cell消失后等待被重用时,怎么把它创建的逃逸闭包给释放掉?在第一步中,我把逃逸闭包以可变数组的形式保存在字典中,上面我们也分析了,我们没法在数组中去定位一个逃逸闭包把它删掉,因此,我们需要定义一个类,把逃逸闭包封装起来,再存在数组里,这样我们就可以释放指定的逃逸闭包了。最终的代码就是下面这个样子。

图片 8第一部分图片 9第二部分

总结,很多语言都支持闭包这种表达式,但是OC或者Swift的闭包应该是目前所有语言的闭包实现中最好用的,你可以在闭包中随意操作上下文变量,而不需要去担心上下文变量的生命周期,你只需要注意循环引用的问题就行了,而很多其他语言的闭包是不能捕获上下文变量的。闭包在异步任务中使用非常广泛,你可以把闭包保存起来,这感觉就像我把一串代码实现给保存了起来,待异步任务完成时看情况去调用或者释放闭包,如果你需要定位某一个保存起来的闭包,你需要把闭包封装在类中使用。

第一次写东西,瞎写的,有错误的地方望指正。

本文由星彩网app下载发布于计算机编程,转载请注明出处:非逃逸闭包,OC中的block与Swift中的尾随闭包的使

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。