iOS-разработчик
0,0
рейтинг
5 января в 13:59

Разработка → Method Swizzling и Swift: но есть нюанс из песочницы

Иногда для удобства, иногда для того, чтобы обойти баг в фрэймворке, а иногда просто от безысходности, может понадобиться переопределить поведение некоторого метода класса, созданного кем-то другим. Method Swizzling позволяет подменить метод вашим прямо в runtime, притом оставляя оригинальную имплементацию доступной.

В статье Objective-C Runtime. Теория и практическое применение этот процесс хорошо описан. Но с переходом на Swift появляются некоторые нюансы.

Давайте начнем с примера. Допустим, в стороннем фрэймворке Charts, есть некоторый класс ChartRenderer, в котором есть метод drawDots, который на графике отрисовывает кружочек в узловых точках. А нам очень хочется не кружочек, а звездочку. И внутрь фрэймворка лезть нам тоже очень не хочется, потому что время от времени выходят его обновления и лишаться их — жалко.

Поэтому мы создадим extension класса ChartRenderer, который будет прямо в runtime подменять метод drawDots на наш метод drawStars. Не нужно беспокоиться как остальные объекты в Charts обращаются к ChartRenderer, каждый раз на вызов метода drawDots отвечать будет метод drawStars.

extension ChartRenderer {
    public override class func initialize() {
        struct Static {
            static var token: dispatch_once_t = 0
        }

        // убедимся, что это не сабкласс
        if self !== UIViewController.self {
            return
        }

        dispatch_once(&Static.token) {
            let originalSelector = Selector("drawDots:")
            let swizzledSelector = Selector("drawStars:")

            let originalMethod = class_getInstanceMethod(self, originalSelector)
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

            let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

            if didAddMethod {
                class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod)
            }
        }
    }

    //наш метод
    func drawStars(color: CGColor) {
       //рисуем звезды

       //также интересно, что если отсюда вызвать drawStars(...), 
       //то отработает старый метод drawDots(...), отозвавшись на селектор "drawStars:"
    }
}

Во всех материалах по Objective-C пишут, что подменять методы нужно в load(), а не в initialize(), потому что первый вызывается только один раз для класса, а второй — еще и для всех сабклассов. Но так как runtime при использовании Swift не вызывает load() вообще, приходится убеждаться, что этот initialize() вызван классом, а не кем-то из наследников, и что замена будет выполнена лишь однажды с помощью dispatch_once.

Стоит отметить, что существует и альтернативный вариант, более простой, но менее наглядный: вместо extension выполнять все эти операции в AppDelegate в application(_:didFinishLaunchingWithOptions:).

В случае, если ChartRenderer написан на Objective-C, все должно заработать уже так. Хорошая статья есть на NSHipster (англ).

А теперь о нюансе, из-за которого можно сломать голову. При использовании method swizzling в случае, когда и проект, и класс с подменяемым методом написаны на Swift, этот должен удовлетворять двум требованиям:

  • он должен быть наследником NSObject
  • подменяемый метод должен иметь атрибут dynamic

Примерно так:

class ChartRenderer: NSObject
{
    dynamic func drawDots()
    {
        //рисуем круги
    }
}

Природа этого нюанса объясняется в статье Using Swift with Cocoa and Objective-C:
Requiring Dynamic Dispatch

While the objc attribute exposes your Swift API to the Objective-C runtime, it does not guarantee dynamic dispatch of a property, method, subscript, or initializer. The Swift compiler may still devirtualize or inline member access to optimize the performance of your code, bypassing the Objective-C runtime. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched. Because declarations marked with the dynamic modifier are dispatched using the Objective-C runtime, they’re implicitly marked with the objc attribute.
Requiring dynamic dispatch is rarely necessary. However, you must use the dynamic modifier when you know that the implementation of an API is replaced at runtime. For example, you can use the method_exchangeImplementations function in the Objective-C runtime to swap out the implementation of a method while an app is running. If the Swift compiler inlined the implementation of the method or devirtualized access to it, the new implementation would not be used.


Кроме указанных выше статей, на английском хорошо описана проблема у некоего Бартека Хьюго здесь.
Anatoly Rosencrantz @abjurato
карма
5,0
рейтинг 0,0
iOS-разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • 0
    И внутрь фрэймворка лезть нам тоже очень не хочется, потому что время от времени выходят его обновления и лишаться их — жалко.

    а если класс не наследник NSObject?
    ну или хотя б метод не dynamic?

    тогда только лезть? часто ли пишете такие условия сами?
    • 0
      А если не dynamic и не наследник NSObject, то выбора нет и method swizzling не взлетит, сколько ни мучайся. И знать это — не лишне.
      • 0
        Получается в Swift можно использовать swizzling, только если для этого разработчики сами приложат усилия? Практически бесполезно получается ((
        Кстати, а что по поводу SWRoute? Запустить у себя не получилось, видимо не совместимо с последней версией, но люди поставили звезды, возможно у них получилось, не пробовали разобраться с новой версией языка?
        • 0
          Я как-то больше с iOS работаю, а эта штука на нем не запустится, если верить ее документации
          • 0
            Автор пишет, что можно выбрать другую библиотеку для замены основы под iOS.
            Да и если это работает, пусть и не под iOS, тогда вопрос, может все таки можно без NSObject и dynamic? Почему бы не раскопать эту тему, вместо того чтобы просто написать «вот так можно, это логично и просто и все тут», это будет намного интереснее и полезнее. О этой либе упоминается как раз в приведенном вами источнике
            • 0
              Хм, потому что в этих библиотеках схожая задача решается ужасными методами и играми с памятью, которые даже по описаниям авторов there are still innumerable ways that this code can explode in your face?
  • 0
    Подождите, тут предлагается адаптированная под русского пользователя копия статьи с NSHipster (структура повествования + код, за исключением названий класса и пары методов), плюс в конце дополнение, что чтобы это работало, то надо чтобы класс и метод был виден в obj-Runtime.
    По мне это перевод + чуть-чуть автора.
    • 0
      Весь кусок, переведенный с NSHipster — это «как должно работать», а моя часть — почему не работает. Не думаю, что у тех, кому может понадобиться разбираться с этими механизмами, есть проблемы с английским, просто так переводить оттуда (и давать ссылку на оригинал) не имело бы смысла

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.