Swift: ARC e gerenciamento de memória

Sendo uma linguagem moderna de alto nível, o Swift basicamente cuida do gerenciamento de memória em seus aplicativos, alocando e liberando memória. Isso ocorre devido a um mecanismo chamado contagem automática de referência , ou ARC, para abreviar. Neste guia, você aprenderá como o ARC funciona e como gerenciar adequadamente a memória no Swift. Noções básicas sobre esse mecanismo, você pode influenciar a vida útil dos objetos localizados na pilha ( pilha ).

Neste guia, você desenvolverá seu conhecimento sobre Swift e ARC aprendendo o seguinte:

  • como o ARC funciona
  • o que são ciclos de referência e como corrigi-los corretamente
  • como criar um loop de link de exemplo
  • Como encontrar loops de link usando as ferramentas visuais oferecidas pelo Xcode
  • como lidar com tipos de referência e tipos de valor

Introdução


Faça o download dos materiais de origem. Abra o projeto na pasta Cycles / Starter . Na primeira parte do nosso guia, entendendo os principais conceitos, trataremos exclusivamente do arquivo MainViewController.swif t.

Adicione esta classe na parte inferior do MainViewController.swift:

class User { let name: String init(name: String) { self.name = name print("User \(name) was initialized") } deinit { print("Deallocating user named: \(name)") } } 

A classe User é definida aqui, a qual, com a ajuda das instruções print , nos sinaliza sobre a inicialização e o lançamento da instância da classe.

Agora crie uma instância da classe User na parte superior do MainViewController.

Coloque esse código antes do método viewDidLoad () :

 let user = User(name: "John") 

Inicie o aplicativo. Torne o console do Xcode visível com Command-Shift-Y para ver a saída das instruções de impressão.

Observe que o usuário John foi inicializado apareceu no console, mas a instrução print dentro do deinit não foi executada. Isso significa que esse objeto não foi lançado, pois não saiu do escopo .

Em outras palavras, até que o controlador de exibição que contém este objeto fique fora do escopo, o objeto nunca será liberado.

Ele está no escopo?


Ao envolver uma instância da classe User em um método, permitiremos que ela fique fora do escopo, permitindo que o ARC a liberte.

Vamos criar o método runScenario () dentro da classe MainViewController e mover a inicialização da instância da classe User dentro dela.

 func runScenario() { let user = User(name: "John") } 

runScenario () define o escopo da instância do usuário. Ao sair desta zona, o usuário deve ser liberado.

Agora chame runScenario () adicionando isso no final de viewDidLoad ():

 runScenario() 

Inicie o aplicativo. A saída do console agora fica assim:

O usuário John foi inicializado
Desalocando usuário chamado: John

Isso significa que você liberou um objeto que saiu do campo de visão.

Duração do objeto



A existência do objeto é dividida em cinco estágios:

  • alocação de memória: da pilha ou da pilha
  • inicialização: o código é executado dentro do init
  • uso de
  • desinicialização: o código é executado dentro do deinit
  • memória livre: a memória alocada é retornada à pilha ou pilha

Não existe uma maneira direta de rastrear as etapas de alocação e liberação de memória, mas você pode usar o código dentro de init e deinit.

As contagens de referência , também conhecidas como contagens de uso , determinam quando um objeto não é mais necessário. Este contador mostra o número daqueles que "usam" este objeto. Um objeto se torna desnecessário quando o contador de uso é zero. Em seguida, o objeto é dessinicializado e liberado.



Quando o objeto Usuário é inicializado, sua contagem de referência é 1, porque a constante do usuário se refere a esse objeto.

No final de runScenario (), o usuário fica fora do escopo e a contagem de referência é reduzida para 0. Como resultado, o usuário é não inicializado e liberado.

Ciclos de referência


Na maioria dos casos, o ARC funciona como deveria. O desenvolvedor geralmente não precisa se preocupar com vazamentos de memória quando objetos não utilizados permanecem não alocados indefinidamente.

Mas nem sempre! Possíveis vazamentos de memória.

Como isso pode acontecer? Imagine uma situação em que dois objetos não sejam mais usados, mas cada um deles se refira ao outro. Como cada contagem de referência não é 0, nenhum deles será liberado.



Este é um forte ciclo de referência . Essa situação confunde o ARC e não permite que ele limpe a memória.

Como você pode ver, a contagem de referência no final não é 0 e, embora nenhum objeto seja mais necessário, o objeto1 e o objeto2 não serão liberados.

Confira nossos links


Para testar tudo isso em ação, adicione esse código após a classe User em MainViewController.swift:

 class Phone { let model: String var owner: User? init(model: String) { self.model = model print("Phone \(model) was initialized") } deinit { print("Deallocating phone named: \(model)") } } 

Esse código adiciona uma nova classe Phone com duas propriedades, uma para o modelo e outra para o proprietário, além dos métodos init e deinit. A propriedade do proprietário é opcional, pois o telefone pode não ter um proprietário.

Agora adicione esta linha ao runScenario ():

 let iPhone = Phone(model: "iPhone Xs") 

Isso criará uma instância da classe Phone.

Segure o celular


Agora adicione esse código à classe User, imediatamente após a propriedade name:

 private(set) var phones: [Phone] = [] func add(phone: Phone) { phones.append(phone) phone.owner = self } 

Adicione uma variedade de telefones pertencentes ao usuário. O setter está marcado como privado, portanto, adicione (telefone :) deve ser usado.

Inicie o aplicativo. Como você pode ver, as instâncias das classes de objetos Telefone e Usuário são liberadas conforme necessário.

O usuário John foi inicializado
O telefone iPhone XS foi inicializado
Desalocando o telefone com o nome: iPhone Xs
Desalocando usuário chamado: John

Agora adicione isso no final do runScenario ():
 user.add(phone: iPhone) 


Aqui, adicionamos nosso iPhone à lista de telefones pertencentes ao usuário e também definimos a propriedade do proprietário do telefone como ' usuário '.

Execute o aplicativo novamente. Você verá que os objetos de usuário e iPhone não são liberados. O ciclo de fortes vínculos entre eles impede o ARC de liberá-los.



Links Fracos


Para interromper o ciclo de vínculos fortes, você pode designar o relacionamento entre objetos como fraco.

Por padrão, todos os links são fortes e a atribuição leva a um aumento na contagem de referência. Ao usar referências fracas, a contagem de referências não aumenta.

Em outras palavras, os elos fracos não afetam o gerenciamento da vida de um objeto . Links fracos são sempre declarados opcionais . Dessa forma, quando a contagem de links se tornar 0, o link poderá ser definido como nulo.



Nesta ilustração, linhas tracejadas indicam links fracos. Observe que a contagem de referência do objeto1 é 1, pois a variável1 se refere a ele. A contagem de referência do objeto2 é 2, porque é referenciada pela variável2 e pelo objeto1.

O objeto2 também faz referência ao objeto1, mas WEAK , o que significa que ele não afeta a contagem de referências do objeto1.

Quando a variável1 e a variável2 são liberadas, o objeto1 tem uma contagem de referência 0, que a libera. Isso, por sua vez, libera uma forte referência ao objeto2, que já leva ao seu lançamento.

Na classe Phone, altere a declaração de propriedade do proprietário da seguinte maneira:

 weak var owner: User? 

Ao declarar a referência da propriedade do proprietário como 'fraca', interrompemos o ciclo de vínculos fortes entre as classes User e Phone.



Inicie o aplicativo. Agora, usuário e telefone foram liberados corretamente.

Links não proprietários


Há também outro modificador de link que não aumenta a contagem de referência: sem dono .

Qual a diferença entre não dono e fraco ? Uma referência fraca é sempre opcional e se torna automaticamente nula quando o objeto referenciado é liberado.

É por isso que devemos declarar propriedades fracas como uma variável opcional do tipo: essa propriedade deve mudar.

Os links não proprietários, por outro lado, nunca são opcionais. Se você tentar acessar uma propriedade não proprietária que se refere a um objeto liberado, receberá um erro que se parece com um desembrulhamento forçado contendo uma variável nula (forçar desembrulhamento).



Vamos tentar aplicar sem dono .

Adicione uma nova classe CarrierSubscription no final do MainViewController.swift:

 class CarrierSubscription { let name: String let countryCode: String let number: String let user: User init(name: String, countryCode: String, number: String, user: User) { self.name = name self.countryCode = countryCode self.number = number self.user = user print("CarrierSubscription \(name) is initialized") } deinit { print("Deallocating CarrierSubscription named: \(name)") } } 

CarrierSubscription possui quatro propriedades:

Nome: nome do provedor.
CountryCode: código do país.
Número: número de telefone.
Usuário: link para o usuário.

Quem é o seu provedor?


Agora adicione isso à classe User após a propriedade name:

 var subscriptions: [CarrierSubscription] = [] 

Aqui mantemos uma variedade de provedores de usuários.

Agora adicione isso à classe Phone, após a propriedade do proprietário:

 var carrierSubscription: CarrierSubscription? func provision(carrierSubscription: CarrierSubscription) { self.carrierSubscription = carrierSubscription } func decommission() { carrierSubscription = nil } 

Isso adiciona a propriedade opcional CarrierSubscription e dois métodos para registrar e cancelar o registro do telefone com o provedor.

Agora adicione a classe CarrierSubscription dentro do método init, logo antes da instrução print:

 user.subscriptions.append(self) 

Adicionamos CarrierSubscription à matriz de provedores de usuários.

Por fim, adicione isso no final do método runScenario ():

 let subscription = CarrierSubscription( name: "TelBel", countryCode: "0032", number: "31415926", user: user) iPhone.provision(carrierSubscription: subscription) 

Criamos uma assinatura do provedor para o usuário e conectamos o telefone a ele.

Inicie o aplicativo. No console, você verá:

O usuário John foi inicializado
Telefone iPhone Xs foi inicializado
CarrierSubscription TelBel é inicializado

E novamente um ciclo de links! usuário, iPhone e assinatura não foram liberados no final.

Você consegue encontrar um problema?



Quebrando a corrente


O link do usuário para a assinatura ou o link da assinatura para o usuário deve ser não proprietário para interromper o ciclo. A questão é qual opção escolher. Vamos olhar para as estruturas.

Um usuário possui uma assinatura de um provedor, mas vice-versa - não, uma assinatura de um provedor não possui um usuário.

Além disso, não faz sentido a existência de CarrierSubscription sem referência ao usuário que o possui.

Portanto, o link do usuário deve ser sem dono.

Altere a declaração do usuário em CarrierSubscription:

 unowned let user: User 

Agora, o usuário não é proprietário, o que quebra o ciclo dos links e permite liberar todos os objetos.



Loop de links em fechamentos


Os ciclos de link para objetos ocorrem quando os objetos têm propriedades que se referem um ao outro. Como objetos, os fechamentos são um tipo de referência e podem levar a loops de referência. Os fechamentos capturam objetos que eles usam.

Por exemplo, se você atribuir um fechamento a uma propriedade de uma classe, e esse fechamento usar propriedades da mesma classe, obteremos um loop de links. Em outras palavras, o objeto mantém um link para o fechamento através da propriedade O fechamento contém uma referência ao objeto através do valor capturado de si mesmo.



Adicione este código ao CarrierSubscription imediatamente após a propriedade do usuário:

 lazy var completePhoneNumber: () -> String = { self.countryCode + " " + self.number } 

Esse fechamento calcula e retorna o número de telefone completo. A propriedade é declarada preguiçosa , será atribuída no primeiro uso.

Isso é necessário porque ele usa self.countryCode e self.number, que não estarão disponíveis até que o código do inicializador seja executado.

Adicione runScenario () ao final:

 print(subscription.completePhoneNumber()) 

A chamada completePhoneNumber () executará o fechamento.

Inicie o aplicativo e você verá que o usuário e o iPhone foram liberados, mas CarrierSubscription não é, devido a um ciclo de fortes vínculos entre o objeto e o fechamento.



Listas de captura


O Swift fornece uma maneira simples e elegante de quebrar o ciclo de vínculos fortes nos fechamentos. Você declara uma lista de captura na qual define o relacionamento entre o fechamento e os objetos que ele captura.

Para demonstrar a lista de captura, considere o seguinte código:

 var x = 5 var y = 5 let someClosure = { [x] in print("\(x), \(y)") } x = 6 y = 6 someClosure() // Prints 5, 6 print("\(x), \(y)") // Prints 6, 6 

x está na lista de captura de fechamento, portanto, o valor de x é copiado para a definição de fechamento. É capturado pelo valor.

y não está na lista de capturas, é capturado por referência. Isso significa que o valor de y será o que era no momento em que o circuito foi chamado.

As listas de bloqueio ajudam a identificar interações fracas ou não proprietárias com relação aos objetos capturados dentro do loop. No nosso caso, a escolha apropriada não é possuída, pois um fechamento não pode existir se a instância CarrierSubscription for liberada.

Aproveite-se


Substitua a definição completePhoneNumber por CarrierSubscription ::

 lazy var completePhoneNumber: () -> String = { [unowned self] in return self.countryCode + " " + self.number } 

Nós adicionamos [self sem dono] à lista de captura de fechamento. Isso significa que nos capturamos como um elo sem dono em vez de forte.

Inicie o aplicativo e você verá que o CarrierSubscription foi lançado.

De fato, a sintaxe acima é uma forma abreviada de uma mais longa e mais completa, na qual uma nova variável aparece:

 var closure = { [unowned newID = self] in // Use unowned newID here... } 

Aqui newID é uma cópia sem dono do eu. Além do fechamento, o eu permanece em si. Na forma abreviada dada anteriormente, criamos uma nova variável auto que obscurece o eu existente dentro do fechamento.

Use Unowned cuidadosamente


No seu código, o relacionamento entre self e completePhoneNumber é designado como não proprietário.

Se você tiver certeza de que o objeto usado no fechamento não será liberado, poderá usar o não proprietário. Se ele faz, você está com problemas!

Adicione este código no final do MainViewController.swift:

 class WWDCGreeting { let who: String init(who: String) { self.who = who } lazy var greetingMaker: () -> String = { [unowned self] in return "Hello \(self.who)." } } 

Agora, aqui está o fim do runScenario ():

 let greetingMaker: () -> String do { let mermaid = WWDCGreeting(who: "caffeinated mermaid") greetingMaker = mermaid.greetingMaker } print(greetingMaker()) // ! 

Inicie o aplicativo e você verá uma falha e algo assim no console:

O usuário John foi inicializado
O telefone iPhone XS foi inicializado
CarrierSubscription TelBel é inicializado
0032 31415926
Erro fatal: tentativa de ler uma referência sem dono, mas o objeto 0x600000f0de30 já foi desalocado2019-02-24 12: 29: 40.744248-0600 Ciclos [33489: 5926466] Erro fatal: tentativa de ler uma referência sem dono, mas o objeto 0x600000f0de30 já foi desalocado

Uma exceção ocorreu porque o fechamento aguarda a existência de si mesmo, mas foi liberado assim que a sereia saiu do escopo no final do bloco do.

Este exemplo pode parecer sugado de um dedo, mas essas coisas acontecem. Por exemplo, quando usamos fechamentos para iniciar algo muito mais tarde, digamos, após o término da chamada assíncrona na rede.

Desarmar a armadilha


Substitua greetingMaker na classe WWDCGreeting por esta:

 lazy var greetingMaker: () -> String = { [weak self] in return "Hello \(self?.who)." } 

Fizemos duas coisas: primeiro, substituímos os não proprietários pelos fracos. Em segundo lugar, como o eu se tornou fraco, acessamos a propriedade who através do eu? Ignore o aviso do Xcode, iremos corrigi-lo em breve.

O aplicativo não trava mais, mas se você executá-lo, obtemos um resultado engraçado: "Olá, nada".

Talvez o resultado seja bastante aceitável, mas geralmente precisamos fazer algo se o objeto foi liberado. Isso pode ser feito usando a declaração de guarda.

Substitua o texto do fechamento por este:

 lazy var greetingMaker: () -> String = { [weak self] in guard let self = self else { return "No greeting available." } return "Hello \(self.who)." } 

A declaração de guarda designa o eu retirado do eu fraco. Se self for nulo, o fechamento retornará "Nenhuma saudação disponível". Caso contrário, o eu se torna uma referência forte, de modo que o objeto é garantido até o final do fechamento.

Procurando loops de link no Xcode 10


Agora que você entende como o ARC funciona, o que são loops de link e como quebrá-los, é hora de ver um exemplo de aplicativo real.

Abra o projeto Starter localizado na pasta Contatos.

Inicie o aplicativo.



Este é o gerenciador de contatos mais simples. Tente clicar em um contato, adicione alguns novos.

Atribuição de arquivo:

ContactsTableViewController: mostra todos os contatos.
DetailViewController: mostra as informações detalhadas do contato selecionado.
NewContactViewController: permite adicionar um novo contato.
ContactTableViewCell: célula da tabela mostrando detalhes do contato.
Contato: modelo de contato.
Número: modelo de número de telefone.

No entanto, com este projeto, tudo está ruim: há um ciclo de links. No início, os usuários não perceberão problemas devido ao pequeno tamanho da memória que vazou, pelo mesmo motivo que é difícil encontrar o vazamento.

Felizmente, o Xcode 10 possui ferramentas integradas para encontrar o menor vazamento de memória.

Inicie o aplicativo novamente. Exclua 3-4 contatos usando o furto à esquerda e o botão excluir. Parece que eles desaparecem completamente, certo?



Para onde flui?


Quando o aplicativo estiver em execução, clique no botão Debug Memory Graph:



Observe os problemas de tempo de execução no navegador Debug. Eles são marcados com quadrados roxos com um ponto de exclamação branco dentro:



Selecione um dos objetos de contato problemáticos no navegador. O ciclo é claramente visível: os objetos Contato e Número, referindo-se um ao outro, permanecem.



Parece que você deve procurar no código. Lembre-se de que um contato pode existir sem um número, mas não vice-versa.

Como você resolveria esse loop? Link de contato para número ou de número para contato? fraco ou sem dono? Tente você mesmo primeiro!

Se você precisasse de ajuda ...
Existem 2 soluções possíveis: faça um link de Contato para Número fraco ou de Número para Contato sem dono.

A documentação da Apple recomenda que o objeto pai tenha uma forte referência a "filho" - e não vice-versa. Isso significa que damos ao Contato uma forte referência ao Número e Número - um link não proprietário para o Contato:

 class Number { unowned var contact: Contact // Other code... } class Contact { var number: Number? // Other code... } 


Bônus: loops com tipos de referência e tipos de valor.


Swift tem tipos de referência (classes e fechamentos) e tipos de valor (estruturas, enumerações). O tipo de valor é copiado quando é passado e os tipos de referência compartilham o mesmo valor usando o link.

Isso significa que, no caso de tipos de valor, não pode haver ciclos. Para que um loop ocorra, precisamos de pelo menos 2 tipos de referência.

Vamos voltar ao projeto Cycles e adicionar esse código no final do MainViewController.swift:

 struct Node { // Error var payload = 0 var next: Node? } 

Não vai funcionar! Estrutura é um tipo de valor e não pode ter recursão em uma instância de si mesma. Caso contrário, essa estrutura teria tamanho infinito.

Mude a estrutura para uma classe.

 class Node { var payload = 0 var next: Node? } 

A referência a si mesma é bastante aceitável para classes (tipo de referência), portanto, o compilador não tem problemas.

Agora adicione isso no final do MainViewController.swift:

 class Person { var name: String var friends: [Person] = [] init(name: String) { self.name = name print("New person instance: \(name)") } deinit { print("Person instance \(name) is being deallocated") } } 

E isso é no final do runScenario ():

 do { let ernie = Person(name: "Ernie") let bert = Person(name: "Bert") ernie.friends.append(bert) // Not deallocated bert.friends.append(ernie) // Not deallocated } 

Inicie o aplicativo. Observe: nem ernie nem bert são liberados.

Link e significado


Este é um exemplo de uma combinação de um tipo de referência e um tipo de valor que levou a um loop de links.

ernie e bert permanecem inéditos, mantendo-se nas matrizes de seus amigos, embora as matrizes sejam tipos de valor.

Tente fazer com que os amigos arquivem como sem dono, e o Xcode mostrará um erro: sem dono se aplica apenas a classes.

Para corrigir esse loop, precisamos criar um objeto wrapper e usá-lo para adicionar instâncias ao array.

Adicione a seguinte definição antes da classe Person:

 class Unowned<T: AnyObject> { unowned var value: T init (_ value: T) { self.value = value } } 

Altere a definição de amigos na classe Person:

 var friends: [Unowned<Person>] = [] 

Por fim, substitua o conteúdo do bloco do em runScenario ():

 do { let ernie = Person(name: "Ernie") let bert = Person(name: "Bert") ernie.friends.append(Unowned(bert)) bert.friends.append(Unowned(ernie)) } 

Inicie o aplicativo, agora ernie e bert foram lançados corretamente!

A matriz de amigos não é mais uma coleção de objetos Pessoa. Agora, essa é uma coleção de objetos não proprietários que servem como wrappers para instâncias de Person.

Para obter objetos Person de Unowned, use a propriedade value:

 let firstFriend = bert.friends.first?.value // get ernie 

Conclusão


Agora você tem um bom entendimento do gerenciamento de memória no Swift e sabe como o ARC funciona. Espero que a publicação tenha sido útil para você.

Apple: contagem automática de referência

Source: https://habr.com/ru/post/pt451130/


All Articles