Objective-CでAOP (アスペクト指向) ができるライブラリ
※ 今回は試しにQiitaにも投稿してみました。
Aspects
Objective-Cでインターセプター入れて横断的に処理入れてみたいなことってしたいなと思う場面はあったけど、共通処理を行うクラスを使ってそれを継承するみたいなやり方してた。共通の処理ってどんなものがあるかといえば、例えば、ログ出力とか、GAのトラッキングの送信とかですね。
JavaとかだとFrameworkでだいたい用意されててObjective-Cでもあったらなーとは思ってたけど、今回、AOPができるめっちゃいいライブラリを知ったので紹介します。
Aspects steipete/Aspects · GitHub
このライブラリはEvernoteやDropboxでも使われているPDFのライブラリ PSPDFKit の作者である @steipeteさんが作ったライブラリなので、信頼もおけます。
しかも、ライブラリの中身はAspects.h
とAspects.m
だけだからすごく軽量です。
使い方は、READMEで十分分かりやすいですが、一応試してみたので、紹介します。(Aspects ver 1.4.1 2014/5/21時点)
使い方
インストールは、cocoapodsに登録されてるので簡単。それか実際のファイル自体は、Aspects.h
とAspects.m
の2つだからそれだけ持ってきてもOK。
例えば、AppDelegate.m
のapplication:didFinishLaunchingWithOptions:
あたりで、登録する。こんな感じ。
#import "AppDelegate.h" #import "Aspects.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. NSError *error = nil; [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { UIViewController *vc = [aspectInfo instance]; NSArray *args = [aspectInfo arguments]; NSLog(@"viewWillAppearが呼ばれる前にインターセプト"); NSLog(@"呼ばれたインスタンス:%@", vc); NSLog(@"呼ばれたメソッドの引数の数:%ld", [args count]); NSLog(@"配列の中身はボクシングされている:%@", [[args firstObject] class]); NSLog(@"引数が分かっていれば直接ブロックの引数でも取得可能:%@", animated ? @"YES":@"NO"); } error:&error]; return YES; }
実行すると、こんな感じで全てのUIViewController
のviewWillAppear:
が呼ばれるところでログが出力される。
2014-05-22 01:04:36.524 AspectsSample[26879:60b] viewWillAppearが呼ばれる前にインターセプト 2014-05-22 01:04:36.530 AspectsSample[26879:60b] 呼ばれたインスタンス:<UIViewController: 0x109273210> 2014-05-22 01:04:36.533 AspectsSample[26879:60b] 呼ばれたメソッドの引数の数:1 2014-05-22 01:04:36.533 AspectsSample[26879:60b] 配列の中身はボクシングされている:__NSCFBoolean 2014-05-22 01:04:36.535 AspectsSample[26879:60b] 引数が分かっていれば直接ブロックの引数でも取得可能:NO
まぁ、これくらいだと、別に基底クラス作って継承すれば、まぁいいんだけど次のような構成になったとたん破滅します。
こんな風に、途中でUIViewController
以外のUITableViewController
なんかが入ってくると、BaseViewController
はUIViewController
のサブクラスなので、ThirdTableViewController
はBaseViewController
を継承できない!
どうしよう、基底クラスがまた増えちゃう。。
こんなときにUIViewControllerクラス自体に、インターセプタ入れれば、わざわざ基底クラスなんて作らなくてもOKになります。すごく便利ですね!
Aspectsなら、最後のThirdTableViewControllerのviewWillAppear:
も引っ掛けられます。結果はこんな感じ。
2014-05-22 01:12:15.521 AspectsSample[27046:60b] viewWillAppearが呼ばれる前にインターセプト 2014-05-22 01:12:15.547 AspectsSample[27046:60b] 呼ばれたインスタンス:<UINavigationController: 0x109278b90> 2014-05-22 01:12:15.579 AspectsSample[27046:60b] viewWillAppearが呼ばれる前にインターセプト 2014-05-22 01:12:15.580 AspectsSample[27046:60b] 呼ばれたインスタンス:<RootViewController: 0x109279210> 2014-05-22 01:12:30.231 AspectsSample[27046:60b] viewWillAppearが呼ばれる前にインターセプト 2014-05-22 01:12:30.232 AspectsSample[27046:60b] 呼ばれたインスタンス:<SecondViewController: 0x1092a7660> 2014-05-22 01:12:31.705 AspectsSample[27046:60b] viewWillAppearが呼ばれる前にインターセプト 2014-05-22 01:12:31.706 AspectsSample[27046:60b] 呼ばれたインスタンス:<ThirdTableViewController: 0x1092ac880>
一つ想定外でしたが、UIWindow
のrootViewController
として設定してるUINavigationController
もUIViewController
のサブクラスだからログが出力されてますね。
応用編
今回は、クラスに対してインターセプタを入れましたが、インスタンスに対しても設定できます。
Hoge
インスタンスのfuga
プロパティに設定した値がいつのまにか上書きされてしまってなぜだー!!みたいなときも、これで、setFuga:
にインターセプタを入れておけば、上書きされる場所がデバッグできますね。
#import "RootViewController.h" #import "Hoge.h" #import "Aspects.h" @interface RootViewController () @property (nonatomic) Hoge *hoge; @end @implementation RootViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.hoge = [[Hoge alloc] init]; [self.hoge aspect_hookSelector:@selector(setFuga:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info, NSString *fuga) { Hoge *hoge = [info instance]; NSLog(@"setFuge:が呼ばれたときのスタックトレース"); NSLog(@"%@", [NSThread callStackSymbols]); NSLog(@"インスタンス:%@", hoge); NSLog(@"引数:%@", fuga); } error:nil]; self.hoge.fuga = @"hogehoge"; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.hoge.fuga = @"fugafuga"; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; self.hoge.fuga = @"piyopiyo"; }
こうしておくと、こんな感じにログから呼び出しもとが分かります。
// viewDidLoadのところ 2014-05-11 03:06:07.152 AspectsSample[11868:60b] ( ・ 4 AspectsSample 0x0000000100001990 -[RootViewController viewDidLoad] + 336 ・ ) <Hoge: 0x10929ce20> set: hogehoge // viewWillAppearのところ 2014-05-11 03:06:07.174 AspectsSample[11868:60b] ( ・ 4 AspectsSample 0x0000000100001b5d -[RootViewController viewWillAppear:] + 125 ・ ) <Hoge: 0x10929ce20> set: fugafuga // viewDidAppearのところ 2014-05-11 03:06:07.561 AspectsSample[11868:60b] ( ・ 4 AspectsSample 0x0000000100001bed -[RootViewController viewDidAppear:] + 125 ・ ) <Hoge: 0x10929ce20> set: piyopiyo
peterさんのREADME見ればもっと色々書いてありますので、興味を持った人は見てみて下さい。
ちなみに今回、試したサンプルも置いておきます。 mpon/AspectsSample · GitHub