Введение
Каждый iOS-разработчик, иногда сам того не осознавая, сталкивается с диспетчеризацией методов. Знания принципов работы диспетчеризации методов необходимы при написании кода, поскольку эти знания позволят повысить производительность приложения, а также не допустить ошибок, связанных с неочевидным поведением в Swift.
В данной статье будут рассмотрены понятие диспетчеризации, его виды, преимущества и недостатки, а также приведены небольшие задачи для укрепления знаний.
Что такое диспетчеризация методов?
Диспетчеризацией методов называют процесс поиска адреса инструкций, которые нужно выполнить CPU при вызове определенного метода.
Другими словами, цель диспетчеризации методов состоит в том, чтобы программа сообщала процессору, где он может найти исполняемый код конкретного метода в памяти.
Основные виды диспетчеризации
Статическая диспетчеризация (Direct Dispatch)
Статическая диспетчеризация, которую иногда называют прямой диспетчеризацией, является самым быстрым вариантом диспетчеризации. Причина этой скорости заключается в том, что статическая диспетчеризация накладывает запрет на переопределение методов, из-за чего существует только одна реализация каждого метода. В данном случае адрес необходимых инструкций известен уже на стадии компиляции программы, что позволяет системе переходить к необходимым инструкциям без поиска. Существует даже способ оптимизации под названием девиртуализация, с помощью которого компилятор пытается при возможности сделать функции статическими.
Статическая диспетчеризация
Адреса известны на стадии компиляции
+ |
- |
---|---|
Самая быстрая |
Накладывает ограничения (запрет наследования и переопределения методов) |
Табличная диспетчеризация (Table Dispatch)
Данный метод основан на том, что класс хранит таблицу с указателями на реализацию методов. Это метод используется по умолчанию в Swift для ссылочных типов. Причина, по которой этот вид диспетчеризации используется по умолчанию для ссылочных типов, заключается в том, что классы должны поддерживать наследование. В табличной диспетчеризации у каждого класса есть таблица с указателями на функции. Каждый подкласс имеет свою копию таблицы с другим указателем функции для каждого метода, переопределенного классом. Новые методы подкласса добавляются в конец этого массива.
Существуют правила создания виртуальных таблиц:
Если метод расширен из суперкласса, скопируйте адрес метода в таблицу функций подкласса.
Если метод создан в подклассе, добавьте адрес метода в конец таблицы функций.
Если метод переопределен, добавьте адрес нового метода в таблицу функций подкласса.
Приведем популярный пример. Создадим классы Parent и Child. Child является наследником класса Parent. Каждый класс реализует два метода, причем класс Child переопределяет один метод класса Parent:
class Parent {
func method1() {}
func method2() {}
}
class Child: Parent {
override func method2() {}
func method3() {}
}
На этапе компиляции будут созданы две виртуальные таблицы:
При вызове method2 в экземпляре класса Child запустится следующий алгоритм:
Чтение виртуальной таблицы класса Child по адресу 0xB00
Чтение ссылки по адресу 0xB00 + 1. В нашем случае индекс метода для method2 равен 1, поэтому считывается адрес 0xB00 + 1
Переход по ссылке
Табличная диспетчеризация
Таблицы создаются во время компиляции, но к таблице обращаются во время выполнения, чтобы определить метод для запуска
+ |
- |
---|---|
Простота в имплементации |
Медленнее статической диспетчеризации |
Диспетчеризация на сообщениях (Message Dispatch)
Диспетчеризация на сообщениях является наиболее динамичной, но при этом самой медленной, хотя этот недостаток отчасти нивелируется механизмом кэширования, который делает поиск почти таким же быстрым, как в случае с табличной диспетчеризацией. Swift обычно не использует данный вид диспетчеризации, поскольку это концепция Objective-C. Для принудительной смены диспетчеризации необходимо использовать dynamic модификатор. Рассмотрим пример:
class Parent {
dynamic func method1() {}
dynamic func method2() {}
}
class Child: Parent {
override func method2() {}
dynamic func method3() {}
}
В данном случае Child класс хранит ссылку на super (Parent). Для того, чтобы программа поняла какой метод нужно выполнить, необходимо произвести проход по всей иерархии. Это времязатратная операция, но, пройдя по иерархии один раз, в следующий раз скорость нахождения нужных инструкций будет сравнима с табличной диспетчеризацией из-за механизма кэширования.
Диспетчеризация на сообщениях предоставляет разработчикам дополнительные инструменты. Метод swizzling позволяет подменить метод прямо во время выполнения, притом оставляя оригинальную имплементацию доступной, а isa-swizzling позволяет менять тип объекта.
Диспетчеризация на сообщениях
Обходит иерархию классов во время выполнения, чтобы определить, какой метод следует вызвать.
+ |
- |
---|---|
Динамичней табличной диспетчеризации |
Медленнее табличной диспетчеризации |
Может изменять поведение метода и тип объекта во время выполнения |
Выбор варианта диспетчеризации и их модификаторы
На выбор варианта диспетчеризации влияют тип объекта и место объявления метода. Swift имеет ряд модификаторов, которые изменяют способ диспетчеризации.
final
final включает статическую диспетчеризацию. Он включается по умолчанию, если метод не переопределен или класс не унаследован. Этот модификатор накладывает запрет на переопределение и наследование, а также скрывает метод от среды выполнения Objective-C. Но нужно учесть, что final включает статическую диспетчеризацию только там, где это возможно, но не отключает табличную, например final override
.
dynamic
dynamic включает диспетчеризацию на сообщениях, но при этом не открывает видимость для среды выполнения Objective-C.
@objc / @nonobjc
@objc и @nonobjc изменяет видимость для среды выполнения Objective-C. @nonobjc выключает видимость и используется по умолчанию для оптимизации, выключая диспетчеризацию на сообщениях. @objc наоборот включает видимость для среды выполнения Objective-C.
Общие правила отображены на рисунке:
Влияние на производительность
Статическая диспетчеризация менее затратная по сравнению с динамической. Для повышения производительности, задача компилятора и разработчика заключается в том, чтобы, как можно больше методов использовали статическую диспетчеризацию, в этом нам поможет следующие ключевые слова:
final
не позволяет наследоваться классам, а методам переопределяться, что приводит к статической диспетчеризацииprivate
ограничивает видимость метода или всего класса. Отсутствие каких-либо переопределений позволяет компилятору автоматически добавлять ключевое слово finalWhole Module Optimization
позволяет компилятору просматривать все исходные файлы в едином модуле. Это позволяет компилятору использоватьfinal
для всех методов без переопределений.
Данная статья Apple более подробно рассказывает про способы повышения производительности, уменьшая использование табличной диспетчеризации.
Задачи
Для лучшего понимания темы рассмотрим три задачи:
-
Что отобразится при запуске кода и какие способы диспетчеризации используются при вызове методов?
protocol Animals { } extension Animals { func method() { print("????") } } struct Pets: Animals { func method() { print("????") } } let firstAnimal = Pets() firstAnimal.method() let secondAnimal: Animals = Pets() secondAnimal.method()
Ответ
После выполнения строки firstAnimal.method() отобразится “????”, после выполнения secondAnimal.method() отобразится ”????”.
firstAnimal: метод объявлен внутри структуры - статическая диспетчеризация
secondAnimal: метод объявлен в расширении к протоколу - статическая диспетчеризация -
Какие варианты диспетчеризации используется при вызове методов?
protocol Animals { func method() } struct Pets: Animals { func method() { print("????") } } let firstAnimal = Pets() firstAnimal.method() let secondAnimal: Animals = Pets() secondAnimal.method()
Ответ
firstAnimal: метод объявлен внутри структуры - статическая диспетчеризация secondAnimal: метод объявлен при декларации протокола - табличная диспетчеризация
-
Скомпилируется ли код? Если нет, то что нужно добавить?
class Animals { } extension Animals { func method() { } } class Pets: Animals { override func method() { } }
Ответ
Компилятор выведет следующую ошибку:
Non@objcc instance method 'method()' is declared in extension of 'Animals' and cannot be overridden
.
Для Swift возможность переопределять методы расширений запрещена на уровне компилятора, но для Objective-C такая возможность сохранилась для совместимости. Для исправления ошибки необходимо добавить ключевое слово @objc перед объявлением функции в расширении, что включит видимость Objective-C.
Компилятор позволяет переопределять расширение для совместимости с Objective-C, но на самом деле, это нарушает языковую директиву, поэтому стоит избегать подобных конструкций.
Заключение
Мы изучили основные виды диспетчеризации методов, разобрали выбор варианта диспетчеризации и их модификаторы, а также рассмотрели способы увеличения производительности путём использования статической диспетчеризации. Следующие советы помогут вам писать более производительный код:
По возможности используйте value type вместо reference type
По возможности используйте расширения для объявления методов
Используйте ключевое слово final для классов и методов, если класс не наследуется, а метод не переопределяется
Используйте ключевое слово private для ограничения области видимости
По возможности не используйте ключевое слово dynamic
SwifteriOS
firstAnimal: метод объявлен внутри структуры - статическая диспетчеризация
class Pets: Animals {
Но там же класс. Получается, что тут тоже будет табличная диспетчеризация, правильно я понимаю?
ilichev1 Автор
Спасибо, подправил! Да, если
class Pets: Animals{
- табличная, еслиstruct Pets: Animals{
- статическая