Категории Objective-C в статической библиотеке

153

Можете ли вы рассказать мне, как правильно связать статическую библиотеку с проектом iPhone. Я использую статический проект библиотеки, добавленный в проект приложения в качестве прямой зависимости (цель -> общие -> прямые зависимости), и все работает нормально, но категории. Категория, определенная в статической библиотеке, не работает в приложении.

Итак, мой вопрос, как добавить статическую библиотеку с некоторыми категориями в другой проект?

И вообще, что лучше всего использовать в коде проекта приложения из других проектов?

Владимир
источник
1
ну, нашел несколько ответов и, кажется, на этот вопрос уже был дан ответ (извините, пропустил это stackoverflow.com/questions/932856/… )
Владимир,

Ответы:

228

Решение: Начиная с Xcode 4.2, вам нужно всего лишь перейти к приложению, которое связывается с библиотекой (не с самой библиотекой), и щелкнуть проект в Навигаторе проекта, щелкнуть цель вашего приложения, затем настроить параметры, а затем выполнить поиск «Другое». Флаги компоновщика ", нажмите кнопку + и добавьте '-ObjC'. «-all_load» и «-force_load» больше не нужны.

Подробности: я нашел несколько ответов на различных форумах, в блогах и документах Apple. Теперь я попытаюсь сделать краткое изложение моих поисков и экспериментов.

Проблема была вызвана (цитата из технических вопросов и ответов Apple QA1490 https://developer.apple.com/library/content/qa/qa1490/_index.html ):

Objective-C не определяет символы компоновщика для каждой функции (или метода в Objective-C) - вместо этого символы компоновщика генерируются только для каждого класса. Если вы расширяете ранее существующий класс категориями, компоновщик не знает, как связать объектный код реализации базового класса и реализации категории. Это препятствует тому, чтобы объекты, созданные в получающемся приложении, отвечали на селектор, который определен в категории.

И их решение:

Чтобы решить эту проблему, статическая библиотека должна передать компоновщику опцию -ObjC. Этот флаг заставляет компоновщик загружать каждый объектный файл в библиотеке, который определяет класс или категорию Objective-C. Хотя этот параметр обычно приводит к увеличению размера исполняемого файла (из-за загрузки в приложение дополнительного объектного кода), он позволяет успешно создавать эффективные статические библиотеки Objective-C, которые содержат категории в существующих классах.

и есть также рекомендации в FAQ по разработке iPhone:

Как связать все классы Objective-C в статической библиотеке? Установите для параметра сборки других флагов компоновщика значение -ObjC.

и описания флагов:

- all_load Загружает всех членов статических архивных библиотек.

- ObjC Загружает все члены статических архивных библиотек, которые реализуют класс или категорию Objective-C.

- force_load (path_to_archive) Загружает всех членов указанной статической архивной библиотеки. Примечание: -all_load заставляет всех участников всех архивов быть загруженными. Эта опция позволяет вам ориентироваться на определенный архив.

* мы можем использовать force_load, чтобы уменьшить двоичный размер приложения и избежать конфликтов, которые all_load может вызвать в некоторых случаях.

Да, он работает с * .a файлами, добавленными в проект. Но у меня были проблемы с проектом lib, добавленным как прямая зависимость. Но позже я обнаружил, что это была моя вина - проект прямой зависимости, возможно, не был добавлен должным образом. Когда я удаляю это и добавляю снова с шагами:

  1. Перетащите файл проекта lib в проект приложения (или добавьте его с помощью Project-> Add to project…).
  2. Нажмите стрелку на значке проекта lib - показано имя файла mylib.a, перетащите этот файл mylib.a и поместите его в группу Target -> Link Binary With Library.
  3. Откройте информацию о цели на первой странице (Общие) и добавьте мою библиотеку в список зависимостей

после этого все работает ок. Флаг "-ObjC" был достаточно в моем случае.

Меня также заинтересовала идея из блога http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html . Автор говорит, что он может использовать категорию из lib, не устанавливая флаг -all_load или -ObjC. Он просто добавляет в категорию h / m файлы пустой фиктивный интерфейс класса / реализации, чтобы заставить линкер использовать этот файл. И да, этот трюк делает свою работу.

Но автор также сказал, что он даже не создал объект-пустышку. Мм… Как я обнаружил, мы должны явно вызывать некоторый «настоящий» код из файла категории. Так что, по крайней мере, должна быть вызвана функция класса. И нам даже не нужен фиктивный класс. Одиночная функция c делает то же самое.

Так что, если мы напишем lib файлы как:

// 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

и если мы вызываем useMyLib (); в любом месте проекта App, затем в любом классе мы можем использовать метод категории logSelf;

[self logSelf];

И еще блоги по теме:

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

Владимир
источник
8
Техническая заметка Apple, как представляется, с тех пор была изменена, чтобы сказать: «Чтобы решить эту проблему, целевое связывание со статической библиотекой должно передать опцию -ObjC компоновщику». что противоположно тому, что цитируется выше. Мы только что подтвердили, что вы должны указывать ссылку на приложение, а не на саму библиотеку.
Кен Аспеллах
Согласно документу developer.apple.com/library/mac/#qa/qa1490/_index.html , мы должны использовать флаг -all_load или -force_load. Как уже упоминалось, компоновщик имеет ошибку в 64-битном Mac App и iPhone App. «Важно: для 64-битных приложений и приложений для iPhone OS существует ошибка компоновщика, которая не позволяет -ObjC загружать файлы объектов из статических библиотек, которые содержат только категории и не содержат классов. Обходное решение - использовать флаги -all_load или -force_load».
Робин
2
@Ken Aspelagh: Спасибо, у меня была та же проблема. Флаги -ObjC и -all_load необходимо добавить в само приложение , а не в библиотеку.
titaniumdecoy
3
Отличный ответ, хотя новички на этот вопрос должны заметить, что он уже устарел. Ознакомьтесь с ответом tonklon stackoverflow.com/a/9224606/322748 (all_load / force_load больше не нужны)
Джей Пейер,
Я застрял на этих вещах почти полчаса, и методом проб и ошибок я только что сделал это. В любом случае, спасибо. Этот ответ стоит +1, и вы получили это !!!
Дипукджаян
118

Ответ Владимира на самом деле довольно хороший, однако я хотел бы дать здесь несколько дополнительных знаний. Возможно, однажды кто-нибудь найдет мой ответ и может найти его полезным.

Компилятор преобразует исходные файлы (.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 всегда являются частью этапа компоновки. Поэтому, если объектный файл, содержащий категории, загружен, поскольку любой символ из него считается «используемым» (будь то класс, будь то функция, будь то глобальная переменная), категории также загружаются и будут доступны во время выполнения. , Тем не менее, если сам объектный файл не загружен, категории в нем не будут доступны во время выполнения. Объектный файл, содержащий только категории, никогда не загружается, поскольку он не содержит символов, которые компоновщик мог бы считать «используемыми». И это вся проблема здесь.

Было предложено несколько решений, и теперь, когда вы знаете, как все это работает вместе, давайте еще раз посмотрим на предлагаемое решение:

  1. Одним из решений является добавление -all_loadк вызову компоновщика. Что на самом деле будет делать этот флаг компоновщика? На самом деле он сообщает компоновщику следующее: « Загрузите все объектные файлы всех архивов, независимо от того, видите ли вы какой-либо используемый символ или нет ». Конечно, это будет работать, но он также может создавать довольно большие двоичные файлы.

  2. Другое решение - добавить -force_loadк вызову компоновщика, включая путь к архиву. Этот флаг работает точно так же -all_load, но только для указанного архива. Конечно, это будет работать.

  3. Самое популярное решение - добавить -ObjCк линкеру вызов. Что на самом деле будет делать этот флаг компоновщика? Этот флаг сообщает компоновщику « Загружать все объектные файлы из всех архивов, если вы видите, что они содержат какой-либо код Obj-C ». И «любой код Obj-C» включает в себя категории. Это также будет работать и не заставит загружать объектные файлы, не содержащие кода Obj-C (они все еще загружаются только по требованию).

  4. Другое решение - довольно новая настройка сборки XCode Perform Single-Object Prelink. Что будет делать этот параметр? Если этот параметр включен, все объектные файлы (помните, по одному на исходный файл) объединяются в один объектный файл (который не является реальной связью , отсюда и название PreLink ) и этот единственный объектный файл (иногда также называемый «мастер-объектом»). файл ") затем добавляется в архив. Если теперь какой-либо символ главного объектного файла рассматривается как используемый, весь главный объектный файл считается используемым, и, следовательно, все его части Objective C всегда загружаются. А поскольку классы являются обычными символами, достаточно использовать один класс из такой статической библиотеки, чтобы также получить все категории.

  5. Окончательное решение - трюк, который Владимир добавил в самом конце своего ответа. Поместите « поддельный символ » в любой исходный файл, объявляющий только категории. Если вы хотите использовать какую-либо из категорий во время выполнения, убедитесь, что вы каким-то образом ссылаетесь на поддельный символ во время компиляции, так как это вызывает загрузку объектного файла компоновщиком и, следовательно, также весь код 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к последнему вызову ссылки, компоновщик напечатает в журнале сборки, какой объектный файл из какой библиотеки он загружал из-за того, какой символ используется. Он напечатает только первый рассматриваемый символ, но это не обязательно единственный символ, используемый в этом объектном файле.

Mecki
источник
1
Спасибо за упоминание -whyload, попытка отладки, почему компоновщик что-то делает, может быть довольно сложной!
Бен С
Есть вариант Dead Code Strippingв Build Settings>Linking. Это так же, как -dead_stripдобавлено в Other Linker Flags?
Сяо
1
@Sean Да, это то же самое. Просто прочитайте «Быстрая справка», которая существует для каждого параметра сборки, ответ тут же: postimg.org/image/n7megftnr/full
Mecki
@ Mecki Спасибо. Я пытался избавиться от -ObjC, поэтому я попробовал твой взлом, но он жалуется "import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found. Я вставляю import_NSString_jsonObjectв свою встроенную платформу имя Utilityи добавляю #import <Utility/Utility.h>с __attribute__оператором в конце моего AppDelegate.h.
Сяо
@Sean Если компоновщик не может найти символ, вы не связываетесь со статической библиотекой, которая содержит символ. Простой импорт файла ах из фреймворка не сделает ссылку Xcode на фреймворк. Фреймворк должен быть явно связан с этапом сборки фреймворка. Возможно, вы захотите открыть собственный вопрос для вашей проблемы со ссылками, ответы в комментариях громоздки, и вы также не можете предоставить такую ​​информацию, как вывод журнала сборки.
Меки
24

Эта проблема была исправлена ​​в LLVM . Исправление поставляется как часть LLVM 2.9. Первая версия Xcode, в которой содержится исправление, - это поставка Xcode 4.2 с LLVM 3.0. Использование -all_loadили -force_loadбольше не требуется при работе с XCode 4.2 -ObjC все еще необходимо.

tonklon
источник
вы уверены в этом? Я работаю над проектом iOS с использованием Xcode 4.3.2, компилирую с LLVM 3.1, и это все еще было проблемой для меня.
Эшли Миллс
Хорошо, это было немного неточно. -ObjCФлаг по - прежнему необходимо , и всегда будет. Обходным путем было использование -all_loadили -force_load. И это больше не нужно. Я исправил свой ответ выше.
Тонклон
Есть ли какой-либо недостаток в том, чтобы включить флаг -all_load (даже если он не нужен)? Это как-то влияет на время компиляции / запуска?
ZS
Я работаю с версией 4.5 Xcode (4G182), и флаг -ObjC перемещает мою нераспознанную ошибку селектора из сторонней зависимости, которую я пытаюсь использовать в то, что похоже на глубины среды выполнения Objective C: "- [__ NSArrayM map :]: нераспознанный селектор отправлен на экземпляр ... ". Есть какие-нибудь подсказки?
Роберт Аткинс
16

Вот что вам нужно сделать, чтобы полностью решить эту проблему при компиляции статической библиотеки:

Либо перейдите в Настройки сборки XCode и установите Perform Single Object Prelink в YES, либо GENERATE_MASTER_OBJECT_FILE = YESв файле конфигурации сборки.

По умолчанию компоновщик создает файл .o для каждого файла .m. Так что категории получают разные .o файлы. Когда компоновщик просматривает статическую библиотеку .o файлов, он не создает индекс всех символов для класса (время выполнения будет, неважно, что).

Эта директива попросит компоновщик упаковать все объекты вместе в один большой файл .o и тем самым вынудит компоновщика, который обрабатывает статическую библиотеку, получить индекс всех категорий классов.

Надеюсь, что это проясняет.

amosel
источник
Это исправило это для меня без необходимости добавлять -ObjC к цели связывания.
Мэтью Креншоу
После обновления до последней версии библиотеки BlocksKit мне пришлось использовать этот параметр, чтобы исправить проблему (я уже использовал флаг -ObjC, но все еще вижу проблему).
rakmoh
1
На самом деле ваш ответ не совсем правильный. Я не «прошу компоновщика упаковать все категории одного класса вместе в один файл .o», он просит компоновщика связать все объектные файлы (.o) в один большой объектный файл перед созданием статической библиотеки из их / его. После ссылки на любой символ из библиотеки все символы загружаются. Тем не менее, это не будет работать, если нет ссылок на символы (например, если это не будет работать, если в библиотеке есть только категории).
Меки
Я не думаю, что это будет работать, если вы добавите категории в существующие классы, такие как NSData.
Боб Вайтман
У меня тоже возникают проблемы с добавлением категорий в существующие классы. Мой плагин не может распознать их во время выполнения.
Дэвид Данхэм
9

Один из факторов, который редко упоминается при обсуждении связывания статической библиотеки, - это тот факт, что вы также должны включать сами категории в фазы сборки -> копировать файлы и компилировать источники самой статической библиотеки .

Apple также не подчеркивает этот факт в своих недавно опубликованных статьях Использование статических библиотек в iOS .

Я провел целый день, пробуя всевозможные варианты -objC, -all_load и т. Д., Но ничего из этого не вышло ... этот вопрос привлек эту проблему к моему вниманию. (не поймите меня неправильно ... вы все равно должны делать вещи -objC ... но это больше, чем просто).

еще одно действие, которое всегда помогало мне, - это то, что я всегда сначала собираю включенную статическую библиотеку самостоятельно, а затем создаю приложение.

abbood
источник
-1

Возможно, вам нужно иметь категорию в заголовке вашей публичной статической библиотеки: #import "MyStaticLib.h"

christo16
источник