Objective-CでAOP (アスペクト指向) ができるライブラリ

※ 今回は試しにQiitaにも投稿してみました。

Aspects

Objective-Cでインターセプター入れて横断的に処理入れてみたいなことってしたいなと思う場面はあったけど、共通処理を行うクラスを使ってそれを継承するみたいなやり方してた。共通の処理ってどんなものがあるかといえば、例えば、ログ出力とか、GAのトラッキングの送信とかですね。

JavaとかだとFrameworkでだいたい用意されててObjective-Cでもあったらなーとは思ってたけど、今回、AOPができるめっちゃいいライブラリを知ったので紹介します。

Aspects steipete/Aspects · GitHub

このライブラリはEvernoteDropboxでも使われているPDFのライブラリ PSPDFKit の作者である @steipeteさんが作ったライブラリなので、信頼もおけます。

しかも、ライブラリの中身はAspects.hAspects.mだけだからすごく軽量です。

使い方は、READMEで十分分かりやすいですが、一応試してみたので、紹介します。(Aspects ver 1.4.1 2014/5/21時点)

使い方

インストールは、cocoapodsに登録されてるので簡単。それか実際のファイル自体は、Aspects.hAspects.mの2つだからそれだけ持ってきてもOK。

例えば、AppDelegate.mapplication: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;
}

実行すると、こんな感じで全てのUIViewControllerviewWillAppear:が呼ばれるところでログが出力される。

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

まぁ、これくらいだと、別に基底クラス作って継承すれば、まぁいいんだけど次のような構成になったとたん破滅します。

f:id:masato47744:20140511024629p:plain

こんな風に、途中でUIViewController以外のUITableViewControllerなんかが入ってくると、BaseViewControllerUIViewControllerのサブクラスなので、ThirdTableViewControllerBaseViewControllerを継承できない! どうしよう、基底クラスがまた増えちゃう。。

こんなときに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>

一つ想定外でしたが、UIWindowrootViewControllerとして設定してるUINavigationControllerUIViewControllerのサブクラスだからログが出力されてますね。

応用編

今回は、クラスに対してインターセプタを入れましたが、インスタンスに対しても設定できます。

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