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() {
Static struct {
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.


Крім зазначених вище статей, англійською добре описана проблема в якогось Бартека Хьюго тут.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.