Swift e Objective-C

+load vs +initialize: momento da chamada e armadilhas de herança

Por que o código de method swizzling sempre fica dentro de +load? Não dá para colocá-lo no +initialize, que parece semelhante?

5 min de leitura
Imagem de capa de +load vs +initialize: momento da chamada e armadilhas de herança

Por que o código de method swizzling sempre fica dentro de +load? Não dá para colocá-lo no +initialize, que parece semelhante?

+load e +initialize parecem métodos “chamados uma vez quando a classe está pronta”, mas diferem completamente no momento e na forma da chamada e nas regras de herança. Também é um tema frequente em entrevistas de Objective-C e, quando mal compreendido, é fácil ficar se perguntando: “Por que este código está sendo executado duas vezes?”. Vale a pena resumir o assunto.


Tabela comparativa rápida

+load +initialize
Momento da chamada Quando a classe é carregada pelo runtime (antes de main) Imediatamente antes de a classe receber sua primeira mensagem (lazy)
Forma da chamada Chamada direta por ponteiro de função Via objc_msgSend
Categorias As implementações da classe e da categoria são chamadas separadamente A implementação da categoria sobrescreve a implementação da classe
Herança Somente a classe que implementa é chamada É herdado — pode ser chamado várias vezes por causa das subclasses
Se não for usado? Ainda assim é chamado Nunca é chamado se a classe não for usada

Vamos detalhar os pontos que não ficam claros apenas pela tabela.


+load: antes de main, incondicionalmente

+load é chamado quando o runtime carrega o binário que contém a classe ou categoria, ou seja, antes mesmo de a função main ser executada. A chamada ocorre mesmo que o app nunca use essa classe.

A forma da chamada é peculiar. Ele é chamado diretamente por um ponteiro de função, sem passar por objc_msgSend. Por isso, as regras normais de sobrescrita não se aplicam.

  • Se uma subclasse não implementa +load, a implementação do pai não é chamada em seu lugar.
  • A +load da classe e a +load da categoria são chamadas separadamente — isso não é sobrescrita.
  • A ordem é garantida: a +load da classe pai vem antes da filha, e a +load da classe vem antes da categoria.

Essas características combinam perfeitamente com swizzling. Mesmo implementando +load em uma categoria, isso não interfere na +load da classe original e é executado uma vez, com garantia, no momento mais cedo do ciclo de vida do app.

Mas há um custo. +load é cobrado diretamente do tempo de inicialização do app. Como a +load de todas as classes é executada sequencialmente antes de main, trabalhos pesados aqui atrasam a primeira tela. É por isso que a Apple recomenda há anos “evitar +load sempre que possível”. Na prática, dentro de +load, outras classes fora da imagem à qual self pertence podem ainda não ter sido carregadas, o que limita o que pode ser feito.

Diagrama mostrando os momentos de chamada de +load e +initialize na linha do tempo de execução do app
A única bifurcação é passar ou não por msgSend

+initialize: logo antes da primeira mensagem, de forma lazy

+initialize adota a estratégia oposta. O runtime o chama imediatamente antes de a classe receber sua primeira mensagem. Se o app nunca usar a classe, ela nunca será chamada. É um ponto de inicialização lazy que não pesa no tempo de inicialização.

Como passa por objc_msgSend, as regras de herança se aplicam como em um método normal. É exatamente aqui que surge a famosa armadilha.

@implementation Animal
+ (void)initialize {
    NSLog(@"initialize: %@", self);
}
@end

@interface Dog : Animal
@end
@implementation Dog
@end

Ao enviar a primeira mensagem para Dog, o log fica assim.

initialize: Animal
initialize: Dog

A +initialize de Animal é executada duas vezes: uma vez pela própria Animal e outra porque Dog, que não implementa +initialize, herda e executa a implementação do pai. Por isso, a implementação convencional de +initialize inclui uma verificação de classe.

+ (void)initialize {
    if (self == [Animal class]) {
        // Somente a inicialização que é realmente  Animal responsabilidade própria
    }
}

A propósito, +initialize é thread-safe por si só, pois o runtime usa um lock por classe antes de chamá-lo. Não é necessário adicionar dispatch_once.


Critérios práticos de escolha

O critério é simples.

  • Tarefas que precisam ser feitas “incondicionalmente e o mais cedo possível”, como swizzling e registro de classes+load (mas de forma mínima)
  • Preparação necessária apenas quando a classe é usada+initialize (a verificação de self é obrigatória)
  • A maior parte das inicializações → na verdade, nenhuma das duas; um dispatch_oncesingleton ou uma propriedade lazy é suficiente

Swift não tem esse dilema. Swift não oferece nada correspondente a +load. Também não existe um mecanismo oficial para inserir código executável global antes de main. Em vez disso, propriedades de tipo (static let) garantem inicialização lazy e segurança de thread no nível da linguagem, substituindo o papel de +initialize. Esse é um dos pontos em que Swift é estruturalmente vantajoso para o desempenho de inicialização do app.

Ilustração que representa +load antes de main e +initialize lazy como uma pista de corrida
Um começa a correr antes da largada; o outro dorme até a primeira chamada

Resumo

  • +load é chamado antes de main, incondicionalmente, diretamente por ponteiro de função — as implementações da classe e da categoria são chamadas separadamente
  • +initialize é chamado logo antes da primeira mensagem, de forma lazy e via msgSend — nunca é chamado em classes não usadas
  • +initialize pode ser executado várias vezes por causa da herança, portanto a if (self == [MyClass class]) verificação é convencional
  • Por que o swizzling fica em +load: momento mais cedo + chamada independente da categoria + execução única garantida
  • O uso excessivo de +load atrasa diretamente a inicialização do app — adie inicializações pesadas usando lazy

Ao ler este texto junto com outros artigos da série sobre runtime (objc_msgSend, encaminhamento de mensagens, swizzling e KVO), fica claro que a diferença entre +load e +initialize acaba se dividindo em um único eixo: passar ou não por msgSend. Ao entender o caminho da chamada, você não precisa decorar as regras.

Continue lendo