Слабое «я» и «я» без собственности в Swift многим из нас трудно понять. Хотя автоматический подсчет ссылок (ARC) уже многое для нас решил, нам все равно нужно управлять ссылками, когда мы не работаем с типами значений.

Что такое ARC, Retain и Release?

Нам нужно начать с основ, чтобы полностью понять, что делают слабое и незатронутое я. Эти концепции лучше всего можно объяснить, прочитав Автоматический подсчет ссылок в документации Swift. Все сводится к управлению памятью.

До ARC нам приходилось вручную управлять памятью и ссылками. Это вызвало множество ошибок и головной боли. Счетчик ссылок увеличивается, когда новый экземпляр сохраняет объект, и уменьшается после освобождения ссылки. Память освобождается, как только не остается ссылки на объект, что означает, что объект больше не нужен.

В Swift нам нужно использовать weak self и unowned self, чтобы предоставить ARC необходимую информацию между отношениями в нашем коде. По сути, он сообщает нашему коду, нужна ли определенная сильная ссылка, и предотвращает уменьшение счетчика ссылок до нуля.

Без правильного использования этих ключевых слов мы можем сохранить память, что может вызвать предупреждения о памяти в вашем приложении. Так называемые циклы сильных ссылок или циклы сохранения также могут возникать, если слабые и бесхозные не используются правильно.

Как цитируется и позже объясняется в этом сообщении блога, полезно знать, что:

Подсчет ссылок применяется только к экземплярам классов. Структуры и перечисления являются типами значений, а не ссылочными типами, и не хранятся и не передаются по ссылке.

Когда использовать слабое я

Во-первых, слабые ссылки всегда объявляются как необязательные переменные, поскольку они могут автоматически устанавливаться в nil с помощью ARC, когда его ссылка освобождается. Следующие два класса помогут объяснить, когда использовать слабую ссылку.

class Blog {
    let name: String
    let url: URL
    var owner: Blogger?

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \(name) is being deinitialized")
    }
}

class Blogger {
    let name: String
    var blog: Blog?

    init(name: String) { self.name = name }

    deinit {
        print("Blogger \(name) is being deinitialized")
    }
}

Как только какой-либо из этих классов освобождается, печатается сообщение. В следующем примере кода мы определяем два экземпляра как необязательные, а затем устанавливаем для них значение nil. Хотя некоторые из вас могут ожидать двух операторов печати, на самом деле этого не происходит:

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog = nil
blogger = nil

// Nothing is printed

Это результат цикла сохранения. В блоге есть сильная ссылка на своего владельца, и он не желает выпускать его. В то же время владелец не желает освобождать свой блог. Блог не освобождает своего владельца, который сохраняет свой блог, который сохраняет себя, что… ну, вы поняли. Это бесконечный цикл: цикл сохранения.

Следовательно, нам нужно ввести слабую ссылку. В этом примере нужна только одна слабая ссылка, так как это уже разорвало бы цикл. Например, мы можем установить слабую ссылку из блога на его владельца:

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \(name) is being deinitialized")
    }
}

class Blogger {
    let name: String
    var blog: Blog?

    init(name: String) { self.name = name }

    deinit {
        print("Blogger \(name) is being deinitialized")
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog = nil
blogger = nil

// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized

Но как насчет слабого «я»?

Я знаю, это не был пример слабого «я». Однако это объясняет историю.

Для многих из нас лучше всего всегда использовать weak в сочетании с внутренним закрытием self, чтобы избежать циклов сохранения. Однако это необходимо только в том случае, если self также сохраняет закрытие. Если вы всегда по умолчанию выбираете слабый, вы, вероятно, работаете с опциями чаще, чем вам нужно.

Скажите, что мы вводим метод публикации для наших сообщений в блоге. Обратите внимание, что в этом примере мы подделываем сетевой запрос, добавляя задержку вручную.

struct Post {
    let title: String
    var isPublished: Bool = false

    init(title: String) { self.title = title }
}

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    var publishedPosts: [Post] = []

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \(name) is being deinitialized")
    }

    func publish(post: Post) {
        /// Faking a network request with this delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.publishedPosts.append(post)
            print("Published post count is now: \(self.publishedPosts.count)")
        }
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil

Это распечатает следующее:

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1
// Blog SwiftLee is being deinitialized

Вы можете видеть, что запрос выполнен до того, как блог был выпущен. Сильная ссылка позволила нам завершить публикацию и сохранить сообщение в наших опубликованных сообщениях.

Если бы мы изменили метод публикации, включив вместо этого слабую ссылку:

func publish(post: Post) {
    /// Faking a network request with this delay:
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
        self?.publishedPosts.append(post)
        print("Published post count is now: \(self?.publishedPosts.count)")
    }
}

Мы получили бы следующий результат:

// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized
// Published post count is now: nil

Поскольку блог был выпущен до завершения запроса на публикацию, мы никогда не сможем обновить локальное состояние опубликованных сообщений.

Поэтому убедитесь, что не используете weak self, если нужно выполнить работу со ссылочным экземпляром, как только будет выполнено замыкание.

Слабые ссылки и циклы сохранения

Цикл удержания происходит, как только закрытие сохраняет самость, а самость удерживает закрытие. Если бы вместо этого у нас была переменная, содержащая замыкание onPublish, это могло бы произойти:

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    var publishedPosts: [Post] = []
    var onPublish: ((_ post: Post) -> Void)?

    init(name: String, url: URL) {
        self.name = name
        self.url = url

        // Adding a closure instead to handle published posts
        onPublish = { post in
            self.publishedPosts.append(post)
            print("Published post count is now: \(self.publishedPosts.count)")
        }
    }

    deinit {
        print("Blog \(name) is being deinitialized")
    }

    func publish(post: Post) {
        /// Faking a network request with this delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.onPublish?(post)
        }
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil

Закрытие сохраняет блог, в то время как блог сохраняет закрытие. В результате печатается следующее:

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1

Несмотря на то, что при счете 1 все в порядке, мы не наблюдаем деинициализации блога и издателя. Это происходит из-за цикла сохранения и приводит к тому, что память не освобождается.

Добавление слабой ссылки на наш экземпляр блога внутри метода onPublish решает наш цикл сохранения:

onPublish = { [weak self] post in
    self?.publishedPosts.append(post)
    print("Published post count is now: \(self?.publishedPosts.count)")
}

И дает следующий результат:

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: Optional(1)
// Blog SwiftLee is being deinitialized

Данные сохраняются локально, и все экземпляры освобождаются. Больше никаких циклов сохранения.

Наконец, чтобы завершить этот раздел, важно знать, что наблюдатели свойств не вызываются, когда ARC устанавливает слабую ссылку на nil.

Когда использовать самообладание без собственности

В отличие от слабых ссылок, ссылка не превращается в необязательную при использовании unowned. Тем не менее, как бесхозяйные, так и слабые не создают сильных ссылок.

Цитата из документации Apple:

Используйте слабую ссылку всякий раз, когда она действительна для того, чтобы эта ссылка стала нулевой в какой-то момент в течение ее жизненного цикла. И наоборот, используйте незарегистрированную ссылку, если вы знаете, что ссылка никогда не будет нулевой, если она была установлена ​​во время инициализации.

В общем, будьте очень осторожны при использовании unowned. Возможно, вы обращаетесь к экземпляру, которого больше нет, что вызывает сбой. Единственное преимущество использования unowned over weak в том, что вам не придется иметь дело с необязательными. Следовательно, в таких сценариях всегда безопаснее использовать weak.

Почему нам это не нужно с такими типами значений, как структуры?

В Swift есть типы значений и ссылочные типы.

Это уже делает его немного более понятным, так как с типом ссылки у вас действительно есть ссылка, о которой нужно позаботиться. Это означает, что вам нужно управлять этими отношениями как сильными, слабыми или лишенными собственности.

Типы значений хранят уникальную копию своих данных - уникальный экземпляр. Это означает, что нет необходимости использовать слабую ссылку в многопоточных средах, поскольку нет ссылки, только уникальная копия, с которой мы работаем.

Используются ли слабые и лишенные собственности только с собой?

Нет, определенно нет. Вы можете указать любое свойство или объявление переменной как слабое или не принадлежащее, если это ссылочный тип. Следовательно, это также может сработать:

download(imageURL, completion: { [weak imageViewController] result in
    // ...
})

И вы даже можете ссылаться на несколько экземпляров - это, по сути, массив:

download(imageURL, completion: { [weak imageViewController, weak imageFinalizer] result in
    // ...
})

Заключение

Это сложная тема для понимания. Лучше всего начать с прочтения документации Swift, которая содержит еще более подробные сведения. Кроме того, если вы не уверены, используйте слабый вместо безымянный. Это может уберечь вас от надоедливых ошибок.

Спасибо!