Я немного запутался в том, как создаются сильные ссылки и когда возникают циклы ссылок. Вот простой пример:
class Model {
var foo: Data?
func makeRequest(url: URL) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// assume request completes successfully
self.foo = data!
}
task.resume()
}
}
class ViewController: UIViewController {
var model = Model()
let url = URL(string: "abc.com")! // assume URL is valid
override func viewDidLoad() {
super.viewDidLoad()
model.makeRequest(url: url)
}
}
Это мое понимание того, как ссылки работают в приведенном выше коде:
model
содержит сильную ссылку на экземпляр класса Model.Это верно? И если да, то я действительно не понимаю шаг 4. Почему экземпляр модели не содержит строгой ссылки на задачу данных, поскольку задача создается в функции класса Model?
Примечание. Я видел несколько связанных вопросов, но до сих пор не понимаю, почему экземпляр модели не содержит строгой ссылки на сеанс, задачу или закрытие.
Здесь нет цикла (как вы заметили). Однако URLSession.shared
, который никогда не исчезает, содержит ссылку на задачу, которая содержит ссылку на Model
. Это означает, что Model
не может быть освобожден до тех пор, пока задача не будет завершена. (Если бы у Model
было свойство urlSession
, то технически это был бы цикл, но на практике это ничего не изменило бы. «Петли» не волшебны. Если что-то, что живет вечно, содержит ссылку на что-то, оно сохранит этот объект живым. навсегда.)
Как правило, это хорошо. Задачи URLSession автоматически освобождают свой блок завершения, когда они завершаются, поэтому Model
сохраняется только до тех пор, пока задача не будет выполнена. Пока Model
не предполагает, что ViewController
все еще существует (чего не должно быть), здесь нет ничего плохого в том, что касается ссылочных циклов.
Единственное, что плохо в этом коде, это то, что Model
не удерживает задачу, поэтому может отменить ее или даже определить, что она выполняется (чтобы избежать параллельных дублирующих запросов). Это не является серьезной проблемой для простых приложений, но это полезно улучшить в более сложных приложениях.
Давайте рассмотрим ваши вопросы по одному:
Это мое понимание того, как ссылки работают в приведенном выше коде:
- Переменная
model
содержит сильную ссылку на экземпляр классаModel
.
Правильно, контроллер представления сохраняет эту сильную ссылку до тех пор, пока сам контроллер представления не будет освобожден.
URLSession
имеет сильную ссылку на свою задачу данных, которая содержит сильную ссылку на закрытие.
URLSession
поддерживает эту сильную ссылку до тех пор, пока запрос не завершится/не завершится ошибкой, после чего задача данных освобождается. Вам не нужно сохранять ссылку на задачу данных, так как URLSession
автоматически зависает на ней на время запроса. При этом мы часто сохраняли собственную weak
ссылку на него, которую мы использовали бы, если/когда мы могли бы захотеть отменить запрос. (См. ниже.)
- Замыкание экранирует функцию, и, поскольку ему необходимо обновить себя, оно содержит строгую ссылку на экземпляр модели.
Да, в нынешнем виде замыкание поддерживает сильную ссылку на self
, и это замыкание не будет выпущено до тех пор, пока задача данных не завершится или не завершится с ошибкой.
Кроме того, мы обычно не будем этого делать. Часто мы использовали бы список захвата [weak self]
в этом закрытии, чтобы он не сохранял эту сильную ссылку на self
. Например, вы можете:
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
// see if `self` was released
guard let self else { return }
// see if request succeeded
guard let data else {
print(error ?? URLError(.badServerResponse))
return
}
// if we got here, we have our `Data`; we always avoid forced unwrapping operators when dealing with data from a remote server
self.foo = data
}
- Однако экземпляр
Model
не содержит строгой ссылки на задачу данных, и поэтому цикл ссылок отсутствует.
Да. Или, точнее, как реализовано в вопросе, URLSession
сохранит сильную ссылку на self
и освободит эту сильную ссылку, когда запрос завершится или завершится ошибкой. Но, опять же, если мы используем список захвата [weak self]
, как описано выше, он вообще не будет хранить сильную ссылку, и Model
будет освобожден, как только будет освобожден контроллер представления.
Еще лучше, если нам явно не нужно, чтобы задача продолжала работать, даже если Model
по какой-то причине освобождается, мы бы cancel
task
, когда Model
освобождается:
class Model {
var foo: Data?
private weak var task: URLSessionTask?
deinit {
task?.cancel()
}
func makeRequest(url: URL) {
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self else { return }
guard let data else {
print(error ?? URLError(.badServerResponse))
return
}
self.foo = data
}
task.resume()
self.task = task
}
}
Обратите внимание, нам не нужно и не хочется сохранять сильную ссылку на URLSessionTask
. (URLSession
будет управлять жизненным циклом URLSessionTask
.) Но мы сохраняем нашу собственную ссылку weak
, которая будет автоматически установлена на nil
, когда URLSessionTask
будет выполнено. Таким образом, если запрос еще не выполнен, когда Model
освобождается, мы можем отменить запрос. Но если запрос уже выполнен, эта ссылка task
будет автоматически установлена на nil
для нас, и в этом случае task?.cancel()
станет «нет операции».
Излишне говорить, что ответ Роба Нэпьера правильный (+1) и должен быть принятым ответом. Я просто хотел добавить несколько уточнений…