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