Callback — это функция, которая передаётся в другую функцию в качестве аргумента и выполняется после завершения асинхронной операции. В мобильной разработке callback применяется для обработки результатов сетевых запросов, работы с базами данных и анимаций. По данным Apple Documentation (2025), замыкания в Swift являются основной формой callback и используются в URLSession, GCD и Combine. В Android callback реализован через интерфейсы, лямбды Kotlin и ListenableFuture.
Главное
Callback (функция обратного вызова) — это исполняемый код, который передаётся в другую функцию и вызывается после завершения определённого действия. В мобильной разработке callback является фундаментальным механизмом асинхронного программирования, позволяя реагировать на завершение сетевых запросов, таймеров, анимаций и операций ввода-вывода без блокировки основного потока. Swift и Kotlin предоставляют встроенные синтаксические конструкции для создания callback — замыкания и лямбды соответственно.
Функция высшего порядка принимает другую функцию как параметр и вызывает её после выполнения своей основной логики. Поток управления передаётся обратно в caller через callback, отсюда и название. В iOS callback применяется в UIKit (анимации UIView.animate), Foundation (URLSession.dataTask) и Combine (sink). В Android callback используется в View.OnClickListener, Retrofit Callback и Room DAO. Современные API всё чаще заменяют callback на async/await или корутины, но понимание callback необходимо для работы с legacy-кодом и низкоуровневыми API.
Callback бывает синхронным (вызывается немедленно внутри функции) и асинхронным (вызывается позже из другого потока или очереди). Синхронные callback используются для сортировки (компараторы) и обхода коллекций. Асинхронные callback применяются для сетевых запросов, чтения файлов и работы с сенсорами. Разница критична для понимания threading: синхронный callback выполняется в том же потоке, асинхронный — в потоке, определённом диспетчером (DispatchQueue в iOS, Dispatchers в Kotlin).
Механизм callback на обеих платформах основан на одном принципе: функция передаётся как объект первого класса и хранится до момента выполнения. Однако реализации различаются из-за разных языковых парадигм. В iOS callback — это замыкание (closure), которое захватывает переменные из окружающего контекста. В Android callback чаще всего реализован через анонимные классы или лямбда-выражения Kotlin, компилируемые в FunctionalInterface.
При вызове асинхронной функции замыкание сохраняется в куче вместе с захваченными переменными. Когда операция завершается, система GCD или OperationQueue помещает callback в соответствующую очередь (main queue или background queue). После выполнения callback удаляется из памяти при отсутствии сильных ссылок. Capture list ([weak self]) предотвращает удержание объекта после его деаллокации. Без capture list возникает retain cycle, при котором объект и callback ссылаются друг на друга.
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 передаётся через интерфейс или лямбду. При выполнении асинхронной операции через ExecutorService или корутину, callback хранится в памяти до завершения фоновой работы. Kotlin-лямбды компилируются в анонимные классы, захватывающие внешние переменные. Отсутствие weak-ссылок в JVM требует ручного управления: обнуление callback в onDestroy() или отмена корутин через Job.cancel(). ViewModel и LiveData решают эту проблему на уровне архитектурного компонента.
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) }
}
}
}
}
// Использование с лямбдой
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
Синтаксис callback определяется языковыми возможностями работы с функциями как с объектами первого класса. В Swift замыкания имеют краткий синтаксис с автоматическими именами аргументов ($0, $1). В Kotlin лямбды также поддерживают it для единственного аргумента. Различия проявляются в обработке захвата переменных (capture list в Swift против изменяемых ссылок в Kotlin) и типизации (Result
Swift-замыкание — это самодостаточный блок кода, который может быть передан и использован в другой функции. Замыкания бывают глобальными (именованными), вложенными и expression-level. @escaping помечает замыкание, которое будет выполнено после возврата из функции — это обязательное требование для асинхронных callback. Без @escaping замыкание может быть выполнено только внутри тела функции. Trailing closure syntax позволяет передавать замыкание после круглых скобок: fetchData { result in ... }.
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 в Kotlin передаётся через параметр типа (T) -> Unit или (T) -> R для возврата значения. suspend-функции Kotlin корутин заменяют callback на sequential-код, но callback остаётся в Java-совместимых API и Android SDK (View.setOnClickListener, TextWatcher). Kotlin-лямбды автоматически захватывают val-переменные, var-переменные требуют mutability-обёрток.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Пример с лямбдой
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle — ситуация, когда два объекта держат сильные ссылки друг на друга, предотвращая их освобождение сборщиком памяти. В Swift retain cycle возникает, когда viewController захватывает замыкание, а замыкание захватывает self. В Kotlin/Java утечка происходит, когда Activity передаёт inner class или лямбду в длительную фоновую операцию. По данным WWDC Session 10216 (2024), неправильное управление замыканиями — третья по частоте причина утечек памяти в iOS-приложениях.
Swift использует Automatic Reference Counting (ARC), который освобождает объект при обнулении счётчика ссылок. Capture list [weak self] или [unowned self] в замыкании предотвращает retain cycle. weak self создаёт опциональную ссылку, которая становится nil при деаллокации объекта. unowned self предполагает, что объект живёт дольше замыкания — при нарушении этого предположения происходит crash. Рекомендуется использовать weak self как безопасный вариант по умолчанию.
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 callback-утечка происходит, когда Activity или Fragment передаёт слушатель в singleton-компонент (например, EventBus или Service). WeakReference позволяет сборщику мусора освободить Activity, даже если на неё есть слабая ссылка. Lifecycle-aware компоненты (LiveData, Flow) решают проблему автоматически. Kotlin-лямбды, захватывающие Activity-контекст, тоже могут вызывать утечку: лямбда неявно хранит ссылку на this.
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 (также известный как Pyramid of Doom) — это ситуация, при которой множество вложенных callback создают глубоко вложенную структуру кода, сложную для чтения и отладки. Каждый следующий шаг требует ожидания завершения предыдущего, что приводит к вложенности 5-10 уровней. Эта проблема характерна для последовательных асинхронных операций: загрузка данных → парсинг → сохранение в БД → обновление UI.
Swift 5.5 ввёл асинхронные функции (async/await), которые позволяют писать асинхронный код последовательно. AsyncSequence и AsyncStream заменяют callback-based итерации. Combine фреймворк предоставляет операторы flatMap, merge, combineLatest для композиции асинхронных потоков без вложенности. Однако callback остаётся необходимым для работы с Objective-C API и сторонними библиотеками без async-поддержки.
// Вложенные callback — 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 корутины заменяют callback на suspend-функции с последовательным выполнением. Flow предоставляет cold-потоки с операторами map, flatMapConcat, combine. CoroutineScope позволяет отменять все запущенные корутины при уничтожении компонента. Room, Retrofit и другие библиотеки Jetpack имеют встроенную поддержку suspend-функций, устраняя необходимость в callback для стандартных операций.
// Последовательные callback — 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 и Delegate — два подхода к асинхронному уведомлению, и выбор между ними зависит от архитектурных требований. Callback подходит для разовых операций с одним результатом. Delegate предназначен для множественных событий с разными сигнатурами методов. Apple рекомендует delegate для сложных протоколов с несколькими методами, callback — для простых замыканий с одним результатом. В Android callback заменяет delegate в большинстве случаев из-за поддержки лямбд.
Callback оптимален для операций с единственным результатом: сетевой запрос, чтение файла, анимация с completion-блоком. Преимущества: компактный синтаксис, отсутствие отдельного протокола, прямой захват контекста. Недостатки: сложность при множественных результатах (прогресс, пауза, отмена), невозможность множественной отправки (если callback может быть вызван более одного раза — используйте publisher).
Delegate подходит для протоколов с несколькими обязательными и опциональными методами: UITableViewDelegate, CLLocationManagerDelegate, Bluetooth-соединения. Преимущества: чёткая типизация каждого метода, документация через протокол, поддержка опциональных методов через @objc optional. Недостатки: бойлерплейт-код, слабая ссылка на delegate обязательна (weak var delegate), сложность при захвате контекста.
Часто задаваемые вопросы
Callback — это частный случай функции высшего порядка. Функция высшего порядка принимает другую функцию как аргумент или возвращает её. Callback — это функция, передаваемая специально для асинхронного выполнения после завершения операции. Все callback реализуются через функции высшего порядка, но не каждая функция высшего порядка является callback.
По соглашению callback должен вызываться ровно один раз — либо success, либо failure. Множественный вызов одного callback считается ошибкой проектирования. Для множественных событий (прогресс, поток данных) используйте Observable, Publisher или Flow — они поддерживают множественную эмиссию значений. Некоторые API нарушают это правило, что ведёт к трудноотловимым багам.
Trailing closure — синтаксический сахар Swift, позволяющий передать замыкание после круглых скобок вызова функции. Если функция принимает замыкание последним аргументом, его можно вынести за скобки: fetchData { result in ... }. Для нескольких замыканий trailing closure применяется только к последнему, остальные именуются внутри скобок. Это улучшает читаемость callback-based API.
Используйте WeakReference для долгоживущих слушателей, отменяйте корутины через Job.cancel() в onDestroy(), применяйте lifecycleScope для автоматической отмены. ViewModel + LiveData/Flow решают проблему на архитектурном уровне. Избегайте передачи Activity-контекста в статические callback — используйте Application context. Kotlin-лямбды захватывают this неявно, проверяйте через memory profiler.
Async/await заменяет callback для последовательного асинхронного кода, но не для event-driven архитектуры. Callback остаётся в системных API (View.OnClickListener, URLSession делегаты), обратных вызовах с прогрессом и сторонних библиотеках. Полная замена невозможна из-за обратной совместимости. Современная стратегия — использовать async/await с callback-обёртками (continuation в Swift, suspendCancellableCoroutine в Kotlin).
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также