Ответ от Владимира на самом деле довольно хороший, однако я хотел бы дать здесь несколько дополнительных знаний. Может быть, однажды кто-нибудь найдет мой ответ и может найти его полезным.
Компилятор преобразует исходные файлы (.c, .cc, .cpp, .m) в объектные файлы (.o). В исходном файле есть один объектный файл. Объектные файлы содержат символы, код и данные. Объектные файлы не могут использоваться непосредственно операционной системой.
Теперь при создании динамической библиотеки (.dylib), инфраструктуры, загружаемого пакета (.bundle) или исполняемого двоичного файла эти объектные файлы связаны между собой компоновщиком для создания того, что операционная система считает «пригодным для использования», например, что-то, что он может напрямую загрузить на определенный адрес памяти.
Однако при создании статической библиотеки все эти объектные файлы просто добавляются в большой архивный файл, отсюда и расширение статических библиотек (.a для архива). Таким образом, файл .a - это не что иное, как архив объектных (.o) файлов. Подумайте об архиве TAR или ZIP-архиве без сжатия. Просто проще скопировать один файл .a вокруг, чем целую кучу файлов .o (аналогично Java, где вы упаковываете файлы .class в архив .jar для легкого распространения).
При связывании двоичного файла со статической библиотекой (= архив) компоновщик получит таблицу всех символов в архиве и проверит, на какие из этих символов ссылаются двоичные файлы. Только объектные файлы, содержащие ссылочные символы, фактически загружаются компоновщиком и рассматриваются процессом компоновки. Например. если в вашем архиве 50 объектных файлов, но только 20 содержат символы, используемые двоичным файлом, компоновщик загружает только те 20, остальные 30 полностью игнорируются в процессе компоновки.
Это работает довольно хорошо для кода на C и C ++, поскольку эти языки пытаются сделать как можно больше во время компиляции (хотя C ++ также имеет некоторые функции только для времени выполнения). Obj-C, однако, это другой тип языка. Obj-C в значительной степени зависит от функций времени выполнения, и многие функции Obj-C фактически являются функциями только времени выполнения. Классы Obj-C на самом деле имеют символы, сравнимые с функциями C или глобальными переменными C (по крайней мере, в текущей среде выполнения Obj-C). Компоновщик может видеть, есть ли ссылка на класс или нет, поэтому он может определить, используется ли класс или нет. Если вы используете класс из объектного файла в статической библиотеке, этот объектный файл будет загружен компоновщиком, поскольку компоновщик видит используемый символ. Категории доступны только во время выполнения, категории не являются символами, такими как классы или функции, и это также означает, что компоновщик не может определить, используется категория или нет.
Если компоновщик загружает объектный файл, содержащий код Obj-C, все его части Obj-C всегда являются частью этапа компоновки. Поэтому, если объектный файл, содержащий категории, загружен, поскольку любой символ из него считается «используемым» (будь то класс, будь то функция, будь то глобальная переменная), категории также загружаются и будут доступны во время выполнения. , Тем не менее, если сам объектный файл не загружен, категории в нем не будут доступны во время выполнения. Объектный файл, содержащий только категорий, никогда загружен, потому что он содержит без символов компоновщик когда-либо считает "используемым". И в этом вся проблема.
Было предложено несколько решений, и теперь, когда вы знаете, как все это работает вместе, давайте еще раз посмотрим на предлагаемое решение:
Одним из решений является добавление -all_load
к вызову компоновщика. Что на самом деле будет делать этот флаг компоновщика? На самом деле он сообщает компоновщику следующее: « Загрузите все объектные файлы всех архивов, независимо от того, видите ли вы какой-либо используемый символ или нет ». Конечно, это будет работать; но он также может создавать довольно большие двоичные файлы.
Другим решением является добавление -force_load
к вызову компоновщика, включая путь к архиву. Этот флаг работает точно так же, как -all_load
, но только для указанного архива. Конечно, это будет работать.
Самое популярное решение - добавить -ObjC
к вызову компоновщика. Что на самом деле будет делать этот флаг компоновщика? Этот флаг сообщает компоновщику " Загружать все объектные файлы из всех архивов, если вы видите, что они содержат какой-либо код Obj-C ". И «любой код Obj-C» включает в себя категории. Это также будет работать и не заставит загружать объектные файлы, не содержащие кода Obj-C (они все еще загружаются только по требованию).
Другим решением является довольно новая настройка сборки Xcode Perform Single-Object Prelink
. Что будет делать этот параметр? Если этот параметр включен, все объектные файлы (помните, по одному на исходный файл) объединяются в один объектный файл (который не является реальной связью, отсюда и имя PreLink ) и этот единственный объектный файл (иногда также называется «главный объектный файл»), затем добавляется в архив. Если теперь какой-либо символ главного объектного файла рассматривается как используемый, весь главный объектный файл считается используемым, и, следовательно, все его части Objective C всегда загружаются. А поскольку классы являются обычными символами, достаточно использовать один класс из такой статической библиотеки, чтобы также получить все категории.
Окончательное решение - трюк, который Владимир добавил в самом конце своего ответа. Поместите « поддельный символ » в любой исходный файл, объявляющий только категории. Если вы хотите использовать какую-либо из категорий во время выполнения, убедитесь, что вы каким-то образом ссылаетесь на фальшивый символ во время компиляции, так как это вызывает загрузку объектного файла компоновщиком и, следовательно, также весь код Obj-C в этом. Например. это может быть функция с пустым телом функции (которая ничего не будет делать при вызове), или это может быть глобальная переменная, к которой обращаются (например, глобальная int
однажды прочитанная или однажды записанная, этого достаточно). В отличие от всех других решений, приведенных выше, это решение переносит управление тем, какие категории доступны во время выполнения для скомпилированного кода (если он хочет, чтобы они были связаны и доступны, он обращается к символу, в противном случае он не обращается к символу, и компоновщик будет игнорировать это).
Вот и все, ребята.
Ой, подождите, есть еще одна вещь:
У компоновщика есть опция с именем -dead_strip
. Что делает этот вариант? Если компоновщик решил загрузить объектный файл, все символы объектного файла становятся частью связанного двоичного файла, независимо от того, используются они или нет. Например. объектный файл содержит 100 функций, но только одна из них используется двоичным файлом, все 100 функций все еще добавляются в двоичный файл, поскольку объектные файлы либо добавляются целиком, либо не добавляются вообще. Частичное добавление объектного файла обычно не поддерживается компоновщиками.
Однако, если вы скажете компоновщику "мертвую полосу", компоновщик сначала добавит все объектные файлы в двоичный файл, разрешит все ссылки и, наконец, просканирует двоичный файл на наличие символов, которые не используются (или используются только другими символы не используются). Все символы, которые были найдены неиспользуемыми, затем удаляются как часть этапа оптимизации. В приведенном выше примере 99 неиспользуемых функций снова удаляются. Это очень полезно, если вы используете параметры, такие как -load_all
, -force_load
или Perform Single-Object Prelink
, потому что в некоторых случаях эти параметры могут значительно увеличить размер двоичного файла, а мертвое удаление снова удалит неиспользуемый код и данные.
Мертвая разметка работает очень хорошо для кода C (например, неиспользуемые функции, переменные и константы удаляются, как и ожидалось), а также очень хорошо работает для C ++ (например, удаляются неиспользуемые классы). Он не идеален, в некоторых случаях некоторые символы не удаляются, хотя было бы неплохо удалить их, но в большинстве случаев это работает довольно хорошо для этих языков.
А как насчет Obj-C? Забудь об этом! Для Obj-C нет мертвой зачистки. Поскольку Obj-C является языком функций времени исполнения, компилятор не может сказать во время компиляции, действительно ли используется символ или нет. Например. класс Obj-C не используется, если нет кода, прямо ссылающегося на него, правильно? Неправильно! Вы можете динамически создать строку, содержащую имя класса, запросить указатель класса для этого имени и динамически выделить класс. Например. вместо
MyCoolClass * mcc = [[MyCoolClass alloc] init];
я бы тоже написал
NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];
В обоих случаях mmc
является ссылкой на объект класса "MyCoolClass", но нет прямой ссылки на этот класс во втором примере кода (даже имя класса как статическая строка). Все происходит только во время выполнения. И это при том, что классы являются на самом деле реальными символами. Еще хуже для категорий, поскольку они даже не являются реальными символами.
Так что если у вас есть статическая библиотека с сотнями объектов, но большинству ваших двоичных файлов нужно только несколько из них, вы можете предпочесть не использовать решения (1) - (4) выше. В противном случае вы получите очень большие двоичные файлы, содержащие все эти классы, хотя большинство из них никогда не используются. Для классов вам обычно вообще не нужно никакого специального решения, поскольку у классов есть реальные символы, и если вы ссылаетесь на них напрямую (не так, как во втором примере кода), компоновщик сам определит их использование довольно хорошо. Однако для категорий рассмотрим решение (5), поскольку оно позволяет включать только те категории, которые вам действительно нужны.
например. если вы хотите категорию для NSData, например добавив к нему метод сжатия / распаковки, вы создадите файл заголовка:
// NSData+Compress.h
@interface NSData (Compression)
- (NSData *)compressedData;
- (NSData *)decompressedData;
@end
void import_NSData_Compression ( );
и файл реализации
// NSData+Compress
@implementation NSData (Compression)
- (NSData *)compressedData
{
// ... magic ...
}
- (NSData *)decompressedData
{
// ... magic ...
}
@end
void import_NSData_Compression ( ) { }
Теперь просто убедитесь, что где-то в вашем коде import_NSData_Compression()
вызывается. Неважно, где это называется или как часто это называется. На самом деле его вообще не нужно вызывать, достаточно, если так считает компоновщик. Например. Вы можете поместить следующий код в любом месте вашего проекта:
__attribute__((used)) static void importCategories ()
{
import_NSData_Compression();
// add more import calls here
}
Вы не должны когда-либо вызывать importCategories()
в своем коде, атрибут заставит компилятор и компоновщик поверить, что он вызывается, даже если это не так.
И последний совет:
Если вы добавите -whyload
к последнему вызову ссылки, компоновщик напечатает в журнале сборки, какой объектный файл из какой библиотеки он загружал из-за того, какой символ используется. Он будет печатать только первый рассматриваемый символ, но это не обязательно единственный символ, используемый в этом объектном файле.