逃逸闭包:异步API的回调神器

1,332 阅读4分钟

在 Swift 中,我们可以使用 @escaping 关键字来定义逃逸闭包。逃逸闭包是指闭包在函数返回后才被调用的闭包。这种闭包通常作为异步API的回调参数,用于在异步操作完成后执行一些代码。

逃逸闭包与非逃逸闭包

闭包作为函数的参数可以是被传递和使用的。当一个闭包被定义为函数的参数时,它默认为非逃逸闭包,即它必须在函数执行结束前调用。但是,当一个闭包在函数返回后仍然被调用时,我们称之为逃逸闭包

使用场景

异步API的回调参数

例如,在使用URLSession进行网络请求时,我们可以使用逃逸闭包来处理异步请求的结果:

func loadData(completionHandler: @escaping (Data?, Error?) -> Void) {
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        completionHandler(data, error)
    }

    task.resume()
}

延迟执行的操作

例如,我们可以使用逃逸闭包来实现一些延迟执行的操作,例如在一些情况下我们需要在弹出视图控制器之后执行一些操作:

class MyClass {
    // 默认为逃逸闭包
    var completionHandler: (() -> Void)?
    func doSomething() {
        // 异步操作
        completionHandler?()
    }
}

let myObject = MyClass()
myObject.completionHandler = {
    print("操作完成")
}
myObject.doSomething()

在这个例子中,我们将定义一个completionHandler闭包,该闭包将在视图控制器被弹出后执行。我们可以使用逃逸闭包来完成这个功能。

当一个闭包被定义为函数的参数时,默认为非逃逸闭包
当一个闭包被定义为某个类的变量时,默认为逃逸闭包

定时器

有时候,我们需要开启一个定时器,然后在定时器结束后执行某些操作。使用逃逸闭包可以方便地实现这样的需求。例如:

class TimerManager {
    func startTimer(duration: TimeInterval, completionHandler: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
            completionHandler()
        }
    }
}

let timerManager = TimerManager()
timerManager.startTimer(duration: 2) {
    print("定时器结束!")
}

在这个例子中,我们定义了一个TimerManager类,并在其中声明了一个逃逸闭包completionHandler。 当我们调用startTimer方法时,它会开启一个计时器并在指定的时间后调用completionHandler

动画

当我们需要在一段时间内执行一些动画操作时,可以使用逃逸闭包完成。例如:

class AnimationManager {
    func animate(duration: TimeInterval, completionHandler: @escaping () -> Void) {
        UIView.animate(withDuration: duration) {
            // 执行动画
        } completion: { finished in
            completionHandler()
        }
    }
}

let animationManager = AnimationManager()
animationManager.animate(duration: 1) {
    print("动画结束!")
}

在这个例子中,我们定义了一个AnimationManager类,并在其中声明了一个逃逸闭包completionHandler。当我们调用animate方法时,它会执行一些动画操作,并在动画完成后调用completionHandler进行处理。

使用陷阱

避免循环引用

逃逸闭包捕获了self或其它对象时,可能会发生循环引用的问题。如果不处理这个问题,会导致内存泄漏和应用程序的崩溃。为了避免循环引用,我们需要使用一个弱引用或无主引用来捕获self。例如:

class MyClass {
    var completionHandler: (() -> Void)?
    func doSomething() {
        someAsyncOperation { [weak self] in
            self?.completionHandler?()
        }
    }
}

在这个例子中,我们使用了一个弱引用self,避免了循环引用问题。当逃逸闭包被调用时,我们使用self?.completionHandler?()来访问completionHandler属性,因为此时self可能已经被释放了

总结

Swift中,@escaping关键字用于定义逃逸闭包,逃逸闭包通常作为异步API的回调参数使用。使用逃逸闭包有许多好处

  1. 异步API的回调参数必须是逃逸闭包。由于异步API执行时间较长,非逃逸闭包可能会在函数返回之前就被销毁。因此,逃逸闭包可以确保在异步操作完成后一定会执行
  2. 在某些情况下,闭包需要在函数外部被调用。例如,如果我们要将一个闭包作为一个数组的元素,那么闭包必须是逃逸闭包
  3. 逃逸闭包可以使代码更加简洁和易于管理。当一个函数需要异步执行时,我们不需要创建一个新的类或对象来管理异步操作的结果,而是可以使用逃逸闭包来处理它们