A resposta de Vladimir é realmente muito boa, no entanto, gostaria de dar um pouco mais de conhecimento aqui. Talvez um dia alguém encontre minha resposta e possa ser útil.
O compilador transforma os arquivos de origem (.c, .cc, .cpp, .m) em arquivos de objeto (.o). Há um arquivo de objeto por arquivo de origem. Os arquivos de objetos contêm símbolos, código e dados. Os arquivos de objeto não podem ser usados diretamente pelo sistema operacional.
Agora, ao criar uma biblioteca dinâmica (.dylib), uma estrutura, um pacote configurável carregável (.bundle) ou um binário executável, esses arquivos de objeto são vinculados pelo vinculador para produzir algo que o sistema operacional considera "utilizável", por exemplo, algo que pode carregar diretamente em um endereço de memória específico.
No entanto, ao criar uma biblioteca estática, todos esses arquivos de objeto são simplesmente adicionados a um grande arquivo morto, daí a extensão de bibliotecas estáticas (.a para arquivo morto). Portanto, um arquivo .a nada mais é do que um arquivo morto de objetos (.o). Pense em um arquivo TAR ou em um arquivo ZIP sem compactação. É mais fácil copiar um único arquivo .a do que um monte de arquivos .o (semelhante ao Java, onde você agrupa arquivos .class em um arquivo .jar para facilitar a distribuição).
Ao vincular um binário a uma biblioteca estática (= arquivo morto), o vinculador obtém uma tabela de todos os símbolos no arquivo morto e verifica quais desses símbolos são referenciados pelos binários. Somente os arquivos de objeto que contêm símbolos referenciados são realmente carregados pelo vinculador e são considerados pelo processo de vinculação. Por exemplo, se o seu arquivo possui 50 arquivos de objeto, mas apenas 20 contêm símbolos usados pelo binário, apenas os 20 são carregados pelo vinculador, os outros 30 são totalmente ignorados no processo de vinculação.
Isso funciona muito bem para os códigos C e C ++, pois essas linguagens tentam fazer o máximo possível em tempo de compilação (embora o C ++ também tenha alguns recursos apenas de tempo de execução). Obj-C, no entanto, é um tipo diferente de linguagem. O Obj-C depende muito dos recursos de tempo de execução e muitos recursos de Obj-C são, na verdade, somente de tempo de execução. As classes Obj-C realmente têm símbolos comparáveis às funções C ou variáveis C globais (pelo menos no tempo de execução atual do Obj-C). Um vinculador pode ver se uma classe é referenciada ou não, para determinar se uma classe está sendo usada ou não. Se você usar uma classe de um arquivo de objeto em uma biblioteca estática, esse arquivo de objeto será carregado pelo vinculador porque o vinculador vê um símbolo em uso. Categorias são um recurso apenas de tempo de execução, categorias não são símbolos como classes ou funções e isso também significa que um vinculador não pode determinar se uma categoria está em uso ou não.
Se o vinculador carregar um arquivo de objeto contendo o código Obj-C, todas as partes dele serão sempre parte do estágio de vinculação. Portanto, se um arquivo de objeto contendo categorias for carregado porque qualquer símbolo dele é considerado "em uso" (seja uma classe, seja uma função, seja uma variável global), as categorias também serão carregadas e estarão disponíveis no tempo de execução . No entanto, se o próprio arquivo de objeto não for carregado, as categorias nele não estarão disponíveis no tempo de execução. Um arquivo de objeto contendo apenas categorias nunca é carregado porque não contém símbolos que o vinculador jamais considerar "em uso". E este é todo o problema aqui.
Várias soluções foram propostas e agora que você sabe como tudo isso funciona em conjunto, vamos dar uma olhada na solução proposta:
Uma solução é adicionar -all_load
à chamada do vinculador. O que esse sinalizador de vinculador realmente fará? Na verdade, ele diz ao vinculador o seguinte " Carregue todos os arquivos de objeto de todos os arquivos, independentemente de você ver ou não algum símbolo '. É claro que isso funcionará; mas também poderá produzir binários bastante grandes.
Outra solução é adicionar -force_load
à chamada do vinculador, incluindo o caminho para o arquivo morto. Esse sinalizador funciona exatamente como -all_load
, mas apenas para o arquivo especificado. Claro que isso também funcionará.
A solução mais popular é adicionar -ObjC
à chamada do vinculador. O que esse sinalizador de vinculador realmente fará? Esse sinalizador informa ao vinculador " Carregue todos os arquivos de objetos de todos os arquivos, se você vir que eles contêm algum código Obj-C ". E "qualquer código Obj-C" inclui categorias. Isso também funcionará e não forçará o carregamento de arquivos de objetos que não contêm código Obj-C (eles ainda são carregados somente sob demanda).
Outra solução é a nova configuração de compilação do Xcode Perform Single-Object Prelink
. O que essa configuração fará? Se ativado, todos os arquivos de objeto (lembre-se, existe um por arquivo de origem) são mesclados em um único arquivo de objeto (que não é um vínculo real, daí o nome PreLink ) e esse arquivo de objeto único (às vezes também chamado de "objeto mestre" arquivo ") é então adicionado ao arquivo. Se agora qualquer símbolo do arquivo de objeto principal for considerado em uso, todo o arquivo de objeto principal será considerado em uso e, portanto, todas as partes do Objective-C dele serão sempre carregadas. E como as classes são símbolos normais, basta usar uma classe de uma biblioteca estática para obter também todas as categorias.
A solução final é o truque que Vladimir adicionou no final de sua resposta. Coloque um " símbolo falso " em qualquer arquivo de origem, declarando apenas categorias. Se você deseja usar qualquer uma das categorias no tempo de execução, certifique-se de alguma forma referenciar o símbolo falso no tempo de compilação, pois isso faz com que o arquivo de objeto seja carregado pelo vinculador e, portanto, também todo o código Obj-C nele. Por exemplo, pode ser uma função com um corpo de função vazio (que não fará nada ao ser chamado) ou pode ser uma variável global acessada (por exemplo, uma variável globalint
uma vez lido ou escrito, isso é suficiente). Diferente de todas as outras soluções acima, essa solução muda o controle sobre quais categorias estão disponíveis em tempo de execução para o código compilado (se desejar que elas estejam vinculadas e disponíveis, acessa o símbolo, caso contrário, não acessa o símbolo e o vinculador ignorará isto).
Isso é tudo, pessoal.
Ah, espere, há mais uma coisa:
o vinculador tem uma opção chamada -dead_strip
. O que essa opção faz? Se o vinculador decidir carregar um arquivo de objeto, todos os símbolos do arquivo de objeto se tornarão parte do binário vinculado, sejam eles usados ou não. Por exemplo, um arquivo de objeto contém 100 funções, mas apenas uma delas é usada pelo binário, todas as 100 funções ainda são adicionadas ao binário porque os arquivos de objeto são adicionados como um todo ou não são adicionados. A adição parcial de um arquivo de objeto geralmente não é suportada por vinculadores.
No entanto, se você instruir o vinculador a "faixa inoperante", o vinculador primeiro adicionará todos os arquivos de objeto ao binário, resolverá todas as referências e finalmente examinará o binário em busca de símbolos que não estejam em uso (ou apenas em uso por outros símbolos que não estejam em uso). usar). Todos os símbolos que não estão em uso são removidos como parte do estágio de otimização. No exemplo acima, as 99 funções não utilizadas são removidas novamente. Isso é muito útil se você usar opções como -load_all
, -force_load
ou Perform Single-Object Prelink
porque essas opções podem facilmente explodir tamanhos binários drasticamente em alguns casos, e a remoção morta removerá código e dados não utilizados novamente.
A remoção morta funciona muito bem para o código C (por exemplo, funções, variáveis e constantes não utilizadas são removidas conforme o esperado) e também funciona muito bem para C ++ (por exemplo, as classes não utilizadas são removidas). Não é perfeito; em alguns casos, alguns símbolos não são removidos, mesmo que seja bom removê-los, mas na maioria dos casos funciona muito bem para esses idiomas.
E o Obj-C? Esqueça isso! Não há remoção morta para Obj-C. Como o Obj-C é uma linguagem de recurso de tempo de execução, o compilador não pode dizer em tempo de compilação se um símbolo está realmente em uso ou não. Por exemplo, uma classe Obj-C não está em uso se não houver código que faça referência direta a ela, correto? Errado! Você pode criar dinamicamente uma sequência contendo um nome de classe, solicitar um ponteiro de classe para esse nome e alocar dinamicamente a classe. Por exemplo, em vez de
MyCoolClass * mcc = [[MyCoolClass alloc] init];
Eu também poderia escrever
NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];
Nos dois casos, mmc
há uma referência a um objeto da classe "MyCoolClass", mas não há referência direta a essa classe no segundo exemplo de código (nem mesmo o nome da classe como uma sequência estática). Tudo acontece apenas em tempo de execução. E isso mesmo que as classes sejam realmente símbolos reais. É ainda pior para as categorias, pois elas nem são símbolos reais.
Portanto, se você tem uma biblioteca estática com centenas de objetos, mas a maioria dos seus binários precisa apenas de alguns deles, talvez prefira não usar as soluções (1) a (4) acima. Caso contrário, você terminará com binários muito grandes contendo todas essas classes, mesmo que a maioria nunca seja usada. Para as classes, geralmente você não precisa de nenhuma solução especial, uma vez que as classes têm símbolos reais e, desde que você as refira diretamente (não como no segundo exemplo de código), o vinculador identificará seu uso por si só. Para categorias, no entanto, considere a solução (5), pois torna possível incluir apenas as categorias que você realmente precisa.
Por exemplo, se você deseja uma categoria para o NSData, por exemplo, adicionando um método de compactação / descompactação, você cria um arquivo de cabeçalho:
// NSData+Compress.h
@interface NSData (Compression)
- (NSData *)compressedData;
- (NSData *)decompressedData;
@end
void import_NSData_Compression ( );
e um arquivo de implementação
// NSData+Compress
@implementation NSData (Compression)
- (NSData *)compressedData
{
// ... magic ...
}
- (NSData *)decompressedData
{
// ... magic ...
}
@end
void import_NSData_Compression ( ) { }
Agora apenas certifique-se de que qualquer lugar no seu código import_NSData_Compression()
seja chamado. Não importa onde é chamado ou com que frequência é chamado. Na verdade, ele realmente não precisa ser chamado, basta que o vinculador pense assim. Por exemplo, você pode colocar o seguinte código em qualquer lugar do seu projeto:
__attribute__((used)) static void importCategories ()
{
import_NSData_Compression();
// add more import calls here
}
Você nunca precisa chamar importCategories()
seu código, o atributo fará o compilador e o vinculador acreditarem que ele é chamado, mesmo que não seja.
E uma dica final:
se você adicionar -whyload
à chamada de link final, o vinculador imprimirá no log de compilação qual arquivo de objeto de qual biblioteca foi carregada devido a qual símbolo em uso. Ele imprimirá apenas o primeiro símbolo considerado em uso, mas esse não é necessariamente o único símbolo em uso desse arquivo de objeto.