Swift e Objective-C

Associated Objects no Objective-C: adicionando propriedades a categorias

Categorias não podem adicionar ivar, mas podem associar valores usando Associated Objects. Este texto explica o armazenamento em uma tabela lateral ao objeto, a implementação completa de propriedades em categorias e a política de memória em que só é preciso ter cuidado com assign.

4 min de leitura
Imagem de capa de Associated Objects no Objective-C: adicionando propriedades a categorias

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.

Diagrama de como objc_setAssociatedObject registra valores na tabela associativa global do runtime
O layout do objeto permanece igual; o valor fica na tabela ao lado

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.

Ilustração da analogia de Associated Objects: uma nécessaire presa à mala com um mosquetão
É como pendurar uma nécessaire do lado de fora da mala

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, ASSIGN nã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”.

Continue lendo