Categorias de Objective-C na biblioteca estática

153

Você pode me orientar sobre como vincular corretamente a biblioteca estática ao projeto do iPhone. Eu uso o projeto de biblioteca estática adicionado ao projeto do aplicativo como dependência direta (destino -> geral -> dependências diretas) e tudo funciona bem, mas categorias. Uma categoria definida na biblioteca estática não está funcionando no aplicativo.

Então, minha pergunta é como adicionar uma biblioteca estática com algumas categorias em outro projeto?

E, em geral, qual é a melhor prática a ser usada no código de projeto de aplicativo de outros projetos?

Vladimir
fonte
1
bem, encontrei algumas respostas e parece que esta questão já foi respondida aqui (desculpe faltou stackoverflow.com/questions/932856/... )
Vladimir

Respostas:

228

Solução: A partir do Xcode 4.2, você só precisa acessar o aplicativo vinculado à biblioteca (não a biblioteca) e clicar no projeto no Navegador de Projeto, clicar no destino do aplicativo, criar configurações e procurar "Outros Sinalizadores de vinculador ", clique no botão + e adicione '-ObjC'. '-all_load' e '-force_load' não são mais necessários.

Detalhes: Encontrei algumas respostas em vários fóruns, blogs e documentos da apple. Agora, tento fazer um breve resumo de minhas pesquisas e experiências.

O problema foi causado por (citação de Perguntas e respostas técnicas da Apple QA1490 https://developer.apple.com/library/content/qa/qa1490/_index.html ):

O Objective-C não define símbolos do vinculador para cada função (ou método, no Objective-C). Em vez disso, os símbolos do vinculador são gerados apenas para cada classe. Se você estender uma classe pré-existente a categorias, o vinculador não saberá associar o código de objeto da implementação da classe principal e da implementação da categoria. Isso impede que os objetos criados no aplicativo resultante respondam a um seletor definido na categoria.

E a solução deles:

Para resolver esse problema, a biblioteca estática deve passar a opção -ObjC para o vinculador. Esse sinalizador faz com que o vinculador carregue todos os arquivos de objetos na biblioteca que definem uma classe ou categoria de Objective-C. Embora essa opção normalmente resulte em um executável maior (devido ao código de objeto adicional carregado no aplicativo), permitirá a criação bem-sucedida de bibliotecas estáticas Objective-C efetivas que contêm categorias nas classes existentes.

e também há recomendações nas Perguntas frequentes sobre desenvolvimento do iPhone:

Como vincular todas as classes de Objective-C em uma biblioteca estática? Defina a configuração de construção Outros sinalizadores de vinculador como -ObjC.

e descrições de bandeiras:

- all_load Carrega todos os membros das bibliotecas de archive estático.

- ObjC Carrega todos os membros de bibliotecas de archive estático que implementam uma classe ou categoria de Objective-C.

- force_load (path_to_archive) Carrega todos os membros da biblioteca de archive estático especificada. Nota: -all_load força todos os membros de todos os arquivos a serem carregados. Esta opção permite que você direcione um arquivo específico.

* podemos usar o force_load para reduzir o tamanho binário do aplicativo e evitar conflitos que o all_load pode causar em alguns casos.

Sim, funciona com arquivos * .a adicionados ao projeto. No entanto, tive problemas com o projeto lib adicionado como dependência direta. Mas depois descobri que era minha culpa - o projeto de dependência direta possivelmente não foi adicionado adequadamente. Quando removo e adiciono novamente com as etapas:

  1. Arraste e solte o arquivo do projeto lib no projeto do aplicativo (ou adicione-o em Projeto-> Adicionar ao projeto…).
  2. Clique na seta no ícone do projeto lib - nome do arquivo mylib.a mostrado, arraste esse arquivo mylib.a e solte-o no grupo Target -> Link Binary With Library.
  3. Abra as informações de destino na página principal (Geral) e adicione minha lib à lista de dependências

depois disso tudo funciona bem. A flag "-ObjC" foi suficiente no meu caso.

Eu também estava interessado em ideias do blog http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html . O autor diz que pode usar a categoria da lib sem definir o sinalizador -all_load ou -ObjC. Ele acabou de adicionar à categoria h / m arquivos interface / implementação de classe fictícia vazia para forçar o vinculador a usar esse arquivo. E sim, esse truque faz o trabalho.

Mas o autor também disse que nem instanciava objetos fictícios. Mm ... Como eu descobri, devemos chamar explicitamente algum código "real" do arquivo de categoria. Portanto, pelo menos a função de classe deve ser chamada. E nós nem precisamos de aula fictícia. A função c simples faz o mesmo.

Portanto, se escrevermos arquivos lib como:

// mylib.h
void useMyLib();

@interface NSObject (Logger)
-(void)logSelf;
@end


// mylib.m
void useMyLib(){
    NSLog(@"do nothing, just for make mylib linked");
}


@implementation NSObject (Logger)
-(void)logSelf{
    NSLog(@"self is:%@", [self description]);
}
@end

e se chamamos useMyLib (); em qualquer lugar no projeto App, em qualquer classe, podemos usar o método da categoria logSelf;

[self logSelf];

E mais blogs sobre o tema:

http://t-machine.org/index.php/2009/10/13/how-to-make-an-iphone-static-library-part-1/

http://blog.costan.us/2009/12/fat-iphone-static-libraries-device-and.html

Vladimir
fonte
8
A nota técnica da Apple parece ter sido modificada para dizer "Para resolver esse problema, o destino vinculado à biblioteca estática deve passar a opção -ObjC para o vinculador". que é o oposto do que é citado acima. Acabamos de confirmar que você deve incluir ao vincular o aplicativo e não a própria biblioteca.
Ken Aspeslagh
De acordo com o doc developer.apple.com/library/mac/#qa/qa1490/_index.html , devemos usar o sinalizador -all_load ou -force_load. Como mencionado, o vinculador possui um bug no Mac App de 64 bits e no iPhone. "Importante: Para aplicativos de SO de 64 bits e iPhone, existe um bug no vinculador que impede o -ObjC de carregar arquivos de objetos de bibliotecas estáticas que contêm apenas categorias e sem classes. A solução alternativa é usar os sinalizadores -all_load ou -force_load."
Robin
2
@ Ken Aspelagh: Obrigado, tive o mesmo problema. Os sinalizadores -ObjC e -all_load precisam ser adicionados ao próprio aplicativo , não à biblioteca.
titaniumdecoy
3
Ótima resposta, embora os recém-chegados a essa pergunta devam notar que ela está desatualizada. Confira a resposta de tonklon stackoverflow.com/a/9224606/322748 (all_load / force_load não são mais necessários)
Jay Peyer
Fiquei preso nessas coisas por quase meia hora e, com uma tentativa e erro, acabei de descobrir. De qualquer forma, obrigado. Esta resposta vale +1 e você conseguiu !!!
Deepukjayan
118

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:

  1. 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.

  2. 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á.

  3. 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).

  4. 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.

  5. 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 globalintuma 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_loadou Perform Single-Object Prelinkporque 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, mmchá 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.

Mecki
fonte
1
Obrigado por mencionar -whyload, tentar depurar por que o vinculador está fazendo algo pode ser bastante difícil!
Ben S
Existe uma opção Dead Code Strippingno Build Settings>Linking. É o mesmo que -dead_stripadicionado Other Linker Flags?
Xiao
1
@ Sean Sim, é o mesmo. Basta ler a "Ajuda Rápida" que existe para todos os ambientes de construção, a resposta está ali: postimg.org/image/n7megftnr/full
Mecki
@Mecki Obrigado. Eu tentei me livrar dele -ObjC, então tentei o seu hack, mas ele reclama "import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found. Eu coloquei import_NSString_jsonObjectno meu Framework incorporado chamado Utilitye adicionei #import <Utility/Utility.h>uma __attribute__declaração no final do meu AppDelegate.h.
Xiao
@Sean Se o vinculador não conseguir encontrar o símbolo, você não estará vinculando a biblioteca estática que contém o símbolo. Apenas importar um arquivo ah de uma estrutura não fará o link do Xcode contra a estrutura. A estrutura deve ser explicitamente vinculada à fase de criação da estrutura. Convém abrir uma pergunta própria para o seu problema de vinculação, responder aos comentários é complicado e também não é possível fornecer informações como saída do log de compilação.
Mecki
24

Este problema foi corrigido no LLVM . A correção é enviada como parte do LLVM 2.9 A primeira versão do Xcode a conter a correção é o Xcode 4.2 enviado com o LLVM 3.0. O uso -all_loadou -force_loadnão é mais necessário ao trabalhar com o XCode 4.2 -ObjC ainda é necessário.

tonklon
fonte
Você tem certeza disso? Estou trabalhando em um projeto iOS usando o Xcode 4.3.2, compilando com o LLVM 3.1 e isso ainda era um problema para mim.
21812 Ashley Mills # 1
Ok, isso foi um pouco impreciso. A -ObjCbandeira ainda é necessária e sempre será. A solução alternativa foi o uso de -all_loadou -force_load. E isso não é mais necessário. Corrigi minha resposta acima.
tonklon
Existe alguma desvantagem em incluir o sinalizador -all_load (mesmo que desnecessário)? Isso afeta o tempo de compilação / inicialização de alguma forma?
ZS
Estou trabalhando com o Xcode versão 4.5 (4G182) e o sinalizador -ObjC move meu erro de seletor não reconhecido da dependência de terceiros. Estou tentando usar o que parece ser a profundidade do tempo de execução do Objective C: "- [__ NSArrayM map :]: seletor não reconhecido enviado para a instância ... ". Alguma pista?
Robert Atkins
16

Aqui está o que você precisa fazer para resolver esse problema completamente ao compilar sua biblioteca estática:

Vá para Configurações de compilação do Xcode e defina Executar pré-vinculação de objeto único como YES ou GENERATE_MASTER_OBJECT_FILE = YESno arquivo de configuração da compilação.

Por padrão, o vinculador gera um arquivo .o para cada arquivo .m. Portanto, as categorias obtêm arquivos .o diferentes. Quando o vinculador examina os arquivos .o da biblioteca estática, ele não cria um índice de todos os símbolos por classe (o Runtime fará, não importa o quê).

Essa diretiva solicitará que o vinculador empacote todos os objetos em um grande arquivo .o e, por isso, força o vinculador que processa a biblioteca estática a obter o índice de todas as categorias de classe.

Espero que isso esclareça.

amosel
fonte
Isso foi corrigido para mim sem a necessidade de adicionar -ObjC ao destino do link.
Matthew Crenshaw
Após atualizar para a versão mais recente da biblioteca BlocksKit , tive que usar essa configuração para corrigir o problema (eu já estava usando o sinalizador -ObjC, mas ainda estava vendo o problema).
Rakmoh #
1
Na verdade, sua resposta não está certa. Não "solicito ao vinculador que agrupe todas as categorias da mesma classe em um arquivo .o", solicita ao vinculador que vincule todos os arquivos de objeto (.o) em um único arquivo grande de objeto antes de criar uma biblioteca estática a partir de eles / isto. Depois que qualquer símbolo é referenciado na biblioteca, todos os símbolos são carregados. No entanto, isso não funcionará se nenhum símbolo for mencionado (por exemplo, se não funcionar, se houver apenas categorias na biblioteca).
Mecki
Eu não acho que isso funcionará se você adicionar categorias a classes existentes, como NSData.
Bob Whiteman
Eu também estou tendo problemas para adicionar categorias às classes existentes. Meu plug-in não pode reconhecê-los em tempo de execução.
David Dunham
9

Um fator que raramente é mencionado sempre que a discussão sobre a vinculação da biblioteca estática é o fato de que você também deve incluir as categorias nas fases de construção-> copiar arquivos e compilar fontes da própria biblioteca estática .

A Apple também não enfatiza esse fato em suas recentemente usadas Using Static Libraries in iOS .

Passei um dia inteiro tentando todos os tipos de variações de -objC e -all_load etc. Mas nada saiu disso. Essa questão chamou minha atenção para esse problema. (não me interpretem mal ... você ainda precisa fazer as coisas -objC .. mas é mais do que isso).

Outra ação que sempre me ajudou é que eu sempre construo a biblioteca estática incluída primeiro por conta própria .. então eu construo o aplicativo anexo ..

abbood
fonte
-1

Você provavelmente precisa ter a categoria no cabeçalho "público" da biblioteca estática: #import "MyStaticLib.h"

christo16
fonte