14 协议、闭包和懒加载

1. 协议

1.1 协议的格式

  • 协议的定义方式与类,结构体,枚举的定义都非常相似
protocol SomeProtocol {
    // 协议方法
}
  • 遵守协议的格式
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 类的内容
    // 实现协议中的方法
}

1.2 协议的基本使用

  • 定义协议和遵守协议
// 1.定义协议
protocol SportProtocol {
    func playBasketball()
    func playFootball()
}

// 2.遵守协议
// 注意:默认情况下在swift中所有的协议方法都是必须实现的,如果不实现,则编译器会报错
class Person : SportProtocol {
    var name : String?
    var age : Int = 0

    // 实现协议中的方法
    func playBasketball() {
        print("人在打篮球")
    }

    func playFootball() {
        print("人在踢足球")
    }
}
  • 协议之间的继承
protocol CrazySportProtocol {
    func jumping()
}

protocol SportProtocol : CrazySportProtocol {
    func playBasketball()
    func playFootball()
}

1.3 代理设计模式

  • 协议继承用于代理设计模式
protocol BuyTicketProtocol {
    func buyTicket()
}

class Person {
    // 1.定义协议属性
    var delegate : BuyTicketProtocol

    // 2.自定义构造函数
    init (delegate : BuyTicketProtocol) {
        self.delegate = delegate
    }

    // 3.行为
    func goToBeijing() {
        delegate.buyTicket()
    }
}


class HuangNiu: BuyTicketProtocol {
    func buyTicket() {
        print("买了一张火车票")
    }
}

let p = Person(delegate: HuangNiu())
p.goToBeijing()

1.4 协议中方法的可选

// 1.定义协议
@objc
protocol SportProtocol {
    func playBasketball()

    optional func playFootball()
}

// 2.遵守协议
class Person : SportProtocol {
    var name : String?
    var age : Int = 0

    // 实现协议中的方法
    @objc func playBasketball() {
        print("人在打篮球")
    }
}

2. 闭包

2.1 闭包的介绍

  • 闭包和OC中的block非常相似
    • OC中的block是匿名的函数
    • Swift中的闭包是一个特殊的函数
    • block和闭包都经常用于回调
  • 注意:闭包和block一样,第一次使用时可能不习惯它的语法,可以先使用简单的闭包,随着学习的深入,慢慢掌握其灵活的运用方法.

2.2 闭包的使用

block的用法回顾

  • 定义网络请求的类
@interface HttpTool : NSObject
- (void)loadRequest:(void (^)())callBackBlock;
@end

@implementation HttpTool
- (void)loadRequest:(void (^)())callBackBlock
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"加载网络数据:%@", [NSThread currentThread]);

        dispatch_async(dispatch_get_main_queue(), ^{
            callBackBlock();
        });
    });
}
@end
  • 进行网络请求,请求到数据后利用block进行回调
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.httpTool loadRequest:^{
        NSLog(@"主线程中,将数据回调.%@", [NSThread currentThread]);
    }];
}
  • block写法总结:
    • 类型:
返回值(^block的名称)(block的参数)

^(参数列表) {
   // 执行的代码
};

使用闭包代替block

  • 定义网络请求的类
class HttpTool: NSObject {

    func loadRequest(callBack : ()->()){
        dispatch_async(dispatch_get_global_queue(0, 0)) { () -> Void in
            print("加载数据", [NSThread.currentThread()])

             dispatch_async(dispatch_get_main_queue(), { () -> Void in
                callBack()
             })
        }
    }
}
  • 进行网络请求,请求到数据后利用闭包进行回调
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        // 网络请求
        httpTool.loadRequest ({ () -> () in
            print("回到主线程", NSThread.currentThread());
        })
    }
  • 闭包写法总结:
    • 技巧:初学者定义闭包类型,直接写()->().再填充参数和返回值
类型:(形参列表)->(返回值)
    
{
    (形参) -> 返回值类型 in
    // 执行代码
}

闭包的简写

  • 如果闭包没有参数,没有返回值.in和in之前的内容可以省略
 httpTool.loadRequest({
     print("回到主线程", NSThread.currentThread());
 })
  • 尾随闭包写法:
    • 如果闭包是函数的最后一个参数,则可以将闭包写在()后面
    • 如果函数只有一个参数,并且这个参数是闭包,那么()可以不写
httpTool.loadRequest() {
    print("回到主线程", NSThread.currentThread());
}
// 开发中建议该写法
httpTool.loadRequest {
    print("回到主线程", NSThread.currentThread());
}

2.3 闭包的循环引用

  • 如果在HttpTool中有对闭包进行强引用,则会形成循环引用
  • 补充:在Swift中检测一个对象是否销毁,可以实现对象的deinit函数
 // 析构函数(相当于OC中dealloc方法)
 deinit {
     print("ViewController----deinit")
 }
  • 循环引用的(实现)
    • 该实现是为了产生循环引用
class HttpTool: NSObject {

    // 定义属性,来强引用传入的闭包
    var callBack : (()->())?

    func loadRequest(callBack : ()->()){
        dispatch_async(dispatch_get_global_queue(0, 0)) { () -> Void in
            print("加载数据", [NSThread.currentThread()])

             dispatch_async(dispatch_get_main_queue(), { () -> Void in
                callBack()
             })
        }

        self.callBack = callBack
    }
}
  • swift中解决循环引用的方式
  • 方案一:
    • 使用weak,对当前控制器使用弱引用
    • 但是因为self可能有值也可能没有值,因此weakSelf是一个可选类型,在真正使用时可以对其强制解包(该处强制解包没有问题,因为控制器一定存在,否则无法调用所在函数)
// 解决方案一:
weak var weakSelf = self
httpTool.loadData {
    print("加载数据完成,更新界面:", NSThread.currentThread())
    weakSelf!.view.backgroundColor = UIColor.redColor()
}
  • 方案二:
    • 和方案一类型,只是书写方式更加简单
    • 可以写在闭包中,并且在闭包中用到的self都是弱引用
httpTool.loadData {[weak self] () -> () in
     print("加载数据完成,更新界面:", NSThread.currentThread())
     self!.view.backgroundColor = UIColor.redColor()
}
  • 方案三:(常用)
    • 使用关键字unowned
    • 从行为上来说 unowned 更像OC中的 unsafe_unretained
    • unowned 表示:即使它原来引用的对象被释放了,仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil
httpTool.loadData {[unowned self] () -> () in
    print("加载数据完成,更新界面:", NSThread.currentThread())
    self.view.backgroundColor = UIColor.redColor()
}

3. 懒加载

3.1 懒加载的介绍

  • swift中也有懒加载的方式
    • (苹果的设计思想:希望所有的对象在使用时才真正加载到内存中)
  • 和OC不同的是swift有专门的关键字来实现懒加载
  • lazy关键字可以用于定义某一个属性懒加载

3.2 懒加载的使用

  • 格式
lazy var 变量: 类型 = { 创建变量代码 }()
  • 懒加载的使用
// 懒加载的本质是,在第一次使用的时候执行闭包,将闭包的返回值赋值给属性
// lazy的作用是只会赋值一次
lazy var array : [String] = {
    () -> [String] in
    return ["why", "lmj", "lnj"]
}()

4. 非逃逸闭包与逃逸闭包理解(难点)

从Swift3开始,传递闭包到函数中的时候,系统默认为非逃逸闭包(Nonescaping Closures),有非逃逸闭包必然就有逃逸闭包(Escaping Closures),下面分析一下两者的区别。

4.1 非逃逸闭包

非逃逸闭包的生命周期比较简单:

1、把闭包作为参数传递给函数。
2、函数中运行该闭包。
3、退出函数。

非逃逸闭包

非逃逸闭包被限制在函数内,当函数退出的时候,该闭包引用计数不会增加,也就是说其引用计数在进入函数和退出函数时保持不变。

而对于非逃逸型闭包,由于其生命周期确定短于相关函数,编译器可以据此做性能优化。

class ClassA {
  // 接受非逃逸闭包作为参数
  func someMethod(closure: () -> Void) {
    // 想干什么?
  }
}

class ClassB {
  let classA = ClassA()
  var someProperty = "Hello"

  func testClosure() {
    classA.someMethod {
      // self 被捕获
      someProperty = "闭包内..."
    }
  }
}

当传递闭包参数给函数someMethod时,要注意ClassB中的属性someProperty,虽然闭包会捕获self,但是由于默认闭包参数是非逃逸型,这里可以省略 self, 反正编译器已经知道这里不会有循环引用的潜在风险。

4.2 逃逸闭包

逃逸闭包恰恰与非逃逸闭包相反,其生命周期长于相关函数,当函数退出的时候,逃逸闭包的引用仍然被其他对象持有,不会在相关函数结束后释放。

逃逸闭包
  • 逃逸闭包的定义:一个传入函数的闭包如果在函数执行结束之后才会被调用,那么这个闭包就叫做逃逸闭包。

下面通过一个例子来理解逃逸闭包:

// 定义一个存放闭包的全局数组
var completionHandlers: [() -> Void] = []

// 定义一个接收闭包的函数
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) 
{
    completionHandlers.append(completionHandler)
}

// 定义另一个接收闭包的函数
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

/*
定义一个类:
初始化x值为10
通过调用上面定义的两个函数,使用尾随闭包的方式将实现"对x赋值"这么一个功能的闭包传入
*/
class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

// 创建类的对象
let instance = SomeClass()

/*
执行doSomething函数
PS:内部执行someFunctionWithEscapingClosure,someFunctionWithNonescapingClosure,即期望内部会利用两个尾随闭包对x进行赋值
*/
instance.doSomething()
print(instance.x)
// 打印出 "200"

completionHandlers.first?()
print(instance.x)
// 打印出 "100"

因为逃逸闭包是在函数执行之后才会执行,所以可以这么理解:

(1)我创建了一个类的对象instance
(2)对象中初始化了一个x = 10
(3)利用对象执行了函数doSomething
(4)函数内部先调用了全局函数someFunctionWithEscapingClosure该函数传入了尾随闭包{ self.x = 100 },期望修改instance对象中的x值为100(但是此时并没有执行这个包含了赋值语句的闭包)
(5)函数内部进一步调用全局函数someFunctionWisthNonescapingClosure该函数传入了尾随闭包{ x = 200 },期望修改instance对象中的x值为200(因为此时全局函数someFunctionWithNonescapingClosure内部执行这个包含了赋值语句的闭包,所以x的值由10变为了200)
(6)输出(显然结果为200)
(7)查找全局数组completionHandlers,找到里面第一个元素,显然找到的是在someFunctionWithEscapingClosure函数中添加的闭包{ self.x = 100 },此时才通过全局数组的查询找出闭包并执行,于是x此时才被赋值为100(典型的someFunctionWithEscapingClosure函数执行完毕后才执行闭包{ self.x = 100 },于是这个在函数之后最后才执行到的闭包,就是符合定义的逃逸闭包了。

  • 使用逃逸闭包的2个场景:
    (1)异步调用:如果需要调度队列中异步调用闭包, 这个队列会持有闭包的引用,至于什么时候调用闭包,或闭包什么时候运行结束都是不可预知的。
    (2)存储:需要存储闭包作为属性,全局变量或其他类型做稍后使用。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容