Objective-C, static libraries, categories, -ObjC, біль...

Не всім пощастило писати програми повністю на Swift, та й ще під ios 8+ онлі. Багато легасі на Objective-C, багато залежностей йде через статік либы, ні cocoapods, ні carthage, все ручками. Ми ж круті девелопери, тому строго дотримуємося DRY і все реюзабельные вкусшянки виносимо або в окремі проекти, або в статік бібліотеки. Зараз розглянемо випадок, коли ми зробили класну статичну бібліотеку з не менш прикольним апі, і хотіли б поділитися з товаришами по цеху всередині компанії — на вікі ресурсі/гіті викласти архівчик з либой, хедерами і, звичайно ж, ридмиком де описаний весь апі і як ним користуватися.

Для прикладу заради розглянемо один клас + категорію





На скріншоті у нас структура проекту, де клас + клас категорія, все просто. Збираємо звичайним чином, пишемо readme.md з описом апі і архівуємо бібліотеку. Все круто, залили на вікі, пацанам твитнули в slack/skype/etc і пішли собі за черговим кави. Тільки сіли назад зі свіжозвареним кави і курсор мишки майже досяг закладки на хабр, як в чати посипалися якісь логи, і всі вимагають вашої негайної відповіді, так як проблема в свежезарелизенной лібе. Вас кинуло в піт, адже у вас тестове покриття 146%, на всі сто раз перепровірено. У цей же самий час в чаті вже в лічку знову пишуть той самий лог помилки:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Deadpool guns]: unrecognized selector sent to instance 0x7ffecbc12df0'


Після ознайомлення з логом, причина ясна і до болю знайома коли працюєш зі статік либами. Зрозумівши проблему, ви впевнено витираєте піт з чола, відкриваєте раніше відправлений readme.md і дописуєте:

don't forget to add '-ObjC' flag to 'Other Linker Flags' in Build Settins of Xcode's scheme.

Після, оновили вікі, знову всіх оповістили і начебто все заспокоїлося, меседжери замовкли, кава навіть не встигло охолонути. «Ну зараз точно ніхто мене не оставновит» — шепоче ваш внутрішній голос і курсор миші знову тягнеться до заповітної закладці (ненене, тільки хабр!). Від бажаного тебе відділяє лише клік по лівій кнопці миші, але тебе не покидає думка: «чи Можна було уникнути цієї помилки або як запобігти її в майбутньому?!». «До біса все!» — вигукнув внутрішній голос і курсор потягнувся до Terminal.app.

otool -tV -arch x86_64 libDeadpool.a


видає:

Archive : libDeadpool.a
libDeadpool.a(Deadpool+Guns.o):
(__TEXT,__text) section
-[Deadpool(Guns) guns]:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 subq $0x10, %rsp
0000000000000008 leaq 0x71(%rip), %rax ## Objc cfstring ref: @"sword 2"
000000000000000f movd %rax, %xmm0
0000000000000014 leaq 0x45(%rip), %rax ## Objc cfstring ref: @"sword 1"
000000000000001b movd %rax, %xmm1
0000000000000020 punpcklqdq %xmm0, %xmm1 ## xmm1 = xmm1[0],xmm0[0]
0000000000000024 movdqa %xmm1, -0x10(%rbp)
0000000000000029 movq 0x70(%rip), %rdi ## Objc class ref: NSArray
0000000000000030 movq 0x91(%rip), %rsi ## Objc selector ref: arrayWithObjects:count:
0000000000000037 leaq -0x10(%rbp), %rdx
000000000000003b movl $0x2, %ecx
0000000000000040 callq *_objc_msgSend(%rip)
0000000000000046 addq $0x10, %rsp
000000000000004a popq %rbp
000000000000004b retq
libDeadpool.a(Deadpool.o):
(__TEXT,__text) section
-[Deadpool name]:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 movq 0x1d(%rip), %rsi ## Objc selector ref: class
000000000000000b callq *_objc_msgSend(%rip)
0000000000000011 movq %rax, %rdi
0000000000000014 popq %rbp
0000000000000015 jmp _NSStringFromClass


хм, в самій лібе методи на місці, тепер подивимося вихідні коди програми:

otool -tV -arch x86_64 DemoApp.app/DemoApp | grep Deadpool


видає:

0000000100001ae8 movq 0x21f1(%rip), %rdi ## Objc class ref: Deadpool
0000000100001af6 movq 0x1513(%rip), %r12 ## Objc message: +[Deadpool new]
-[Deadpool name]:


WTF! Окей гугл, де ж все таки метод з категорії?

гугл нам вміло підсовує посилання на документацію эпла за цією якраз проблемі https://developer.apple.com/library/mac/qa/qa1490/_index.html, де швидкий переклад говорить наступне:

The Linker

Коли сі-програма скомпільована, то кожен файл (.c) складений так званий «object file» (.o), який містить імплементації функцій та іншу статичну інформацію. Після лінкер збирає всі ці файли в один файл — executable. І цей executable file якраз і потрапляє всередину нашої .app допомогою Xcode.

Але коли source файл (.c) використовує або, наприклад функцію, що визначено в іншому файлі (інший .c файл), тоді «undefined symbol» записується .o файл для цієї ділянки коду. І на етапі складання линкеру достатньо інформації щоб «undefined symbol» зрозуміти звідки потрібно витягнути потрібну річ щоб зібрати кінцевий executable. Цей опис для складання UNIX static library.


Objective-C

динамічної природи мови цей процес в Objective-C трохи ускладнений, так як пошук реалізації методу відбувається тільки за фактом звернення до цього методу. Objective-C не визначає допоміжних symbols для методів линкеру, а лише визначає symbols для класів. Наприклад, у класі/файлі main.o код:

[[FooClass alloc] initWithBar:nil]

тобто, FooClass це окремий клас, в окремому FooClass.o файлі, так от main.o буде містити тільки «undefined symbol» для самого FooClass, але ніяких додаткових symbols для методу -initWithBar: у цьому класі.

Так як категорія це просто окремий файл з методами, то у лінкера немає абсолютно ніякої інформації, що цей файл потрібно слинковать, так як для методів не створюються допоміжні линкеру «undefined symbol» штуки.


Так, ніби розібралися, ще раз подивимося на байт-код либы:

Archive : libDeadpool.a
libDeadpool.a(Deadpool+Guns.o):
(__TEXT,__text) section
-[Deadpool(Guns) guns]:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 subq $0x10, %rsp
0000000000000008 leaq 0x71(%rip), %rax ## Objc cfstring ref: @"sword 2"
000000000000000f movd %rax, %xmm0
0000000000000014 leaq 0x45(%rip), %rax ## Objc cfstring ref: @"sword 1"
000000000000001b movd %rax, %xmm1
0000000000000020 punpcklqdq %xmm0, %xmm1 ## xmm1 = xmm1[0],xmm0[0]
0000000000000024 movdqa %xmm1, -0x10(%rbp)
0000000000000029 movq 0x70(%rip), %rdi ## Objc class ref: NSArray
0000000000000030 movq 0x91(%rip), %rsi ## Objc selector ref: arrayWithObjects:count:
0000000000000037 leaq -0x10(%rbp), %rdx
000000000000003b movl $0x2, %ecx
0000000000000040 callq *_objc_msgSend(%rip)
0000000000000046 addq $0x10, %rsp
000000000000004a popq %rbp
000000000000004b retq
libDeadpool.a(Deadpool.o):
(__TEXT,__text) section
-[Deadpool name]:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 movq 0x1d(%rip), %rsi ## Objc selector ref: class
000000000000000b callq *_objc_msgSend(%rip)
0000000000000011 movq %rax, %rdi
0000000000000014 popq %rbp
0000000000000015 jmp _NSStringFromClass


Дійсно, у нас скомпилировалось два файлу Deadpool.o та Deadpool+Guns.o, так як другий файл це просто набір методів для першого, то лінкер про нього нічого не знає і тому отримуємо цю помилку тільки в рантайме.

Відразу перше рішення — перенести категорію в файл основного класу. Так, це буде працювати :) але для нас це не зовсім зручно, так як ми звикли всі категорії тримати в окремих течках для порядку.

Інше рішення. Ті, хто використовує нашу либу, повинні вказати -ObjC прапор «Other Linker Flags», цей прапор говорить линкеру завантажити всі всі всі з статичною либы. Ну, нам підходить це рішення тим, що на нашій стороні нічого правити не потрібно. Але якщо подумати, якщо розробник підключить купу ліб і тільки через нашу йому доводиться додавати цей прапор, то він може отримати нехилое збільшення у вазі для свого застосування (я так припускаю).

А можна чи як то сказати линкеру, щоб він зібрав клас і його категорії в один файл? Виявляється є таке і назва йому «Perform Single-Object Prelink» або «GENERATE_MASTER_OBJECT_FILE» в pbxproj файлі. Щоправда відбувається не просто об'єднання класу і його категорії в єдиний файл, а всі файли проекту будуть як єдиний «object file». Якщо це значення виставити в true, то ми повинні отримати поведінка, що хочемо. Перевіримо.

Виставляємо:



otool -tV -arch x86_64 libDeadpool.a


отримуємо:

Archive : libDeadpool.a
libDeadpool.a(libDeadpool.a-x86_64-master.o):
(__TEXT,__text) section
-[Deadpool(Guns) guns]:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 subq $0x10, %rsp
0000000000000008 leaq 0x149(%rip), %rax ## Objc cfstring ref: @"sword 2"
000000000000000f movd %rax, %xmm0
0000000000000014 leaq 0x11d(%rip), %rax ## Objc cfstring ref: @"sword 1"
000000000000001b movd %rax, %xmm1
0000000000000020 punpcklqdq %xmm0, %xmm1 ## xmm1 = xmm1[0],xmm0[0]
0000000000000024 movdqa %xmm1, -0x10(%rbp)
0000000000000029 movq 0x270(%rip), %rdi ## Objc class ref: NSArray
0000000000000030 movq 0x259(%rip), %rsi ## Objc selector ref: arrayWithObjects:count:
0000000000000037 leaq -0x10(%rbp), %rdx
000000000000003b movl $0x2, %ecx
0000000000000040 callq *_objc_msgSend(%rip)
0000000000000046 addq $0x10, %rsp
000000000000004a popq %rbp
000000000000004b retq
-[Deadpool name]:
000000000000004c pushq %rbp
000000000000004d movq %rsp, %rbp
0000000000000050 movq 0x241(%rip), %rsi ## Objc selector ref: class
0000000000000057 callq *_objc_msgSend(%rip)
000000000000005d movq %rax, %rdi
0000000000000060 popq %rbp
0000000000000061 jmp _NSStringFromClass


Що й хотіли, зараз все в одному файлі. Прибираємо з програми -ObjC і пересобираем з новою версією нашої бібліотеки і дивимося:

otool -tV -arch x86_64 DemoApp.app/DemoApp | grep Deadpool


висновок:

0000000100001a70 movq 0x22c9(%rip), %rdi ## Objc class ref: Deadpool
0000000100001a7e movq 0x158b(%rip), %r12 ## Objc message: +[Deadpool new]
-[Deadpool(Guns) guns]:
-[Deadpool name]:


Відмінно. Зараз можна з readme.md видаляти інформацію про -ObjC прапорі, сміливо відкривати хабр і допивати, на жаль, вже остиглий кави )

пс.

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

Корисні посилання:

https://developer.apple.com/library/mac/qa/qa1490/_index.html
http://stackoverflow.com/questions/2567498/objective-c-categories-in-static-library

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.