O Obj-C trabalha com contagem de referência para limpar memória. Toda vez que se chama o método alloc ou new memória é alocada, um novo objeto é instanciado e seu contador sobe para 1. Toda vez se envia a mensagem retain a esse objeto o contador é incrementado, toda vez que se envia a mensagem release o contador é decrementado. Quando o contador chega a zero, o sistema pode destruir o objeto (chamando também seu método dealloc) e a memória é devolvida ao sistema.
O sistema devolve memória liberado ao sistema ao final de uma execução, mas existe um caso em específico que pode dar picos de consumo de memória antes do sistema ter chance de limpá-la. Veja este trecho:
1 2 3 4 5 6 7 |
- (IBAction) onClick:(id)sender { int i; for (i = 0; i < 50000; i++) { NSString * teste = [NSString stringWithFormat:@"Teste %i", i]; NSLog(@"X: %@", teste); } } |
Imagine situações onde você está consumindo um stream de dados como de um arquivo, um web service, um parser, um tweet stream ou coisas assim, com cada ítem sendo processado em loop. Não é difícil sair criando milhares de Strings sem liberá-los.
No caso, estamos criando Strings usando o método stringWithFormat que, por convenção, devolve o String criado depois de ter já chamado o método autorelease. Explicando, você é responsável por criar e destruir objetos. Porém, quando você repassa um objeto que você criou para outro objeto, quem é o responsável por mandar release ao objeto?
Esse é o caso do stringWithFormat. Nesse caso não é nem a classe NSString nem nosso código que o chamou que vai liberar a memória ocupada por esse String. Quando você tem essa situação, em vez de mandar um release, deve enviar autorelease. Isso colocará o objeto no último NSAutoreleasePool criado. Os pools são empilháveis: os objetos sempre são colocados sob a responsabilidade do último pool criado.
Assim, como no main.m criamos um pool, os Strings serão todos colocados lá. Porém, eles serão apenas liberados depois do loop de 50 mil voltas. Por isso teremos um pico de consumo de memória até o término do loop. Dependendo do que está processando, você pode consumir toda a memória do sistema sem saber. Veja quando rodamos esse código num aplicativo via Instruments:
Note quanto de memória está sendo usada somente por CFString, mais de 1MB (!) Precisamos melhorar isso. E essa memória vai crescer quanto mais Strings forem criados dentro desse loop.
Esse padrão é fácil de identificar, basta procurar por loops que podem ser muito longos (centenas ou milhares de interações). Para “consertar” isso, podemos fazer o seguinte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (IBAction) onClick:(id)sender { int i; // cria novo pool NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; for (i = 0; i < 50000; i++) { NSString * teste = [NSString stringWithFormat:@"Teste %i", i]; NSLog(@"X: %@", teste); if (i % 1000 == 0) { // limpa o pool a cada mil interações [pool release]; // cria um novo pool vazio pool = [[NSAutoreleasePool alloc] init]; } } // limpa o último pool criado [pool release]; } |
É o mesmo código, porém criamos um novo NSAutoreleasePool especialmente para os objetos criados pelo loop. Dentro do loop limpamos o pool depois de alguma certa quantidade de interações que faça sentido, no exemplo, a cada 1000 interações. Uma vez que o pool é liberado, criamos outro vazio para poder continuar o loop. E no final garantimos que estamos liberando o último pool criado.
Com isso o consumo de memória nunca passará de um certo teto bem mais baixo que o pico causado pelo exemplo anterior. Vejamos rodando esse novo código via Instruments:
Muito melhor! Não muito mais do que 100Kb. E esse consumo é constante e não crescente como antes, o que é mais importante. Mais do que consumir pouca memória é importante conseguir comportamentos onde o consumo não passe de um certo teto.
Esse é apenas um dos aspectos do gerenciamento de memória do Objective-C que é importante que os programadores se atentem. Vou fazer mais artigos sobre esse assunto, aguardem.