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.

10 Comentários

  1. seufagner 11/04/2012 at 12:53 #

    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

  2. Ricardo Valeriano 11/04/2012 at 13:04 #

    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.

  3. Osni 11/04/2012 at 23:59 #

    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

  4. Ricardo Valeriano 12/04/2012 at 13:58 #

    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 {
    NSObject *delegate;
    }

    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.

  5. Waelson 20/04/2012 at 22:48 #

    Ricardo,

    Parabéns, pelo post!

  6. Thiago 19/05/2012 at 08:31 #

    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

  7. Rafael 29/05/2012 at 15:54 #

    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!

  8. Ricardo Valeriano 29/05/2012 at 16:27 #

    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 valor nil, podendo receber chamadas de método sem dar erro.

  9. Raul 22/08/2012 at 22:06 #

    Ricardo, poderia tirar uma duvida minha? No curso IP-67 é abordado programação por blocos? Poderia fazer um post falando um pouco sobre isso?

Deixe uma resposta