Для примера ради рассмотрим один класс + его категорию
На скриншоте у нас структура проекта, где класс + класс категория, всё просто. Собираем обычным образом, пишем 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! Окей гугл, где же все таки метод из категории?
гугл нам умело подсовывает ссылку на документацию эпла по этой как раз проблеме http://ift.tt/1kroaiT, где беглый перевод говорит следующее:
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, то мы должны получить поведение, которое хотим. Проверим.
Выставляем:
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 флаге, смело открывать хабр и допивать, к сожалению, уже остывший кофе )
пс.
Проблема старая, давно ее решил, сейчас вот дошли руки написать и более подробно в этом разобраться )
Не уверен в идеальности решения, но мне помогло с этой проблемой, может кому будет интересно.
Полезные ссылки:
Комментарии (0)