Цілі-C категорії в статичній бібліотеці


153

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

Отже, моє запитання - як додати статичну бібліотеку з деякими категоріями до іншого проекту?

І взагалі, яку найкращу практику використовувати в коді додатків для інших проектів?


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

Відповіді:


228

Рішення: Станом на Xcode 4.2, вам потрібно лише перейти до програми, яка пов'язує бібліотеку (а не саму бібліотеку) та натиснути проект у Навігаторі проектів, натиснути ціль програми, а потім створити налаштування, а потім знайти "Інше Прапор Linker ", натисніть кнопку + і додайте" -ObjC ". '-all_load' і '-force_load' більше не потрібні.

Детальніше: я знайшов відповіді на різних форумах, блогах та яблучних документах. Зараз я намагаюся зробити короткий підсумок своїх пошуків та експериментів.

Проблема була викликана (цитування з Apple Technical Q&A QA1490 https://developer.apple.com/library/content/qa/qa1490/_index.html ):

Objective-C не визначає символи зв’язків для кожної функції (або методу в Objective-C) - натомість символи лінкерів генеруються лише для кожного класу. Якщо ви розширите попередньо існуючий клас категоріями, лінкер не знає асоціювати об'єктний код реалізації базового класу та реалізації категорії. Це не дозволяє об’єктам, створеним у отриманій програмі, відповідати на селектор, визначений у категорії.

І їх рішення:

Щоб вирішити цю проблему, статична бібліотека повинна передати опцію -ObjC до лінкера. Цей прапор змушує лінкер завантажувати кожен об’єктний файл у бібліотеці, який визначає клас або категорію Objective-C. Хоча ця опція, як правило, призводить до більшого виконуваного файлу (за рахунок додаткового об'єктного коду, завантаженого в додаток), це дозволить успішно створити ефективні статичні бібліотеки Objective-C, що містять категорії в існуючих класах.

а також є відповіді в FAQ щодо розробки iPhone:

Як з'єднати всі класи Objective-C у статичній бібліотеці? Встановіть для параметра збірки Інші прапорці Linker значення -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 та перепустіть його у ціль -> Посилання Бінарне із групою бібліотеки.
  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 на лінкер." що протилежне тому, що цитується вище. Ми щойно підтвердили, що вам потрібно включати додаток, а не саму бібліотеку.
Ken Aspeslagh

Відповідно до doc developer.apple.com/library/mac/#qa/qa1490/_index.html , ми повинні використовувати прапор -all_load або -force_load. Як згадувалося, у лінкера є помилка в 64-бітній програмі Mac і iPhone. "Важливо. Для 64-розрядних програм та додатків для ОС iPhone існує помилка посилання, яка не дозволяє -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 ++ (наприклад, невикористані класи видаляються). Це не ідеально, в деяких випадках деякі символи не видаляються, хоча було б нормально їх видаляти, але в більшості випадків це досить добре працює для цих мов.

А що з 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до остаточного виклику посилання, то лінкер виведе в журнал збирання файл об’єкта, з якої бібліотеки він завантажився через який символ, який використовується. Він буде друкувати лише перший символ, що розглядається у використанні, але це не обов'язково єдиний символ, який використовується у цьому об'єктному файлі.


1
Дякую за те -whyload, що ви згадали , намагаючись налагодити, чому лінкер щось робить, може бути досить складно!
Ben S

Є варіант Dead Code Strippingв Build Settings>Linking. Це те саме, що -dead_stripдодано в Other Linker Flags?
Сяо

1
@Sean Так, це те саме. Просто прочитайте "Швидку допомогу", яка існує для кожного налаштування збірки, відповідь є саме тут: postimg.org/image/n7megftnr/full
Меккі

@ Mecki Дякую Я намагався позбутися -ObjC, тому я спробував ваш хак, але він скаржиться "import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found. Я вкладаю import_NSString_jsonObjectсвій вбудований Framework з назвою Utilityта додаю #import <Utility/Utility.h>із __attribute__заявою в кінці мого AppDelegate.h.
Сяо

@Sean Якщо лінкер не може знайти символ, ви не зв'язуєтесь зі статичною бібліотекою, що містить символ. Просто імпорт ах-файлу з фреймворку не зробить посилання Xcode на рамки. Рамка повинна бути явно пов'язана у зв'язку із фазою побудови фреймворків. Можливо, ви захочете відкрити власне запитання щодо вашої проблеми з посиланням, відповіді в коментарях громіздкі, і ви також не можете надати таку інформацію, як вихідний журнал збірки.
Mecki

24

Ця проблема була виправлена ​​в LLVM . Виправлення постачається як частина LLVM 2.9 Перша версія Xcode, яка містить виправлення, - це доставка Xcode 4.2 з LLVM 3.0. Використання -all_loadабо -force_loadбільше не потрібно при роботі з XCode 4.2 -ObjC як і раніше.


Ви впевнені в цьому? Я працюю над проектом iOS, використовуючи Xcode 4.3.2, компілюючи LLVM 3.1, і це все ще було проблемою для мене.
Ешлі Міллс

Гаразд, це було трохи неточно. -ObjCПрапор по - , як і раніше необхідно , і завжди буде. Обхідним шляхом було використання -all_loadабо -force_load. І це вже не потрібно. Свою відповідь я виправив вище.
тонклон

Чи є недолік включити прапор -all_load (навіть якщо це непотрібно)? Чи впливає це на час компіляції / запуску?
ZS

Я працюю з Xcode версією 4.5 (4G182), і прапор -ObjC переміщує мою невпізнану помилку селектора із залежності третьої сторони, яку я намагаюсь використати у тому, що виглядає як глибина виконання об'єктивного C: "- [__ карта NSArrayM :]: нерозпізнаний селектор відправлений до екземпляра ... ". Будь-які підказки?
Роберт Аткінс

16

Ось що потрібно зробити, щоб повністю вирішити цю проблему під час складання статичної бібліотеки:

Перейдіть GENERATE_MASTER_OBJECT_FILE = YESу параметри збірки Xcode та встановіть Виконати попереднє посилання з одного об’єкта на YES або у файл конфігурації збірки.

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

Ця директива попросить лінкер спакувати всі об'єкти разом в один великий.

Сподіваюся, що це прояснить.


Це зафіксувало це для мене, не потребуючи додавання -ObjC до цілі зв'язку.
Меттью Креншо

Після оновлення до останньої версії бібліотеки BlocksKit мені довелося скористатися цим налаштуванням, щоб виправити проблему (я вже використовував прапор -ObjC, але проблему все ще бачив).
rakmoh

1
Насправді ваша відповідь не зовсім правильна. Я не "просимо лінкер упакувати всі категорії одного класу разом в один .o файл", він попросить у лінкера зв'язати всі об'єктні файли (.o) в єдиний великий об'єктний файл, перш ніж створити статичну бібліотеку з їх / це. Після посилання будь-якого символу з бібліотеки всі символи завантажуються. Однак це не спрацює, якщо жоден символ не посилається (наприклад, якщо він не працюватиме, якщо в бібліотеці є лише категорії).
Меккі

Я не думаю, що це спрацює, якщо ви додасте категорії до існуючих класів, таких як NSData.
Боб Вітман

У мене теж виникають проблеми з додаванням категорій до існуючих класів. Мій плагін не може розпізнати їх під час виконання.
Девід Данхем

9

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

Apple також не наголошує на цьому факті в нещодавно опублікованій програмі Використання статичних бібліотек в iOS .

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

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


-1

Вам, мабуть, потрібна категорія у заголовку "загальнодоступного" статичної бібліотеки: #import "MyStaticLib.h"

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.