Callback——什么是回调函数及其工作原理

作者: IT Sectr 发布日期: 2026-03-17 阅读时间: 11 分钟

Callback——是一个函数,作为参数传递给另一个函数,在异步操作完成后执行。在移动开发中,回调用于处理网络请求结果、数据库操作和动画。根据Apple Documentation(2025),Swift中的闭包是回调的主要形式,用于URLSession、GCD和Combine。在Android中,回调通过接口、Kotlin lambda表达式和ListenableFuture实现。

要点

  • Callback——作为参数传递用于异步执行的回调函数。
  • Swift使用带有@escaping关键字的闭包进行回调。
  • Kotlin应用lambda表达式和高阶函数进行回调。
  • Retain cycle——在iOS上回调中捕获self时的内存泄漏。
  • Callback Hell——嵌套回调的问题,通过async/await和协程解决。

什么是Callback?

Callback(回调函数)——是可执行代码,传递给另一个函数并在特定操作完成后被调用。在移动开发中,回调是异步编程的基本机制,允许在不阻塞主线程的情况下响应网络请求、定时器、动画和输入/输出操作的完成。Swift和Kotlin提供了内置的语法结构来创建回调——分别是闭包和lambda表达式。

Callback的工作原理

高阶函数接受另一个函数作为参数,并在执行完其主要逻辑后调用它。控制流通过回调返回给调用者,因此得名。在iOS中,回调应用于UIKit(UIView.animate动画)、Foundation(URLSession.dataTask)和Combine(sink)。在Android中,回调用于View.OnClickListener、Retrofit Callback和Room DAO。现代API越来越多地用async/await或协程替换回调,但理解回调对于处理遗留代码和低级API是必要的。

同步和异步Callback

回调可以是同步的(在函数内部立即调用)和异步的(稍后从另一个线程或队列调用)。同步回调用于排序(比较器)和遍历集合。异步回调应用于网络请求、文件读取和传感器工作。这种区别对于理解线程至关重要:同步回调在同一线程中执行,异步回调——在由调度程序确定的线程中执行(iOS中的DispatchQueue,Kotlin中的Dispatchers)。

Callback在iOS和Android中如何工作?

两个平台上的回调机制基于相同原理:函数作为一等对象传递并存储到执行时刻。然而,由于不同的语言范式,实现有所不同。在iOS中,回调是一个闭包,它捕获周围上下文中的变量。在Android中,回调通常通过匿名类或Kotlin lambda表达式实现,编译为FunctionalInterface。

iOS中Callback的生命周期

当调用异步函数时,闭包与捕获的变量一起存储在堆中。当操作完成时,GCD或OperationQueue系统将回调放入相应的队列(主队列或后台队列)。执行后,如果没有强引用,回调将从内存中删除。捕获列表([weak self])防止对象在释放后被保留。没有捕获列表就会产生保留循环,其中对象和回调相互引用。

swift
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        completion(.success(data))
    }
    task.resume()
}

// 使用[weak self]
fetchData { [weak self] result in
    guard let self else { return }
    switch result {
    case .success(let data):
        self.updateUI(data)
    case .failure(let error):
        self.showError(error)
    }
}

Android中Callback的生命周期

在Android中,回调通过接口或lambda传递。当通过ExecutorService或协程执行异步操作时,回调存储在内存中直到后台工作完成。Kotlin lambda被编译为捕获外部变量的匿名类。JVM中缺少弱引用需要手动管理:在onDestroy()中重置回调或通过Job.cancel()取消协程。ViewModel和LiveData在架构组件级别解决了这个问题。

kotlin
interface Callback<T> {
    fun onSuccess(data: T)
    fun onError(error: Throwable)
}

class Repository {
    fun loadData(callback: Callback<List<User>>) {
        thread {
            try {
                val result = api.fetchUsers()
                runOnUiThread { callback.onSuccess(result) }
            } catch (e: Exception) {
                runOnUiThread { callback.onError(e) }
            }
        }
    }
}

// 使用lambda
repository.loadData(object : Callback<List<User>> {
    override fun onSuccess(data: List<User>) { showUsers(data) }
    override fun onError(error: Throwable) { showError(error.message) }
})

Swift和Kotlin中的Callback语法

回调语法由语言将函数作为一等对象处理的能力决定。在Swift中,闭包具有简洁的语法和自动参数名称($0,$1)。在Kotlin中,lambda也支持单个参数的it。差异体现在变量捕获处理(Swift中的捕获列表 vs Kotlin中的可变引用)和类型化(Result<Success, Failure> vs Result<T>)上。

Swift中的Callback:闭包

Swift闭包是一个自包含的代码块,可以传递给另一个函数并使用。闭包可以是全局的(命名的)、嵌套的和表达式级别的。@escaping标记一个将在函数返回后执行的闭包——这是异步回调的强制性要求。没有@escaping,闭包只能在函数体内部执行。尾随闭包语法允许在圆括号之后传递闭包:fetchData { result in ... }。

swift
typealias NetworkResult = (Result<[String: Any], Error>) -> Void

func performRequest(
    url: URL,
    then handler: @escaping NetworkResult
) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        handler(Result {
            guard let json = try JSONSerialization.jsonObject(with: data)
            else { throw NetworkError.invalidData }
            return json as! [String: Any]
        })
    }
    task.resume()
}

performRequest(url: url) { result in
    switch result {
    case .success(let json): process(json)
    case .failure(let error): log(error.localizedDescription)
    }
}

Kotlin中的Callback:lambda和高阶函数

Kotlin支持接受其他函数作为参数的高阶函数。Callback在Kotlin中通过类型为(T) -> Unit或(T) -> R的参数传递以返回值。Kotlin协程的挂起函数用顺序代码替换回调,但回调仍然存在于兼容Java的API和Android SDK中(View.setOnClickListener,TextWatcher)。Kotlin lambda自动捕获val变量,var变量需要可变包装器。

kotlin
fun <T, R> processWithCallback(
    input: T,
    transform: (T) -> R,
    onResult: (R) -> Unit
) {
    thread {
        val result = transform(input)
        runOnUiThread { onResult(result) }
    }
}

// 使用lambda的示例
processWithCallback(
    input = "Hello",
    transform = { it.length },
    onResult = { length ->
        textView.text = "Length: $length"
    }
)

Retain cycles和Callback中的内存泄漏

Retain cycle——两个对象保持彼此的强引用,阻止垃圾收集器释放它们的情况。在Swift中,当视图控制器捕获一个闭包且闭包捕获self时,就会产生保留循环。在Kotlin/Java中,当Activity将内部类或lambda传递给长时间运行的后台操作时,就会发生泄漏。根据WWDC Session 10216(2024),闭包管理不当是iOS应用中第三常见的内存泄漏原因。

Swift中的Retain cycles

Swift使用自动引用计数(ARC),在引用计数器归零时释放对象。闭包中的捕获列表[weak self]或[unowned self]可防止保留循环。weak self创建一个可选引用,在对象释放时变为nil。unowned self假定对象比闭包存活更久——违反此假设会导致崩溃。建议使用weak self作为安全的默认选项。

swift
class DataController {
    var onDataUpdate: ((String) -> Void)?

    func setupCallback() {
        // Retain cycle!
        onDataUpdate = { text in
            self.process(text)
        }

        // 已修复:[weak self]
        onDataUpdate = { [weak self] text in
            guard let self else { return }
            self.process(text)
        }
    }

    func process(_ input: String) { }
}

Android中的内存泄漏

在Android中,当Activity或Fragment将监听器传递给单例组件(例如EventBus或Service)时,就会发生回调泄漏。弱引用允许垃圾收集器释放Activity,即使存在对它的弱引用。Lifecycle-aware组件(LiveData、Flow)自动解决问题。捕获Activity上下文的Kotlin lambda也可能导致泄漏:lambda隐式保存对this的引用。

kotlin
class SafeCallbackManager {
    private val listeners = mutableListOf<WeakReference<(String) -> Unit>>()

    fun addListener(callback: (String) -> Unit) {
        listeners.add(WeakReference(callback))
    }

    fun notifyAll(data: String) {
        val iterator = listeners.iterator()
        while (iterator.hasNext()) {
            val ref = iterator.next().get()
            if (ref != null) ref(data)
            else iterator.remove()
        }
    }
}

// 在Fragment中使用
manager.addListener { result ->
    // WeakReference不持有Fragment
    updateUI(result)
}

Callback Hell及应对方法

Callback Hell(也称为厄运金字塔)——大量嵌套回调创建深度嵌套的代码结构,难以阅读和调试的情况。每一步都需要等待前一步完成,导致5-10层嵌套。这个问题典型于顺序异步操作:加载数据→解析→保存到数据库→更新UI。

Swift中的解决方案:async/await

Swift 5.5引入了异步函数(async/await),允许顺序编写异步代码。AsyncSequenceAsyncStream取代了基于回调的迭代。Combine框架提供了flatMap、merge、combineLatest操作符,用于无嵌套组合异步流。然而,对于处理没有async支持的Objective-C API和第三方库,回调仍然是必要的。

swift
// 嵌套回调——Callback Hell
loginUser(credentials) { user in
    fetchProfile(user.id) { profile in
        downloadAvatar(profile.avatarUrl) { image in
            cacheImage(image) { success in
                updateUI(user, profile, image)
            }
        }
    }
}

// async/await——解决方案
func loadUserExperience() async throws {
    let user = try await loginUser(credentials)
    let profile = try await fetchProfile(user.id)
    let image = try await downloadAvatar(profile.avatarUrl)
    try await cacheImage(image)
    updateUI(user, profile, image)
}

Kotlin中的解决方案:协程和Flow

Kotlin协程用具有顺序执行的挂起函数替换回调。Flow提供带有map、flatMapConcat、combine操作符的冷流。CoroutineScope允许在组件销毁时取消所有启动的协程。Room、Retrofit和其他Jetpack库具有内置的挂起函数支持,消除了标准操作对回调的需求。

kotlin
// 顺序回调——Callback Hell
api.login(credentials) { user ->
    api.fetchProfile(user.id) { profile ->
        api.download(profile.avatarUrl) { bytes ->
            file.save(bytes) { result ->
                textView.text = result.toString()
            }
        }
    }
}

// 协程——解决方案
suspend fun loadUserData() {
    val user = withContext(Dispatchers.IO) { api.login(credentials) }
    val profile = withContext(Dispatchers.IO) { api.fetchProfile(user.id) }
    val bytes = withContext(Dispatchers.IO) { api.download(profile.avatarUrl) }
    withContext(Dispatchers.IO) { file.save(bytes) }
    textView.text = "Done"
}

Callback vs Delegate:如何选择?

CallbackDelegate——异步通知的两种方法,它们之间的选择取决于架构要求。Callback适用于具有单一结果的一次性操作。Delegate适用于具有不同方法签名的多个事件。Apple推荐将Delegate用于具有多个方法的复杂协议,Callback用于具有单一结果的简单闭包。在Android中,由于对lambda的支持,Callback在大多数情况下取代了Delegate。

何时选择Callback

Callback对于具有单一结果的操作是最佳的:网络请求、文件读取、带有完成块的动画。优点:语法紧凑,没有单独的协议,直接捕获上下文。缺点:多结果时的复杂性(进度、暂停、取消),无法多次发送(如果回调可能被多次调用——使用publisher)。

何时选择Delegate

Delegate适用于具有多个必需和可选方法的协议:UITableViewDelegate、CLLocationManagerDelegate、蓝牙连接。优点:每个方法都有清晰的类型化,通过协议进行文档化,通过@objc optional支持可选方法。缺点:模板代码,对委托的必需弱引用(weak var delegate),上下文捕获的复杂性。

常见问题

回调和更高阶函数有什么区别?

Callback是更高阶函数的一个特例。高阶函数接受另一个函数作为参数或返回它。回调是专门为操作完成后异步执行而传递的函数。所有回调都通过更高阶函数实现,但并非每个更高阶函数都是回调。

回调可以被多次调用吗?

按照约定,回调应该只被调用一次——要么成功要么失败。多次调用同一个回调被认为是设计错误。对于多个事件(进度、数据流),请使用Observable、Publisher或Flow——它们支持多次值发射。有些API违反此规则,导致难以发现的错误。

Swift中的尾随闭包是什么?

尾随闭包——Swift的语法糖,允许在函数调用的圆括号之后传递闭包。如果函数接受闭包作为最后一个参数,可以将其放在括号外:fetchData { result in ... }。对于多个闭包,尾随闭包仅适用于最后一个,其余的在括号内命名。这提高了基于回调的API的可读性。

如何在Android中避免回调时的内存泄漏?

对于长期存在的监听器使用弱引用,通过Job.cancel()在onDestroy()中取消协程,应用lifecycleScope进行自动取消。ViewModel + LiveData/Flow在架构级别解决问题。避免将Activity上下文传递给静态回调——使用Application上下文。Kotlin lambda隐式捕获this,通过memory profiler检查。

async/await会完全取代回调吗?

Async/await取代了顺序异步代码的回调,但不取代事件驱动架构。回调仍然存在于系统API(View.OnClickListener、URLSession委托)、带进度的回调和第三方库中。由于向后兼容性,完全取代是不可能的。现代策略是将async/await与回调包装器一起使用(Swift中的continuation,Kotlin中的suspendCancellableCoroutine)。

总结

  • Callback——作为参数传递用于操作完成后异步执行的回调函数。
  • Swift通过带有@escaping、捕获列表[weak self]和尾随闭包语法的闭包实现回调。
  • Kotlin使用lambda、高阶函数和协程挂起函数进行异步处理。
  • Retain cycle在iOS中通过捕获列表防止;在Android中——通过弱引用和Lifecycle-aware组件。
  • Callback Hell通过Swift中的async/await和Kotlin中带有Flow的协程解决。
  • Delegate对于具有多个方法的协议比回调更受欢迎;回调——用于一次性操作。
  • 对于简单的异步操作使用回调,对于顺序链使用async/await,对于多个事件使用delegate。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读