object—c runtime経典説明シリーズ2


前編ではObjective-C Messagingについて紹介しました.Objective-CのRuntime特性を利用して、言語を拡張して、プロジェクト開発におけるいくつかの設計と技術の問題を解決することができます.今回は、Objective-C Runtimeを利用したブラックテクニックを探ってみましょう.これらのテクニックの中で最も議論されているのは、Method Swizzlingかもしれません.
テクニックを紹介するには、具体的なニーズを提出し、他の解決方法と比較するのが一番です.
まず、Appのユーザーの行動を追跡し、分析する必要があります.簡単に言えば、ユーザがあるViewを見たり、あるButtonをクリックしたりしたときに、このイベントをメモすることである.
手動で追加
最も直接的で乱暴な方法は、各viewDidAppearに記録イベントのコードを追加することである.
@implementation MyViewController () - (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated]; // Custom code  // Logging [Logging logWithEventName:@“my view did appear”];
}


- (void)myButtonClicked:(id)sender
{ // Custom code  // Logging [Logging logWithEventName:@“my button clicked”];
} 

この方法の欠点も明らかです.コードの清潔さを破壊します.Loggingのコード自体がViewControllerの主な論理に属していないためです.プロジェクトが拡大し、コード量が増加するにつれて、あなたのViewControllerにはLoggingのコードがあちこちに散らばっています.この場合,イベントレコードのコードを見つけることが困難になり,イベントレコードのコードを追加することも忘れがちになる.
継承やカテゴリで、書き換え方法にイベントレコードのコードを追加することを考えるかもしれません.コードは長いこのようにすることができます.
@implementation UIViewController () - (void)myViewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated]; // Custom code  // Logging [Logging logWithEventName:NSStringFromClass([self class])];
}


- (void)myButtonClicked:(id)sender
{ // Custom code  // Logging NSString *name = [NSString stringWithFormat:@“my button in %@ is clicked”, NSStringFromClass([self class])];
    [Logging logWithEventName:name];
} 
Loggingのコードは似ていて、継承やカテゴリ書き換えによって主な論理から剥離することができます.しかし、新しい問題ももたらします.UIViewController,UITableViewController,UICollectionViewControllerのすべてのビューコントローラを継承するか、カテゴリを追加する必要があります. 
各ViewControllerのButtonClickメソッドの名前は同じではありません. 
他の人がどのようにあなたのサブクラスをインスタンス化するかをコントロールすることはできません. 
カテゴリについては、元のメソッド実装を呼び出すことはできません.多くの場合、私たちは完全に置換するのではなく、コードを追加するために方法を書き直します. 
両方のカテゴリで同じメソッドが実装されている場合、実行時にどのカテゴリのメソッドが呼び出されるかは保証されません.
Method Swizzling
Method SwizzlingはRuntime特性を利用して一つの方法の実現を別の方法の実現と置き換える.
前の記事では、各クラスに1つのDispatch Tableがあり、メソッドの名前(SEL)とメソッドの実装(IMP、C関数を指すポインタ)を1つずつ対応していると述べています.Swizzleの1つの方法は、プログラムの実行時にDispatch Tableで変更し、このメソッドの名前(SEL)を別のIMPに対応させることです.
まずカテゴリを定義し、Swizzledのメソッドを追加します.
@implementation UIViewController (Logging) - (void)swizzled_viewDidAppear:(BOOL)animated
{ // call original implementation [self swizzled_viewDidAppear:animated]; // Logging [Logging logWithEventName:NSStringFromClass([self class])];
} 

コードは少し変に見えるかもしれませんが、再帰的ではないでしょうか.もちろん再帰ではありません.runtimeのとき、関数実装が交換されたからです.呼び出しviewDidAppear:は、実装swizzled_viewDidAppear:を呼び出しますが、swizzled_viewDidAppear:で呼び出しswizzled_viewDidAppear:は、実際には元のviewDidAppear:を呼び出します.
次にswizzleを実装する方法は、次のとおりです.
@implementation UIViewController (Logging) void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) { // the method might not exist in the class, but in its superclass Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // class_addMethod will fail if original method already exists BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // the method doesn’t exist and we just added one if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
} 

ここで唯一説明する必要があるのはclass_addMethodです.元のselectorを追加しようとするのは、このクラスがoriginalSelectorを実装していないが、その親が実装されている場合、class_getInstanceMethodが親を返す方法であるため、保護を行うためです.このようにmethod_exchangeImplementationsを置き換えるのは親の方法で、もちろんあなたが望んでいるものではありません.だから私達は先にorginalSelectorを追加することを試みて、もしすでに存在するならば、更にmethod_exchangeImplementationsで元の方法の実現と新しい方法の実現を交換します.
最後に、プログラムが起動されたときにswizzleMethodメソッドが呼び出されることを確認するだけです.たとえば、以前のUIViewControllerのLoggingカテゴリに+load:メソッドを追加し、+load:viewDidAppearを置き換えることができます.
@implementation UIViewController (Logging) + (void)load
{
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
} 

一般的に、カテゴリ内のメソッドは、メインクラスで同じ名前のメソッドを書き換えます.同じ名前のメソッドが2つのカテゴリで実装されている場合、1つのメソッドだけが呼び出されます.ただし、+load:は例外であり、クラスがメモリに読み込まれるとruntimeはクラスおよびその各カテゴリに+load:メッセージを送信します.
実際には、ここでは、置換ではなく新しいIMPで元のIMPを直接置換することも簡略化できます.グローバルな関数ポインタが元のIMPを指すだけでよい.
void (gOriginalViewDidAppear)(id, SEL, BOOL); void newViewDidAppear(UIViewController *self, SEL _cmd, BOOL animated)  
{ // call original implementation gOriginalViewDidAppear(self, _cmd, animated); // Logging [Logging logWithEventName:NSStringFromClass([self class])];
}

+ (void)load
{
    Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
    gOriginalViewDidAppear = (void *)method_getImplementation(originalMethod); if(!class_addMethod(self, @selector(viewDidAppear:), (IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {
        method_setImplementation(originalMethod, (IMP) newViewDidAppear);
    }
} 

Method Swizzlingにより,論理コードをイベントレコードを処理するコードとデカップリングすることに成功した.もちろんLoggingのほかにも、AuthenticationやCachingのような似たような事務がたくさんあります.これらの事務は些細で、主要な業務論理とは関係なく、多くの場所にあり、単独のモジュールを抽象化することは難しい.このようなプログラム設計の問題は、業界も彼らに名前を与えた-Cross Cutting Concerns.
上記の例のようにMethod Swizzlingで指定された方法に動的にコードを追加し、Cross Cutting Concernsを解決するプログラミング方式をAspect Oriented Programmingと呼ぶ.
Aspect Oriented Programming(フェースプログラミング向け)
WikipediaではAOPについてこう紹介しています.
An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).
Objective-Cの世界では、Runtimeプロパティを使用して指定した方法にカスタムコードを追加することを意味します.AOPを実現する方法はたくさんありますが、Method Swizzlingはその一つです.幸いなことに、Runtimeを理解する必要がなく、AOPを直接使用することができるサードパーティ製のライブラリがあります.
Aspectsは良いAOPライブラリで、Runtime、Method Swizzlingなどの黒いテクニックをカプセル化し、2つの簡単なAPIしか提供していません.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; 

Aspectsが提供するAPIを使用すると、私たちの前の例はこのように進化します.
@implementation UIViewController (Logging) + (void)load
{
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) { NSString *className = NSStringFromClass([[aspectInfo instance] class]);
        [Logging logWithEventName:className];
                               } error:NULL];
} 

IBActionのような任意の興味のある方法にカスタムコードを追加することができます.より良い方法では、ロギングのプロファイルを唯一のイベントレコードを処理する場所として提供します.
@implementation AppDelegate (Logging) + (void)setupLogging
{ NSDictionary *config = @{ @"MainViewController": @{
            GLLoggingPageImpression: @"page imp - main page",
            GLLoggingTrackedEvents: @[
                @{
                    GLLoggingEventName: @"button one clicked",
                    GLLoggingEventSelectorName: @"buttonOneClicked:",
                    GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                        [Logging logWithEventName:@"button one clicked"];
                    },
                },
                @{
                    GLLoggingEventName: @"button two clicked",
                    GLLoggingEventSelectorName: @"buttonTwoClicked:",
                    GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                        [Logging logWithEventName:@"button two clicked"];
                    },
                },
           ],
        }, @"DetailViewController": @{
            GLLoggingPageImpression: @"page imp - detail page",
        }
    };

    [AppDelegate setupWithConfiguration:config];
}

+ (void)setupWithConfiguration:(NSDictionary *)configs
{ // Hook Page Impression [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) { NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                    [Logging logWithEventName:className];
                               } error:NULL]; // Hook Events for (NSString *className in configs) {
        Class clazz = NSClassFromString(className); NSDictionary *config = configs[className]; if (config[GLLoggingTrackedEvents]) { for (NSDictionary *event in config[GLLoggingTrackedEvents]) {
                SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);
                AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];

                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionAfter
                                usingBlock:^(id<AspectInfo> aspectInfo) {
                                    block(aspectInfo);
                                } error:NULL];

            }
        }
    }
} 

次に、-application:didFinishLaunchingWithOptions:setupLoggingを呼び出します.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [self setupLogging]; return YES;
} 

最後の言葉
Objective-C Runtime特性とAspect Oriented Programmingを用いて,些細なトランザクションの論理を主論理から分離し,個別のモジュールとすることができる.オブジェクト向けプログラミングモードの補完です.Loggingは古典的な応用で、ここでレンガを投げて玉を引いて、想像力を発揮して、他の面白い応用をすることができます.
Aspectsを用いた完全な例はここから得られる:AspectsDemo.
何か質問やアイデアがあれば、メッセージやメールを送ってください[email protected]討論を進める.
Reference
method-swizzling
method replacement for fun and profit
Aspects