No artigo “Categoria vs. extension”, resumimos que “uma categoria não pode adicionar propriedades armazenadas”. O motivo é que o layout de memória da classe é definido na compilação, deixando a categoria carregada depois sem espaço para inserir ivar.
Ainda assim, é comum encontrar propriedades funcionando normalmente em categorias no código de produção. O segredo é Associated Objects: um recurso do runtime que associa valores a objetos sem ivar.
É o caminho alternativo oficial para o problema de propriedades armazenadas em categorias e o armazenamento auxiliar que o runtime do Objective-C mantém oculto fora do objeto.
Como funciona: a tabela lateral ao objeto
Associated Objects tem duas funções principais.
objc_setAssociatedObject(objeto-alvo, chave, valor e política de memória);
objc_getAssociatedObject(objeto-alvo e chave);
O valor não fica na área de ivar do objeto. O runtime o registra em uma tabela associativa (tabela lateral) global no formato “endereço do objeto → {chave: valor}”.
O layout de memória do objeto não muda nem um byte, portanto não entra em conflito com as restrições definidas na compilação.
Quando o objeto-alvo é liberado, o runtime encontra o item correspondente na tabela associativa e o limpa conforme a política. A gestão automática do ciclo de vida é útil na prática.
Implementação completa de propriedade em categoria
Ao declarar uma propriedade em uma categoria, o compilador cria apenas as declarações de getter/setter, sem espaço de armazenamento. Associated Objects preenche essa lacuna.
#import <objc/runtime.h>
@interface UIView (BadgeCount)
@property (nonatomic, strong) NSNumber *badgeCount;
@end
@implementation UIView (BadgeCount)
- (NSNumber *)badgeCount {
return objc_getAssociatedObject(self, @selector(badgeCount));
}
- (void)setBadgeCount:(NSNumber *)badgeCount {
objc_setAssociatedObject(self, @selector(badgeCount),
badgeCount,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
Usar @selector(badgeCount)como chave é uma convenção. As chaves são comparadas pela endereço do ponteiro, não pelo valor; qualquer endereço único no app serve.
A unicidade dos seletores é garantida pelo runtime, sem precisar declarar uma variável static separada, o que torna essa opção a mais limpa. Métodos clássicos como static void *kBadgeKey = &kBadgeKey;continuam válidos.
Usar literais de string como chave é uma armadilha. Mesmo com o mesmo conteúdo, o endereço pode mudar entre unidades de compilação.
É daí que surge o mistério de “eu salvei claramente, mas veio nil”.
Política de memória: só tome cuidado com assign
O quarto argumento corresponde aos atributos da propriedade.
| Política | Atributo correspondente |
|---|---|
OBJC_ASSOCIATION_RETAIN_NONATOMIC |
strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC |
copy, nonatomic |
OBJC_ASSOCIATION_RETAIN / COPY |
Igual ao anterior, mas atomic |
OBJC_ASSOCIATION_ASSIGN |
assign — não é weak |
O único ponto de atenção é ASSIGN. Apesar do nome parecer weak, trata-se de unsafe_unretained e não vira nil automaticamente.
Se o objeto associado for liberado primeiro, sobra um ponteiro pendente. O acesso causa um crash.
Quando a semântica weak é necessária, associe com RETAIN um objeto wrapper que envolva o valor em uma propriedade weak.
Para remover um valor associado específico, faça set de nil. objc_removeAssociatedObjectsapaga todos os valores associados ao objeto e, na prática, quase nunca deve ser usado.
Usos práticos e limites
Estas são combinações comuns no código de produção.
- Extensões de classes UIKit: adicionar contagem de badge ao UIView, um handler de ação baseado em closure ao UIButton e um nome de tela para analytics ao UIViewController
- Wrapper de conversão de delegate para block: associar o objeto delegate ao objeto original para vincular seus ciclos de vida (um padrão comum nas categorias de rede anteriores ao Alamofire)
- Swizzling e companhia: usado quando a lógica inserida por swizzling precisa armazenar estado. Por isso essas duas técnicas do runtime aparecem juntas com frequência.
Ainda assim, é preciso respeitar os limites. Associated Objects é apenas um canal para dados auxiliares.
Se o estado principal do objeto ficar espalhado pela tabela associativa, será difícil entender o quadro completo. Se puder criar uma subclasse, ivar é a resposta.
Use esse recurso apenas quando precisar anexar informações extras a uma classe que não possui.
As mesmas funções também contornam a restrição de stored property em Swift extension (somente na família NSObject). No mundo Swift, porém, é mais natural combinar protocolo com tipo de armazenamento dedicado ou usar composition.
Resumo
- Associated Objects armazena valores na tabela lateral do runtime, não no objeto; por isso funciona também em categorias.
- As chaves são comparadas pelo endereço do ponteiro. Reutilizar
@selectoré a convenção; literais de string são uma armadilha. - Entre as políticas de memória,
ASSIGNnão é weak, mas unsafe_unretained. Cuidado com crashes causados por ponteiros pendentes. - Quando o objeto-alvo é liberado, os valores associados também são limpos automaticamente.
- O uso se limita a anexar dados auxiliares a classes que não possuem. Colocar o estado principal aqui é um sinal vermelho de design.
O próximo artigo da série sobre runtime será NSProxy. Vamos dissecar como outra classe raiz que não é NSObject, anunciada no artigo sobre message forwarding, funciona como um “delegado puro”.

