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:
Una solución es agregar -all_load
a 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.
Otra solución es agregar -force_load
a 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á.
La solución más popular es agregar -ObjC
a 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).
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.
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 globalint
una 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_load
o Perform Single-Object Prelink
porque 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 mmc
es 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 -whyload
a 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.