Pull to refresh

Method Swizzling и Swift: но есть нюанс

Reading time 3 min
Views 28K
Иногда для удобства, иногда для того, чтобы обойти баг в фрэймворке, а иногда просто от безысходности, может понадобиться переопределить поведение некоторого метода класса, созданного кем-то другим. 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.


Кроме указанных выше статей, на английском хорошо описана проблема у некоего Бартека Хьюго здесь.
Tags:
Hubs:
+5
Comments 10
Comments Comments 10

Articles