Edited at

既存クラスのメソッドを入れ替える(Method Swizzling)

More than 1 year has passed since last update.

状況:


  • 画面が10個ほどあるiPhoneアプリ

  • UIViewControllerのviewWillAppear等にログを仕込みたい。

  • すでにUIViewControllerを継承したクラスが多数あって、全部にログを仕込むのはめんどくさいしダサくていやだ。

解決策:


  • method_exchangeImplementationsでメソッドを入れ替えてしまえばいいよ!


黒魔法へようこそ

Objective-C Runtime には、method_exchangeImplementations()という関数が用意されています。これで「メソッドの交換」が可能となります。これはSDKで提供されているクラスのメソッドも入れ替える事が可能です。なのでUIViewControllerのviewWillAppear等を自作メソッドと差し替える事が出来ます。この差し替えは、動的に行われるので、動作する環境に応じて入れ替えをコントロールしたり、元に戻したりといった事が可能となります。

この方法は、"Method Swizzling"と呼ばれるものみたいです。

以下のような実装をします。


  • カテゴリ拡張で、入れ替えるメソッドを用意します。


    • このとき、「元々実装していたメソッド名」と「入れ替えた実装のメソッド名」も入れ替わるので、無限ループしそうなソースコードになります。



  • class_getInstanceMethod()で、メソッドへの参照(Method)を取得します。

  • method_exchangeImplementations()で、メソッドを入れ替えます。


サンプルコード

サンプルコードでは、UIViewControllerのviewDidLoadviewWillAppearviewDidAppearviewWillDisappearviewDidDisappearを、NSLog出力付きの自作メソッドに入れ替えています。

まずは、UIViewControllerをカテゴリ拡張して、入れ替えるメソッドの実装と、method_exchangeImplementations()を呼び出すクラスメソッドの実装を行います。


UIViewController+MethodSwitch.h

#import <UIKit/UIKit.h>

@interface UIViewController(MethodSwitch)
+(void)switchLoggingMethod;
@end


UIViewController+MethodSwitch.m

#import "UIViewController+MethodSwitch.h"

#import <objc/runtime.h>

@implementation UIViewController(MethodSwitch)

// 入れ替えるメソッドを準備
-(void)loggingViewDidLoad
{
NSLog(@"%@ viewDidLoad",NSStringFromClass(self.class));
[self loggingViewDidLoad]; // ここ、無限ループしそうだけど、メソッドの実装が入れ替わるので、元々あったviewDidLoadの実装が呼ばれます。
}
-(void)loggingViewWillAppear:(BOOL)animated{
NSLog(@"%@ viewWillAppear",NSStringFromClass(self.class));
[self loggingViewWillAppear:animated];
}
-(void)loggingViewDidAppear:(BOOL)animated{
NSLog(@"%@ viewDidAppear",NSStringFromClass(self.class));
[self loggingViewDidAppear:animated];
}
-(void)loggingViewWillDisappear:(BOOL)animated{
NSLog(@"%@ viewWillDisappear",NSStringFromClass(self.class));
[self loggingViewWillDisappear:animated];
}
-(void)loggingViewDidDisappear:(BOOL)animated{
NSLog(@"%@ viewDidDisappear",NSStringFromClass(self.class));
[self loggingViewDidDisappear:animated];
}

+(void)switchLoggingMethod
{
// メソッドを入れ替える
[self switchInstanceMethodFrom:@selector(viewDidLoad) To:@selector(loggingViewDidLoad) ];
[self switchInstanceMethodFrom:@selector(viewWillAppear:) To:@selector(loggingViewWillAppear:) ];
[self switchInstanceMethodFrom:@selector(viewDidAppear:) To:@selector(loggingViewDidAppear:) ];
[self switchInstanceMethodFrom:@selector(viewWillDisappear:) To:@selector(loggingViewWillDisappear:)];
[self switchInstanceMethodFrom:@selector(viewDidDisappear:) To:@selector(loggingViewDidDisappear:) ];
}

+(void)switchInstanceMethodFrom:(SEL)from To:(SEL)to
{
// メソッドの入れ替えの実態はここ
Method fromMethod = class_getInstanceMethod(self,from);
Method toMethod = class_getInstanceMethod(self,to );
method_exchangeImplementations(fromMethod, toMethod);
}
@end


また、AppDelegate内で、[UIViewController switchLoggingMethod]を呼び出します。


AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{
// Override point for customization after application launch.
[UIViewController switchLoggingMethod]; // ←これでメソッドが入れ替わる
return YES;
}

実行するとデバッグエリアにログが出力されます。

MethodSwitch_xcodeproj_—_AppDelegate_m.png

UIViewControllerそのものでも有効になりますし、それを継承したクラス(UINavigationController等)でも有効になります。


用量用法を守って、正しくお使いください。

このメソッドの入れ替えですが、普通にコードを読んで入れ替わってる事に気づくのは難しいです。知らない人がソースコードを読むと、「viewDidLoadに何も実装していないのにログが出力されるナンデ?!」と大混乱する事必至です。チームメンバーや未来の自分を混乱させないように、この辺りの情報はがっつり残しておきましょう。


でも、悪戯って楽しいよね。

例えば NSObject のメソッドを入れ替えちゃったりして。うへへへ


参考にしたサイト

Safari用独自プラグインを作る(4) - Method Swizzling を試す

method_exchangeImplementationsでクラスメソッドを置換する

☆ モテる Objective-C 女子力を磨くための4つの心得「method swizzling できない女をアピールせよ」等 ←タイトルに負けた