Нужно ли писать weak self в Grand Central Dispatch?

Тут у нас возник спор: нужно ли писать [weak self] в GCD?

Один говорит:
– [weak self] нужно писать везде!
Второй говорит:
– Нет, даже если не писать [weak self] внутри DispatchQueue, утечки памяти не будет.

Вместо того, чтобы разбираться, легче написать пару строк. Тяжелее об этом написать пост.
Итак, мы создадим UIViewController, в котором будет вызываться метод в DispatchQueue через пять секунд после viewDidLoad.

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        
        DispatchQueue.main.asyncAfter(deadline: .now()+5, execute: { [weak self] in
            self?.method()
        })
    }
    
    func method() {
        print("method")
    }
    
    deinit {
        print("deinit")
    }
    
}

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

После запуска в консоли видим:
viewDidLoad
deinit

То есть, после создания нашего ViewController-a вызвался viewDidLoad. Затем, после нажатия назад, наш ViewController удалился из памяти и вызвался deinit. А наш метод в DispatchQueue не вызвался с 19-ой строки, потому что в этот момент нашего ViewController-a уже не существует, self равно nil.

Теперь посмотрим, что будет если мы уберем [weak self] из DispatchQueue и оставим его так.

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
            self.method()
        })
    }
    
    func method() {
        print("method")
    }
    
    deinit {
        print("deinit")
    }
    
}

Консоль:
viewDidLoad
method
deinit

Вызывается viewDidLoad. После пяти секунд исполняется наш метод и только потом ViewController деинитится. То есть, после нажатия назад, наш ViewController живет, пока не исполнится метод и только потом освобождается. Но утечки памяти не происходит! Потому что в итоге он удалился.

А что будет, если в DispatchQueue передать какой-нибудь closure. Вот так:

class SecondViewController: UIViewController {
    
    var closure: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        
        closure = {
            print("closure")
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: closure!)
    }
    
    func method() {
        print("method")
    }
    
    deinit {
        print("deinit")
    }
    
}

Output:
viewDidLoad
deinit
closure

Вызывается viewDidLoad. Затем удаляется ViewController. А после пяти секунд исполняется наш closure. То есть ему не важно жив ViewController или нет. У него нету ссылки на наш ViewController. Он по-любому вызовется.

А как должно быть, чтобы произошла утечка? Нужно, чтобы наш closure вызывал метод ViewController-a, то есть имел на него ссылку.

class SecondViewController: UIViewController {
    
    var closure: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        
        closure = {
            self.method()
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: closure!)
    }
    
    func method() {
        print("method")
    }
    
    deinit {
        print("deinit")
    }
    
}

В консоли:
viewDidLoad
method

Вот, в итоге deinit не вызвался и мы получили memory leak. А чтобы избавиться от него, нужно всего лишь в closure написать [weak self].

class SecondViewController: UIViewController {
    
    var closure: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        
        closure = { [weak self] in
            self?.method()
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: closure!)
    }
    
    func method() {
        print("method")
    }
    
    deinit {
        print("deinit")
    }
    
}

Консоль:
viewDidLoad
deinit

Итог

Не важно, писать [weak self] в GCD или нет, утечки памяти не будет. Но надо знать, что у них поведение разное. В первом случае, то, что внутри Dispatch-a не исполнится. А во втором — исполнится, но до его исполнения ViewController будет жить.
Поделиться публикацией

Комментарии 9

      +5
      Статья о том, что вы не знаете swift?
        0
        Как же меня раздражают вот такие напыщенные типы как ты. Ну что сразу выпендриваться. Человек поделился информацией, кому-то она будет полезна. Ну что ты из себя бога-то строишь? Ну укажи на проблемы статьи или покажи «вот это и это не верно, нужно вот так».

        Слышь, Иван Гай, критикуешь — предлагай! (с)
          0
          Человек поделился тем, что не прочитал/не разобрался в главах в книге Swift от Apple, посвященные управлению памятью. Там все это отлично объясняется.
          Меня вот больше раздражает, когда люди учатся по туториалам, а потом выдают свои откровения за что-то стоящее, еще и спорят по чём-то, а стоило просто прочитать один учебник, и тогда бы оказалось, что там все эти вопросы разжеваны давным давно и что тут даже предмета спора нет, надо просто в матчасти разобраться.
        0
        В карму гадят похоже знатоки swift. По этому вопрос к ним, и к автору этой замечательной статьи.

        Если код переписать как-то так, что изменится?

        extension Thread {
            func perform(_ block: @escaping @convention(block) () -> Void) {
                perform(#selector(run(_:)), on: self, with: block, waitUntilDone: false)
            }
            @objc private func run(_ closure: @escaping () -> Void) {
                closure()
            }
        }
        
        class ViewController: UIViewController {
            
            override func viewDidLoad() {
                super.viewDidLoad()
                Thread.detachNewThread {
                    Thread.sleep(forTimeInterval: 5)
                    Thread.main.perform {
                        self.method()
                    }
                }
            }
            
            func method() {
                print("method")
            }
        }


        В «внутри» DispatchQueue будте утечка памяти или нет?
          0
          Не вижу тут цикла удержания, соотвественно и утечки не будет.
          Но я не знаток свифта
          +2
          В первом случае, то, что внутри Dispatch-a не исполнится

          Это не так. Исполнится в любом случае. Но c weak ссылка будет уже убита, и выражение «self?.whatever()» ничего не сделает.

          GCD довольно безопасен в этом плане, там тяжело получить утечку в привычном понимании. После выполнения замыкания (или блока, если хотите), GCD освобождает ссылку него. Хотите «придержать» self до момента выполнения кода? Не пишите weak, GCD подержит замыкание, а замыкание подержит self. Наоборот, хотите избежать выполнения лишней логики и побыстрее освободить память? weak в помощь. Удобно.
            0
            Утечка будет там, где внутри замыкания идет обращение к тому же объекту, свойством которого является замыкание. Не обязательно это self.

            Не будет утечки:
            obj1.closure {
              obj2.func()
            }
            

            Будет утечка:
            obj1.closure {
              obj1.func()
            }
            
            self.closure {
              self.func()
            }
            


            Почитайте про retain cycles.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое