Categorías de Objective-C en la biblioteca estática

153

¿Me puede guiar cómo vincular correctamente la biblioteca estática al proyecto de iPhone? Uso el proyecto de biblioteca estática agregado al proyecto de la aplicación como dependencia directa (destino -> general -> dependencias directas) y todo funciona bien, pero categorías. Una categoría definida en la biblioteca estática no funciona en la aplicación.

Entonces, mi pregunta es cómo agregar una biblioteca estática con algunas categorías en otro proyecto.

Y, en general, ¿cuál es la mejor práctica para usar en el código de proyecto de la aplicación de otros proyectos?

Vladimir
fuente
1
bueno, encontré algunas respuestas y parece que esta pregunta ya fue respondida aquí (perdón, lo perdí stackoverflow.com/questions/932856/… )
Vladimir

Respuestas:

228

Solución: a partir de Xcode 4.2, solo necesita ir a la aplicación que se vincula con la biblioteca (no la biblioteca en sí) y hacer clic en el proyecto en el Navegador de proyectos, hacer clic en el destino de su aplicación, luego crear configuraciones, luego buscar "Otro Banderas del vinculador ", haga clic en el botón + y agregue '-ObjC'. '-all_load' y '-force_load' ya no son necesarias.

Detalles: encontré algunas respuestas en varios foros, blogs y documentos de Apple. Ahora trato de hacer un breve resumen de mis búsquedas y experimentos.

El problema fue causado por (cita de Apple Technical Q&A QA1490 https://developer.apple.com/library/content/qa/qa1490/_index.html ):

Objective-C no define símbolos de enlazador para cada función (o método, en Objective-C); en cambio, los símbolos de enlazador solo se generan para cada clase. Si extiende una clase preexistente con categorías, el vinculador no sabe asociar el código objeto de la implementación de la clase principal y la implementación de la categoría. Esto evita que los objetos creados en la aplicación resultante respondan a un selector definido en la categoría.

Y su solución:

Para resolver este problema, la biblioteca estática debe pasar la opción -ObjC al vinculador. Este indicador hace que el vinculador cargue cada archivo de objeto en la biblioteca que define una clase o categoría de Objective-C. Si bien esta opción generalmente dará como resultado un ejecutable más grande (debido al código de objeto adicional cargado en la aplicación), permitirá la creación exitosa de bibliotecas estáticas efectivas de Objective-C que contienen categorías en las clases existentes.

y también hay recomendaciones en las Preguntas frecuentes sobre desarrollo de iPhone:

¿Cómo vinculo todas las clases de Objective-C en una biblioteca estática? Establezca la configuración de construcción de Otros indicadores de vinculador en -ObjC.

y descripciones de banderas:

- all_load Carga todos los miembros de las bibliotecas de archivos estáticos.

- ObjC Carga todos los miembros de las bibliotecas de archivos estáticos que implementan una clase o categoría de Objective-C.

- force_load (path_to_archive) Carga todos los miembros de la biblioteca de archivos estáticos especificada. Nota: -all_load obliga a todos los miembros de todos los archivos a cargarse. Esta opción le permite apuntar a un archivo específico.

* podemos usar force_load para reducir el tamaño binario de la aplicación y evitar conflictos que all_load puede causar en algunos casos.

Sí, funciona con archivos * .a agregados al proyecto. Sin embargo, tuve problemas con el proyecto lib agregado como dependencia directa. Pero más tarde descubrí que era mi culpa: el proyecto de dependencia directa posiblemente no se agregó correctamente. Cuando lo elimino y lo agrego nuevamente con pasos:

  1. Arrastre y suelte el archivo de proyecto lib en el proyecto de la aplicación (o agréguelo con Proyecto-> Agregar al proyecto ...).
  2. Haga clic en la flecha en el icono del proyecto lib - se muestra el nombre del archivo mylib.a, arrastre este archivo mylib.a y suéltelo en el grupo Destino -> Enlace binario con biblioteca.
  3. Abra la información de destino en la primera página (General) y agregue mi lib a la lista de dependencias

después de eso todo funciona bien. La bandera "-ObjC" fue suficiente en mi caso.

También me interesó la idea del blog http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html . El autor dice que puede usar la categoría de lib sin establecer -all_load o -ObjC flag. Simplemente agrega a la categoría archivos h / m una interfaz / implementación de clase ficticia vacía para forzar que el enlazador use este archivo. Y sí, este truco hace el trabajo.

Pero el autor también dijo que incluso no instanciaba un objeto ficticio. Mm ... Como he encontrado, deberíamos llamar explícitamente algún código "real" del archivo de categoría. Entonces, al menos, la función de clase debería llamarse. E incluso no necesitamos clase falsa. La única función c hace lo mismo.

Entonces, si escribimos archivos 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

y si llamamos useMyLib (); en cualquier parte del proyecto de la aplicación, en cualquier clase podemos usar el método de categoría logSelf;

[self logSelf];

Y más blogs sobre el 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
fuente
8
La nota técnica de Apple parece haberse modificado desde entonces para decir "Para resolver este problema, el enlace de destino contra la biblioteca estática debe pasar la opción -ObjC al enlazador". que es lo opuesto a lo que se cita arriba. Acabamos de confirmar que debe incluir al vincular la aplicación y no la biblioteca en sí.
Ken Aspeslagh
De acuerdo con el documento developer.apple.com/library/mac/#qa/qa1490/_index.html , debemos usar el indicador -all_load o -force_load. Como se mencionó, el vinculador tiene errores en la aplicación Mac de 64 bits y la aplicación iPhone. "Importante: para las aplicaciones de 64 bits y iPhone OS, hay un error de enlace que impide que -ObjC cargue archivos de objetos de bibliotecas estáticas que contienen solo categorías y no clases. La solución consiste en utilizar los indicadores -all_load o -force_load".
Robin
2
@ Ken Aspelagh: Gracias, tuve el mismo problema. Los distintivos -ObjC y -all_load deben agregarse a la aplicación en sí , no a la biblioteca.
titaniumdecoy
3
Gran respuesta, aunque los recién llegados a esta pregunta deben tener en cuenta que ahora está desactualizada. Consulte la respuesta de tonklon stackoverflow.com/a/9224606/322748 (all_load / force_load ya no es necesario)
Jay Peyer,
Me quedé atrapado en estas cosas durante casi media hora y con una prueba y error lo logré. Gracias de todos modos. ¡Esta respuesta vale un +1 y lo tienes!
Deepukjayan
118

La respuesta de Vladimir es bastante buena, sin embargo, me gustaría dar un poco más de conocimiento aquí. Tal vez algún día alguien encuentre mi respuesta y pueda encontrarla útil.

El compilador transforma los archivos fuente (.c, .cc, .cpp, .m) en archivos de objeto (.o). Hay un archivo de objeto por archivo fuente. Los archivos de objetos contienen símbolos, códigos y datos. Los archivos de objetos no son directamente utilizables por el sistema operativo.

Ahora, al construir una biblioteca dinámica (.dylib), un marco, un paquete cargable (.bundle) o un binario ejecutable, el enlazador enlaza estos archivos de objeto para producir algo que el sistema operativo considera "utilizable", por ejemplo, algo que puede cargar directamente a una dirección de memoria específica.

Sin embargo, cuando se construye una biblioteca estática, todos estos archivos de objeto simplemente se agregan a un archivo de archivo grande, de ahí la extensión de las bibliotecas estáticas (.a para archivo). Entonces, un archivo .a no es más que un archivo de objetos (.o). Piense en un archivo TAR o un archivo ZIP sin compresión. Es más fácil copiar un solo archivo .a que un montón de archivos .o (similar a Java, donde empaqueta los archivos .class en un archivo .jar para una fácil distribución).

Al vincular un binario a una biblioteca estática (= archivo), el vinculador obtendrá una tabla de todos los símbolos en el archivo y verificará a cuál de estos símbolos hacen referencia los binarios. Solo los archivos de objetos que contienen símbolos referenciados son realmente cargados por el enlazador y son considerados por el proceso de enlace. Por ejemplo, si su archivo tiene 50 archivos de objetos, pero solo 20 contienen símbolos utilizados por el binario, solo esos 20 son cargados por el enlazador, los otros 30 son completamente ignorados en el proceso de enlace.

Esto funciona bastante bien para el código C y C ++, ya que estos lenguajes intentan hacer todo lo posible en tiempo de compilación (aunque C ++ también tiene algunas características de tiempo de ejecución). Obj-C, sin embargo, es un tipo diferente de lenguaje. Obj-C depende en gran medida de las características de tiempo de ejecución y muchas características de Obj-C son en realidad características solo de tiempo de ejecución. Las clases Obj-C en realidad tienen símbolos comparables a las funciones C o variables C globales (al menos en el tiempo de ejecución Obj-C actual). Un vinculador puede ver si una clase está referenciada o no, por lo que puede determinar si una clase está en uso o no. Si utiliza una clase de un archivo de objeto en una biblioteca estática, el vinculador cargará este archivo de objeto porque el vinculador ve que se está utilizando un símbolo. Las categorías son una característica de tiempo de ejecución, las categorías no son símbolos como clases o funciones y eso también significa que un vinculador no puede determinar si una categoría está en uso o no.

Si el vinculador carga un archivo de objeto que contiene el código Obj-C, todas las partes Obj-C del mismo siempre forman parte de la etapa de vinculación. Por lo tanto, si se carga un archivo de objeto que contiene categorías porque cualquier símbolo del mismo se considera "en uso" (ya sea una clase, una función, una variable global), las categorías también se cargan y estarán disponibles en tiempo de ejecución . Sin embargo, si el archivo objeto en sí no está cargado, las categorías no estarán disponibles en tiempo de ejecución. Un archivo de objeto que contiene solo categorías nunca se carga porque no contiene símbolos que el vinculador alguna vez consideraría "en uso". Y este es todo el problema aquí.

Se han propuesto varias soluciones y ahora que sabe cómo funciona todo esto, echemos otro vistazo a la solución propuesta:

  1. Una solución es agregar -all_loada la llamada del enlazador. ¿Qué hará realmente esa bandera de enlace? En realidad, le dice al enlazador lo siguiente: " Cargue todos los archivos de objetos de todos los archivos independientemente de si ve algún símbolo en uso o no ". Por supuesto, eso funcionará; pero también puede producir binarios bastante grandes.

  2. Otra solución es agregar -force_loada la llamada del vinculador, incluida la ruta al archivo. Este indicador funciona exactamente igual -all_load, pero solo para el archivo especificado. Por supuesto, esto también funcionará.

  3. La solución más popular es agregar -ObjCa la llamada del enlazador. ¿Qué hará realmente esa bandera de enlace? Este indicador le dice al enlazador " Cargue todos los archivos de objetos de todos los archivos si ve que contienen algún código Obj-C ". Y "cualquier código Obj-C" incluye categorías. Esto también funcionará y no forzará la carga de archivos de objetos que no contengan código Obj-C (estos solo se cargan a pedido).

  4. Otra solución es la configuración de compilación Xcode bastante nueva Perform Single-Object Prelink. ¿Qué hará esta configuración? Si está habilitado, todos los archivos de objeto (recuerde, hay uno por archivo fuente) se fusionan en un solo archivo de objeto (que no es un enlace real, de ahí el nombre PreLink ) y este archivo de objeto único (a veces también llamado "objeto maestro" archivo ") se agrega al archivo. Si ahora se considera en uso cualquier símbolo del archivo de objeto maestro, se considera que está en uso todo el archivo de objeto maestro y, por lo tanto, todas las partes de Objective-C siempre se cargan. Y dado que las clases son símbolos normales, es suficiente usar una sola clase de una biblioteca estática para obtener todas las categorías.

  5. La solución final es el truco que Vladimir agregó al final de su respuesta. Coloque un " símbolo falso " en cualquier archivo fuente que declare solo categorías. Si desea utilizar cualquiera de las categorías en tiempo de ejecución, asegúrese de hacer referencia de alguna manera al símbolo falso en tiempo de compilación, ya que esto hace que el vinculador cargue el archivo de objeto y, por lo tanto, también todo el código Obj-C en él. Por ejemplo, podría ser una función con un cuerpo de función vacío (que no hará nada cuando se llame) o podría ser una variable global a la que se accede (por ejemplo, una función globalintuna vez leída o escrita, esto es suficiente). A diferencia de todas las otras soluciones anteriores, esta solución cambia el control sobre qué categorías están disponibles en tiempo de ejecución para el código compilado (si quiere que estén vinculadas y disponibles, accede al símbolo; de lo contrario, no accede al símbolo y el vinculador ignorará eso).

Eso es todo amigos.

Oh, espera, hay una cosa más:
el enlazador tiene una opción llamada -dead_strip. ¿Qué hace esta opción? Si el vinculador decidió cargar un archivo de objeto, todos los símbolos del archivo de objeto se convierten en parte del binario vinculado, se utilicen o no. Por ejemplo, un archivo de objeto contiene 100 funciones, pero el binario solo usa una de ellas, las 100 funciones todavía se agregan al binario porque los archivos de objeto se agregan como un todo o no se agregan en absoluto. Agregar un archivo de objeto parcialmente no suele ser compatible con los vinculadores.

Sin embargo, si le dice al enlazador que "haga una tira muerta", el enlazador primero agregará todos los archivos de objetos al binario, resolverá todas las referencias y finalmente escaneará el binario en busca de símbolos que no estén en uso (o solo en uso por otros símbolos que no estén en utilizar). Todos los símbolos que no se encuentran en uso se eliminan como parte de la etapa de optimización. En el ejemplo anterior, las 99 funciones no utilizadas se eliminan nuevamente. Esto es muy útil si usa opciones como -load_all, -force_loado Perform Single-Object Prelinkporque estas opciones pueden explotar fácilmente los tamaños binarios en algunos casos y la eliminación completa eliminará nuevamente el código y los datos no utilizados.

La eliminación completa funciona muy bien para el código C (por ejemplo, las funciones no utilizadas, las variables y las constantes se eliminan como se esperaba) y también funciona bastante bien para C ++ (por ejemplo, las clases no utilizadas se eliminan). No es perfecto, en algunos casos, algunos símbolos no se eliminan aunque estaría bien eliminarlos, pero en la mayoría de los casos funciona bastante bien para estos idiomas.

¿Qué hay de Obj-C? ¡Olvídalo! No hay desnudos para Obj-C. Como Obj-C es un lenguaje de características de tiempo de ejecución, el compilador no puede decir en tiempo de compilación si un símbolo está realmente en uso o no. Por ejemplo, una clase Obj-C no está en uso si no hay un código que haga referencia directa a ella, ¿correcto? ¡Incorrecto! Puede generar dinámicamente una cadena que contenga un nombre de clase, solicitar un puntero de clase para ese nombre y asignar dinámicamente la clase. Por ejemplo, en lugar de

MyCoolClass * mcc = [[MyCoolClass alloc] init];

Yo tambien podria escribir

NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];

En ambos casos mmces una referencia a un objeto de la clase "MyCoolClass", pero no hay una referencia directa a esta clase en el segundo ejemplo de código (ni siquiera el nombre de la clase como una cadena estática). Todo sucede solo en tiempo de ejecución. Y eso a pesar de que las clases son en realidad símbolos reales. Es aún peor para las categorías, ya que ni siquiera son símbolos reales.

Entonces, si tiene una biblioteca estática con cientos de objetos, pero la mayoría de sus archivos binarios solo necesitan unos pocos, puede preferir no usar las soluciones (1) a (4) anteriores. De lo contrario, terminará con binarios muy grandes que contienen todas estas clases, a pesar de que la mayoría de ellos nunca se utilizan. Para las clases, generalmente no necesita ninguna solución especial, ya que las clases tienen símbolos reales y siempre que los haga referencia directamente (no como en el segundo ejemplo de código), el vinculador identificará su uso bastante bien por sí mismo. Sin embargo, para las categorías, considere la solución (5), ya que permite incluir solo las categorías que realmente necesita.

Por ejemplo, si desea una categoría para NSData, por ejemplo, agregarle un método de compresión / descompresión, crearía un archivo de encabezado:

// NSData+Compress.h
@interface NSData (Compression)
    - (NSData *)compressedData;
    - (NSData *)decompressedData;
@end

void import_NSData_Compression ( );

y un archivo de implementación

// NSData+Compress
@implementation NSData (Compression)
    - (NSData *)compressedData 
    {
        // ... magic ...
    }

    - (NSData *)decompressedData
    {
        // ... magic ...
    }
@end

void import_NSData_Compression ( ) { }

Ahora solo asegúrese de que import_NSData_Compression()se llame a cualquier parte de su código . No importa dónde se llame o con qué frecuencia se llama. En realidad, no es necesario llamarlo en absoluto, es suficiente si el vinculador lo cree así. Por ejemplo, podría poner el siguiente código en cualquier parte de su proyecto:

__attribute__((used)) static void importCategories ()
{
    import_NSData_Compression();
    // add more import calls here
}

No tiene que llamar nunca importCategories()a su código, el atributo hará que el compilador y el vinculador crean que se llama, incluso en caso de que no lo sea.

Y un consejo final:
si agrega -whyloada la llamada de enlace final, el enlazador imprimirá en el registro de compilación qué archivo de objeto de qué biblioteca cargó debido a qué símbolo en uso. Solo imprimirá el primer símbolo considerado en uso, pero ese no es necesariamente el único símbolo en uso de ese archivo de objeto.

Mecki
fuente
1
¡Gracias por mencionar que -whyloadintentar depurar por qué el enlazador está haciendo algo puede ser bastante difícil!
Ben S
Hay una opción Dead Code Strippingen Build Settings>Linking. ¿Es lo mismo que -dead_stripagregado en Other Linker Flags?
Xiao
1
@Sean Sí, es lo mismo. Simplemente lea la "Ayuda rápida" que existe para cada configuración de compilación, la respuesta está ahí: postimg.org/image/n7megftnr/full
Mecki
@Mecki Gracias. Traté de deshacerme -ObjC, así que probé tu truco pero se queja "import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found. Puse import_NSString_jsonObjectmi Framework incrustado llamado Utilityy agrego #import <Utility/Utility.h>con una __attribute__declaración al final de mi AppDelegate.h.
Xiao
@Sean Si el vinculador no puede encontrar el símbolo, no está vinculando contra la biblioteca estática que contiene el símbolo. Solo importar un archivo ah desde un marco no hará que Xcode enlace contra el marco. El marco debe estar vinculado explícitamente en el enlace con la fase de construcción de marcos. Es posible que desee abrir una propia pregunta para su problema de vinculación, responder en los comentarios es engorroso y tampoco puede proporcionar información como la salida del registro de compilación.
Mecki
24

Este problema se ha solucionado en LLVM . El arreglo se envía como parte de LLVM 2.9 La primera versión de Xcode que contiene el arreglo es el envío de Xcode 4.2 con LLVM 3.0. El uso de -all_loado -force_loadya no es necesario cuando se trabaja con XCode 4.2 -ObjC .

tonklon
fuente
¿Estas seguro acerca de esto? Estoy trabajando en un proyecto de iOS usando Xcode 4.3.2, compilando con LLVM 3.1 y esto todavía era un problema para mí.
Ashley Mills
Ok, eso fue un poco impreciso. La -ObjCbandera todavía es necesaria y siempre lo será. La solución fue el uso de -all_loado -force_load. Y eso ya no es necesario. Arreglé mi respuesta arriba.
tonklon
¿Hay alguna desventaja al incluir el indicador -all_load (incluso si es innecesario)? ¿Afecta el tiempo de compilación / lanzamiento de alguna manera?
ZS
Estoy trabajando con Xcode Versión 4.5 (4G182) y el indicador -ObjC mueve mi error de selector no reconocido de la dependencia de terceros que estoy tratando de usar en lo que parece ser la profundidad del tiempo de ejecución de Objective C: "- [__ NSArrayM map :]: selector no reconocido enviado a la instancia ... ". ¿Alguna pista?
Robert Atkins
16

Esto es lo que debe hacer para resolver este problema por completo al compilar su biblioteca estática:

Vaya a Configuración de compilación de Xcode y establezca Realizar enlace de objeto único en SÍ o GENERATE_MASTER_OBJECT_FILE = YESen su archivo de configuración de compilación.

Por defecto, el enlazador genera un archivo .o para cada archivo .m. Entonces las categorías obtienen diferentes archivos .o. Cuando el vinculador mira los archivos .o de una biblioteca estática, no crea un índice de todos los símbolos por clase (el tiempo de ejecución lo hará, no importa qué).

Esta directiva le pedirá al vinculador que empaque todos los objetos juntos en un gran archivo .o y, de este modo, obliga al vinculador que procesa la biblioteca estática para obtener el índice de todas las categorías de clases.

Espero que eso lo aclare.

amosel
fuente
Esto me lo arregló sin tener que agregar -ObjC al objetivo de enlace.
Matthew Crenshaw
Después de actualizar a la última versión de la biblioteca BlocksKit , tuve que usar esta configuración para solucionar el problema (ya estaba usando el indicador -ObjC pero todavía veía el problema).
rakmoh
1
En realidad su respuesta no es del todo correcta. No le "pido al vinculador que empaque todas las categorías de la misma clase en un solo archivo .o", sino que le pide que vincule todos los archivos de objetos (.o) en un solo archivo de objeto grande antes de crear una biblioteca estática a partir de ellos / eso. Una vez que se hace referencia a cualquier símbolo de la biblioteca, se cargan todos los símbolos. Sin embargo, esto no funcionará si no se hace referencia a ningún símbolo (por ejemplo, si no funcionará si solo hay categorías en la biblioteca).
Mecki
No creo que esto funcione si agrega categorías a las clases existentes, como NSData.
Bob Whiteman
Yo también tengo problemas para agregar categorías a las clases existentes. Mi complemento no puede reconocerlos en tiempo de ejecución.
David Dunham
9

Un factor que rara vez se menciona cada vez que surge la discusión de vinculación de la biblioteca estática es el hecho de que también debe incluir las categorías en las fases de compilación-> copiar archivos y compilar las fuentes de la biblioteca estática .

Apple tampoco enfatiza este hecho en su recientemente publicado Uso de bibliotecas estáticas en iOS .

Pasé un día entero probando todo tipo de variaciones de -objC y -all_load, etc., pero no salió nada ... esta pregunta me llamó la atención. (no me malinterpretes ... todavía tienes que hacer las cosas -objC ... pero es más que eso).

También otra acción que siempre me ha ayudado es que siempre construyo la biblioteca estática incluida primero sola ... luego construyo la aplicación adjunta ...

abbood
fuente
-1

Probablemente necesite tener la categoría en el encabezado "público" de su biblioteca estática: #import "MyStaticLib.h"

christo16
fuente