Objective-C, static libraries, categories, -ObjC, боль…

в 1:00, , рубрики: objective-c, xcode, вредные советы

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

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

Objective-C, static libraries, categories, -ObjC, боль… - 1

На скриншоте у нас структура проекта, где класс + класс категория, всё просто. Собираем обычным образом, пишем 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 файл как раз и попадает внутрь нашей .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, то мы должны получить поведение, которое хотим. Проверим.

Выставляем:

Objective-C, static libraries, categories, -ObjC, боль… - 2

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

Автор: house2008

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js