Как работает message dispatch в Objective-C
Что проверяют
Знаете ли вы полную цепочку от [obj foo] до падения с unrecognized selector, включая три точки перехвата. Это база для swizzling, прокси-объектов и для понимания диспетчеризации в Swift.
Как отвечать
- Компилятор превращает
[obj foo:x]вobjc_msgSend(obj, @selector(foo:), x). Селектор это не строка, а уникализированный идентификатор имени метода. - Поиск реализации (IMP): сначала кэш методов класса (быстрый путь на ассемблере), затем список методов класса, затем вверх по цепочке суперклассов.
- Сообщение к
nilбезопасно: возвращает 0, nil или обнулённую структуру, исключения нет. Частый источник молча пропущенной логики. - Метод не найден, значит рантайм идёт по трём этапам:
+resolveInstanceMethod:(добавить реализацию черезclass_addMethod),-forwardingTargetForSelector:(перенаправление другому объекту), полный форвардинг из-methodSignatureForSelector:и-forwardInvocation:сNSInvocation. Дальше-doesNotRecognizeSelector:и падение.
- (id)forwardingTargetForSelector:(SEL)sel {
if ([self.backend respondsToSelector:sel]) return self.backend;
return [super forwardingTargetForSelector:sel];
}- Есть варианты:
objc_msgSendSuperдля super,objc_msgSend_stretи_fpretдля структур и вещественных чисел на старых архитектурах (на arm64 stret не нужен). - Отсюда swizzling:
method_exchangeImplementationsменяет связь селектора с реализацией в рантайме.
Где спотыкаются
Пропускают resolveInstanceMethod: и forwardingTargetForSelector:, сразу называя forwardInvocation:. Полный форвардинг самый дорогой, ждут все три ступени по порядку.
Реализуют forwardInvocation: без methodSignatureForSelector:: без сигнатуры NSInvocation не построить, и вызов упадёт раньше перехвата.
Называют динамическую отправку медленной вообще. Кэшированный вызов стоит единицы наносекунд, разница видна только в горячих циклах, где IMP кэшируют заранее.
Переносят схему на Swift. Там статическая диспетчеризация и таблицы, objc_msgSend включается только для членов с @objc dynamic.