iOSDC 2018 で @_bannzai_ さんの ~~ †††† 漆黒の魔法 Objecitve-C Runtime API †††† ~~
というセッションを聞いたので、 Objective-C の Method Swizzling について書いてみます。
セッションの資料はこちらです
Method Swizzling とは何か
一言で言うと、「メソッドのセレクタと実装の紐づけを変更する」機能です。
簡単な例を見てみましょう。
@implementation ViewController - (void)viewDidLoad { [self originalMethod]; Method from = class_getInstanceMethod([self class], @selector(originalMethod)); Method to = class_getInstanceMethod([self class], @selector(swizzledMethod)); method_exchangeImplementations(from, to); [self originalMethod]; } - (void)originalMethod { NSLog(@"This is original method"); } - (void)swizzledMethod { NSLog(@"This is SWIZZLED method"); [self swizzledMethod]; } @end
このコードを実行すると、以下のように出力されます。
2018-08-31 22:34:29.945 Runtime[35929:890092] This is original method 2018-08-31 22:34:29.946 Runtime[35929:890092] This is SWIZZLED method 2018-08-31 22:34:29.946 Runtime[35929:890092] This is original method
method_exchangeImplementations
で originalMethod と swizzledMethod のセレクタと実装を入れ替えたので、二度目の [self originalMethod];
呼び出しで swizzledMethod の実装が呼ばれています。
図で表すとこのようになります。
メソッドを入れ替える前は、 originalMethod のセレクタに対して originalMethod の実装、 swizzledMethod のセレクタに対して swizzledMethod の実装が紐付いていました。
メソッドを入れ替えると、 originalMethod のセレクタに対して swizzledMethod の実装、 swizzledMethod のセレクタに対して originalMethod の実装が紐付けられます。
swizzledMethod の実装で [self swizzledMethod];
を呼んでいるため無限ループしそうに見えますが、実際には originalMethod の実装が呼ばれるのでループは起こりません。
Firebase iOS SDK で Method Swizzling している例
Firebase Cloud Messaging で APNs のデバイストークンを取得する箇所を例に挙げます。
Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m#L136
- (void)swizzleAppDelegateMethods:(id<UIApplicationDelegate>)appDelegate { Class appDelegateClass = [appDelegate class]; SEL registerForAPNSSuccessSelector = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:); // Receive APNS token [self swizzleSelector:registerForAPNSSuccessSelector inClass:appDelegateClass withImplementation:(IMP)FCM_swizzle_appDidRegisterForRemoteNotifications inProtocol:@protocol(UIApplicationDelegate)]; }
まず、 AppDelegate の application:didRegisterForRemoteNotificationsWithDeviceToken:
と FIRMessagingRemoteNotificationsProxy の FCM_swizzle_appDidRegisterForRemoteNotifications
を入れ替えます。
Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m#L372
- (void)swizzleSelector:(SEL)originalSelector inClass:(Class)klass withImplementation:(IMP)swizzledImplementation inProtocol:(Protocol *)protocol { Method originalMethod = class_getInstanceMethod(klass, originalSelector); if (originalMethod) { // This class implements this method, so replace the original implementation // with our new implementation and save the old implementation. IMP __original_method_implementation = method_setImplementation(originalMethod, swizzledImplementation); IMP __nonexistant_method_implementation = [self nonExistantMethodImplementationForClass:klass]; if (__original_method_implementation && __original_method_implementation != __nonexistant_method_implementation && __original_method_implementation != swizzledImplementation) { [self saveOriginalImplementation:__original_method_implementation forSelector:originalSelector]; } } else { // The class doesn't have this method, so add our swizzled implementation as the // original implementation of the original method. struct objc_method_description method_description = protocol_getMethodDescription(protocol, originalSelector, NO, YES); BOOL methodAdded = class_addMethod(klass, originalSelector, swizzledImplementation, method_description.types); if (!methodAdded) { FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyMethodNotAdded, @"Could not add method for %@ to class %@", NSStringFromSelector(originalSelector), NSStringFromClass(klass)); } } [self trackSwizzledSelector:originalSelector ofClass:klass]; }
Swizzling 処理の中身です。 originalMethod が存在する場合、 originalMethod の実装と swizzledImplementation を入れ替えます。 originalMethod が存在しない場合は、入れ替えではなく単に swizzledImplementation を追加します。
Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m#L709
void FCM_swizzle_appDidRegisterForRemoteNotifications(id self, SEL _cmd, UIApplication *app, NSData *deviceToken) { // Pass the APNSToken along to FIRMessaging (and auto-detect the token type) [FIRMessaging messaging].APNSToken = deviceToken; IMP original_imp = [[FIRMessagingRemoteNotificationsProxy sharedProxy] originalImplementationForSelector:_cmd]; if (original_imp) { ((void (*)(id, SEL, UIApplication *, NSData *))original_imp)(self, _cmd, app, deviceToken); } }
FCM_swizzle_appDidRegisterForRemoteNotifications
でトークンを設定したあと、元々の実装があればそれを呼んでいます。
これによって、アプリ側に意識させずにデバイストークンを取得することが可能になっています。
まとめ
Method Swizzling は強力な機能ですが、ぱっと見で挙動を把握することが難しいため、安易に使用すると思わぬ事故の元になります。 ご利用は計画的に。