Gerenciamento de memória e o ARC no Objective-C
Em outubro de 2011 a Apple lançou o iOS 5. Os desenvolvedores comemoraram bastante a disponibilização de uma funcionalidade chamada Automatic Reference Couting (ARC), que promete facilitar muito o desenvolvimento de aplicações iOS.
O desenvolvedor Objective-C precisa se preocupar com o gerenciamento de memória em suas aplicações, basicamente isso significa alocar a memória que será usada pelos objetos, utilizá-la e depois liberar essa memória. Esquecer de liberar a memória, ou liberá-la enquanto ainda está em uso, são problemas que podem causar mau funcionamento da aplicação e inclusive o famoso “crash”, fazendo-a fechar inesperadamente.
O mecanismo para gerenciamento de memória usado em Objective-C é conhecido como reference counting. Para alocar a memória manualmente, enviamos uma mensagem alloc
(uma invocação de método) para uma classe. Uma receita comum para a criação de objetos é invocar o método init
para o retorno de uma invocação ao alloc
, como no exemplo para instanciar uma NSArray
:
NSArray *array = [[NSArray alloc] init]
O retorno dessa chamada a init
é uma referência a um objeto pronto para uso, mas por quanto tempo esse objeto ficará em uso? Quando vamos permitir que ele seja varrido da memória?
Podemos invocar o retain
para um objeto e isso soma 1 ao seu contador de referências. O método release
é usado para subtrair 1 do contador, podendo possibilitar sua desalocação. Se esse balanço é comprometido (que é o que acontece quando esquecemos de invocar release
para uma referência que não será mais usada) começam aparecer os tais vazamentos de memória. Esse processo manual de alocação e liberação de memória pode ser ilustrado como na imagem abaixo:
Aliás, é bastante didático utilizar o Instruments para poder aprender e visualizar o reference count de cada objeto em memória.
A ideia do ARC é facilitar, tirando a nossa responsabilidade de invocar retain
e release
: essa tarefa passa a ser executada por debaixo dos panos, com a ajuda do compilador. Para desenvolver código que aproveita ao máximo essa funcionalidade é preciso pensar na forma como os objetos se relacionam entre si durante a execução de seu programa. Se optar pelo ARC, você não poderá mais invocar retain
e release
explicitamente, essas invocações serão incluídas automaticamente pelo compilador. Mas como o compilador decide quando usar esses métodos? Ele considera os chamados modificadores de ciclo de vida, na declaração das referências.
Quando declaramos uma propriedade podemos utilizar esses modificadores de ciclo de vida, é isso que define como um objeto é tratado em termos de gerenciamento de memória. O modificador de ciclo de vida padrão para uma proprieade é o assign
. Além dele podemos usar retain
e copy
. Com o ARC surgiram o strong
e o weak
.
assign
É o modificador padrão de ciclo de vida. A propriedade é apenas um ponteiro para o objeto atribuído a ela, não soma ou subtrai do contador de referências e portanto não interfere no gerenciamento da memória para o objeto referenciado. Se o objeto for liberado da memória em um outro ponto do código, a referência apontada pela propriedade se tornará inválida, pois passa a apontar para um endereço de memória que não existe mais. Uma invocação de método nessa propriedade irá causar um erro de execução.
@property NotaFiscal *nota;
retain
Atribuir um objeto para a propriedade declarada com esse modificador soma 1 ao seu contador de referências. Usamos retain
para tornar explícito que essa propriedade, após atribuída, estará disponível enquanto o objeto que a declara estiver sendo utilizando:
@property(retain) Produto *produto;
copy
Usado normalmente quando a propriedade se refere a um objeto que pode ser mutável como strings, arrays e dictionaries. Todos esses tipos têm subclasses que podem gerar objetos mutáveis, porém quando passamos esses objetos como parâmetro a intenção normalmente é a de usar valores no método que recebe o parâmetro, não queremos permitir nenhuma alteração no objeto original. Como uma cópia defensiva, para não expor os atributos do nosso objeto a modificações indesejadas. Se esse for o caso podemos usar o modificador copy
, isso vai garantir que o objeto seja copiado no momento da atribuição, essa cópia inicia sua vida com seu contador de referência valendo 1.
@property(copy) NSString *nomeCompleto;
Todos esses modificadores são tratados automaticamente pelo ARC. Isso significa que o próprio compilador vai se encarregar de procurar o melhor momento para adicionar envios de mensagem release para liberar a memória alocada por essas declarações, quando isso for necessário. O ARC também disponibiliza alguns modificadores novos, que vem de encontro com a ideia de livrar o desenvolvedor da preocupação com a alocação de memória:
strong
Quando um objeto é atribuído o contador de referências é incrementado, e a responsabilidade por fazer um decremento no contador é do objeto que possui a propriedade. O funcionamento é análogo ao do modificador retain
.
@property(strong) Pessoa *cliente;
weak
Atribuir um objeto não incrementa o contador, semelhante ao modificador assign
. Porém se a memória alocada para o objeto for liberada, a propriedade automaticamente torna-se nil
. Essa diferença com relação ao assign
é crucial pois se for invocado um método nessa propriedade (agora com o valor nil
) não haverá um erro de execução, como se ela fosse ignorada.
@property(weak) Endereço *endereco;
A vantagem de usar strong
e weak
com o ARC é poder pensar no grafo de objetos e suas relações, sem se preocupar com o contador de referências. Uma preocupação que ainda temos é a de garantir que não haverá nenhuma referência cíclica impedindo algum objeto de ser liberado da memória. Imagine uma propriedade chamada cliente que contém uma referência com modificador strong para um objeto do tipo Pessoa. Esse objeto internamente faz uma referência para uma instância de Endereco, veja a declaração dessas classes:
@interface Pessoa @property(strong) Endereco *endereco; @end
@interface Endereco @property(strong) Pessoa *pessoa; // outras propriedades... @end
@implementation Endereco -(id) initComLogradouro:(NSString *)_logradouro eNumero:(NSString *)_numero paraPessoa:(Pessoa *)_pessoa { self = [super init]; if(self) { self.pessoa = _pessoa; } // restante das atribuições... return self; }
Podemos pensar em um sistema de cadastro que crie objetos do tipo Pessoa
e Endereço
, e faça as associações entre eles:
@interface Cadastro @property(strong) Pessoa *pessoa; - (void) cadastraPessoaComNome:(NSString *)nome; @end
E essa poderia ser uma implementação para o método cadastraPessoa
:
- (void) cadastraPessoa:(NSString *)nome { self.pessoa = [[Pessoa alloc] initComNome:nome]; Endereco *e = [[Endereco alloc] initComLogradouro:@"Rua Vergueiro" eNumero:3185 paraPessoa:self.pessoa]; }
Dentro do método cadastraPessoa
: é criado um objeto do tipo Pessoa
, quando ele é atribuído à propriedade pessoa
, o contador de referência é incrementado. O compilador irá inserir o envio da mensagem release para a propriedade pessoa
da classe Cadastro
. Mas perceba que o contador de referências para esse objeto foi incrementado também durante a execução do inicializador initComLogradouro:eNumero:paraPessoa:
. Nesse momento o contador de referências para o objeto pessoa criado no método cadastraPessoa
: tem o total de 2.
Para o gerenciador de memória liberar o objeto do tipo Cadastro
utilizado no exemplo, uma das coisas que precisa ser feito é a liberação do objeto apontado pela propriedade do tipo Pessoa
. Para isso será subtraído 1 do contador de referências, agora o contador tem 1, e o objeto referenciado por pessoa
não será liberado da memória. Faz sentido! Mas não é o comportamento esperado, já que o objeto criado pelo cadastro deveria ser liberado da memória junto com o cadastro que o criou. É um vazamento!
Para evitar isso, podemos usar o modificador de ciclo de vida weak
na propriedade pessoa de endereço, dessa forma a propriedade será nil
quando esse objeto for liberado a qualquer momento, e não há o risco de uma referência para o objeto do tipo Pessoa
ficar pendente (dangling
). Uma forma de pensar nesse tipo de relacionamento é: endereço pertence a uma pessoa, portanto ele aponta para a pessoa de forma weak, se a pessoa for liberada, endereço não tem que interferir nisso. Já a pessoa quer ter certeza de que o endereço estará sempre disponível para ela, portanto ela aponta para o endereço de forma strong.
Vamos nos apoiar no ARC para corrigir o problema, podemos declarar as classes Pessoa
e Endereço
da seguinte forma:
@interface Pessoa @property(strong) Endereco *endereco; @end
@interface Endereco @property(weak) Pessoa *pessoa; @end
Existem modificadores específicos para variáveis de instância. Além do __strong
e do __weak
que funcionam da mesma forma que os análogos strong
e weak
utilizados em propriedades, existem também:
__unsafe_unretained
Semelhante ao _weak
, porém a variável não recebe o valor nil quando a memória é liberada, o valor continua sendo um ponteiro que agora fica pendente. Equivalente ao assign
para propriedades.
__autoreleasing
Libera a memória utilizada para armazenar um objeto passado como parâmetro para um método assim que ele retorna. Garante que dentro da execução do método o objeto vai estar disponível, mas só durante esse período.
Existem outros aspectos relacionados ao gerenciamento de memória com o ARC. Por exemplo: não é possível usar retain
e release
explícitamente com o ARC habilitado, isso irá gerar erros de compilação. Também não é possível usar objetos do tipo NSAutoreleasePool
, para essa funcionalidade devemos utilizar um bloco @autoreleasepool. É possível desabilitar o ARC em um projeto para arquivos específicos, isso significa que você pode utilizar bibliotecas “pré-ARC”, mesmo se sua aplicação quiser usar este novo contador automático de referêncais.
Vale lembrar que o código para atribuição e leitura dos dados de uma proprieade com seus devidos retains serão gerados automaticamente pelo compilador ao usar a diretiva @synthesize
.
No curso IP-67 da Caelum esse assunto é debatido e relacionado com vários outros aspectos do desenvolvimento para aplicações iOS.
Programação, Mobile, Front-end, Design & UX, Infraestrutura e Business
Bacana Valeriano
Existe alguma referência a como o ARC funciona e o funcionamento de Garbage Collector para aplicações OSX ?
Seria análogo a isso, porém no IOS ?
https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/GarbageCollection/Introduction.html
Tudo bem Fagner?
O link que você postou é a documentação para o Garbage Collector disponível para aplicações escritas para OS X, não existe nada semelhante para a plataforma iOS.
A documentação oficial para o gerenciamento de memória em aplicações iOS está disponível nesse link.
Já as notas para a transição para o modelo ARC podem ser lidas nesse outro link.
Ricardo, tudo bem?
Ouvi falar que o uso do ARC “mata” algumas características dinâmicas da linguagem. É verdade? Como isso acontece?
Achei o conceito de uso do ARC bem simples, mas quero entender um pouco mais antes de usar nos meus projetos (até porque estou bem ambientado agora com o modelo tradicional, código zero-leak 🙂
Abraço
Tudo bem Osni, e por aí tudo em ordem?
Alguns podem ter ficado com essa impressão porque o uso do ARC requer que o código seja mais explícito em alguns pontos. Por exemplo, se você criar um
seletor
para um método que o compilador não consegue encontar/reconhecer durante a compilação, será gerado um warning. Mas o ARC não vai impedir esse código de ser compilado, num caso como esse, fica novamente a cargo do programador garantir que o método existe e que se algum objeto for retornado por ele, que ele tenha seu ciclo de vida tratado adequadamente.Uma outra coisa comum quando estamos migrando para o ARC é a mudança na forma como estávamos acostumados a declarar nossos
delegates
, vou usar uma declaração fictícia de protocolo como exemplo:@protocol OmgMyProtocol
- (void)willDoSomethingWithUser:(User *)user;
@end
E agora vamos pensar em uma declaração de classe que utilize um delegate que se conforme com o protocolo
OmgMyProtocol
:@interface NiceClass : NSObject { *delegate;
NSObject
}
Note que declarei o delegate com o tipo NSObject, que é o tipo utilizado na declaração do protocolo. Declarar esse
delegate
com o “marcador” id não será aceito pelo ARC.Pode ter uma ou outra alteração na forma como programamos quando usamos o ARC mas as features dinâmicas da linguagem continuam todas por lá! Eu particularmente tenho achado que vale a pena utilizá-lo.
Ricardo,
Parabéns, pelo post!
Olá Ricardo,
Estou com um problema e talvez você possa me ajudar.
Eu estou tentando baixar o SDK do IPhone, faz uams duas semanas, ele chega até +/- 50% do download e para.
Já tentei pelo Windows (Chrome/Firefox/IE) e pelo Mac (Safari), e tenho o mesmo problema. Você sabe porque isso acontece ?
Obrigado
Sou programador Java e estou iniciando os estudos de Objective-C p/ iOS com o Livro Objective-C da série Big Nerd Ranch.
Seu post me ajudou muito, pois há diferenças gritantes entre esse ARC e o GC do java, mas vamos as dúvidas:
Não entendi muito bem o exemplo do delegate.
De qual modo não funcionaria e de qual modo funcionaria?
O que seria esse marcador id e p/ que ele serve?
Obrigado!
Como vão senhores?
@Thiago, depois que nos falamos via twitter deu tudo certo?
@Rafael O GC do Java realmente não tem muito a ver com o ARC, são duas ideias diferentes. O id em Objective-C significa “qualquer referência”. No exemplo que dei para o @Osni, eu estava me referindo especificamente a migração de um código já existente para utilização do ARC.
Para ficar mais simples e claro, vamos assumir que você está usando o ARC em sua aplicação. Para declarar um delegate em sua classe, você pode usar a seguinte sintaxe:
@property (nonatomic, weak) id delegate;
Dessa forma você cria uma referência que pode ser usada para apontar para qualquer objeto. Se o objeto for liberado da memória, sua referência chamada
delegate
fica com o valornil
, podendo receber chamadas de método sem dar erro.Ricardo, poderia tirar uma duvida minha? No curso IP-67 é abordado programação por blocos? Poderia fazer um post falando um pouco sobre isso?