Method Swizzling

本篇由两篇博客组成,请相互参照,以吸取精华:

1. Method Swizzling 和 AOP 实践<http://tech.glowing.com/cn/method-swizzling-aop/>

2. Objective-C的hook方案(一):  Method Swizzling 《http://blog.youkuaiyun.com/yiyaaixuexi/article/details/9374411

 

Method Swizzling 和 AOP 实践<http://tech.glowing.com/cn/method-swizzling-aop/>

利用 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 的代码都很相似,通过继承或类别重写相关方法是可以把它从主要逻辑中剥离出来。但同时也带来新的问题:

  1. 你需要继承 UIViewController, UITableViewController, UICollectionViewController 所有这些 ViewController ,或者给他们添加类别;
  2. 每个 ViewController 里的 ButtonClick 方法命名不可能都一样;
  3. 你不能控制别人如何去实例化你的子类;
  4. 对于类别,你没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它。
  5. 如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用。

Method Swizzling

Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。

上一篇文章 有讲到每个类里都有一个 Dispatch Table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在 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:));
}

一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +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 这些黑色技巧,只提供两个简单的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 的方法里。更好的方式,你提供一个 Logging 的配置文件作为唯一处理事件记录的地方:

@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

如果你有什么问题和想法,欢迎留言或者发邮件给我 peng@glowing.com 进行讨论。

Reference

 

 

 

 

Objective-C的hook方案(一):  Method Swizzling 《http://blog.youkuaiyun.com/yiyaaixuexi/article/details/9374411


在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法吗?在Objective-C编程中,如何实现hook呢?标题有点大,计划分几篇来总结。

本文主要介绍针对selector的hook,主角被标题剧透了———— Method Swizzling 。



Method Swizzling 原理


在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。

每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。




 


我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,

我们可以利用 class_replaceMethod 来修改类,

我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
……

归根结底,都是偷换了selector的IMP,如下图所示:








 

Method Swizzling 实践



举个例子好了,我想钩一下NSArray的lastObject 方法,只需两个步骤。

第一步:给NSArray加一个我自己的lastObject

  1. #import "NSArray+Swizzle.h"   
  2.   
  3.   
  4. @implementation NSArray (Swizzle)  
  5.   
  6.   
  7. - (id)myLastObject  
  8. {  
  9.     id ret = [self myLastObject];  
  10.     NSLog(@"**********  myLastObject *********** ");  
  11.     return ret;  
  12. }  
  13. @end  
#import "NSArray+Swizzle.h"


@implementation NSArray (Swizzle)


- (id)myLastObject
{
    id ret = [self myLastObject];
    NSLog(@"**********  myLastObject *********** ");
    return ret;
}
@end


乍一看,这不递归了么?别忘记这是我们准备调换IMP的selector,[self myLastObject] 将会执行真的 [self lastObject] 。



第二步:调换IMP

  1. #import <objc/runtime.h>   
  2. #import "NSArray+Swizzle.h"   
  3.   
  4.   
  5. int main(int argc, char *argv[])  
  6. {  
  7.     @autoreleasepool {  
  8.           
  9.         Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));  
  10.         Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));  
  11.         method_exchangeImplementations(ori_Method, my_Method);  
  12.           
  13.         NSArray *array = @[@"0",@"1",@"2",@"3"];  
  14.         NSString *string = [array lastObject];  
  15.         NSLog(@"TEST RESULT : %@",string);  
  16.           
  17.         return 0;  
  18.     }  
  19. }  
#import <objc/runtime.h>
#import "NSArray+Swizzle.h"


int main(int argc, char *argv[])
{
    @autoreleasepool {
        
        Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));
        Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));
        method_exchangeImplementations(ori_Method, my_Method);
        
        NSArray *array = @[@"0",@"1",@"2",@"3"];
        NSString *string = [array lastObject];
        NSLog(@"TEST RESULT : %@",string);
        
        return 0;
    }
}



控制台输出Log:

  1. 2013-07-18 16:26:12.585 Hook[1740:c07] **********  myLastObject ***********   
  2. 2013-07-18 16:26:12.589 Hook[1740:c07] TEST RESULT : 3  
2013-07-18 16:26:12.585 Hook[1740:c07] **********  myLastObject *********** 
2013-07-18 16:26:12.589 Hook[1740:c07] TEST RESULT : 3



结果很让人欣喜,是不是忍不住想给UIWebView的loadRequest: 加 TODO 了呢? 




Method Swizzling 的封装



之前在github上找到的RNSwizzle,推荐给大家,可以搜一下。

  1. //   
  2. //  RNSwizzle.m   
  3. //  MethodSwizzle   
  4.   
  5.   
  6. #import "RNSwizzle.h"   
  7. #import <objc/runtime.h>   
  8. @implementation NSObject (RNSwizzle)  
  9.   
  10.   
  11. + (IMP)swizzleSelector:(SEL)origSelector   
  12.                withIMP:(IMP)newIMP {  
  13.   Class class = [self class];  
  14.   Method origMethod = class_getInstanceMethod(class,  
  15.                                               origSelector);  
  16.   IMP origIMP = method_getImplementation(origMethod);  
  17.     
  18.   if(!class_addMethod(self, origSelector, newIMP,  
  19.                       method_getTypeEncoding(origMethod)))  
  20.   {  
  21.     method_setImplementation(origMethod, newIMP);  
  22.   }  
  23.     
  24.   return origIMP;  
  25. }  
  26. @end  
//
//  RNSwizzle.m
//  MethodSwizzle


#import "RNSwizzle.h"
#import <objc/runtime.h>
@implementation NSObject (RNSwizzle)


+ (IMP)swizzleSelector:(SEL)origSelector 
               withIMP:(IMP)newIMP {
  Class class = [self class];
  Method origMethod = class_getInstanceMethod(class,
                                              origSelector);
  IMP origIMP = method_getImplementation(origMethod);
  
  if(!class_addMethod(self, origSelector, newIMP,
                      method_getTypeEncoding(origMethod)))
  {
    method_setImplementation(origMethod, newIMP);
  }
  
  return origIMP;
}
@end





 

Method Swizzling 危险不危险



针对这个问题,我在stackoverflow上看到了满意的答案,这里翻译一下,总结记录在本文中,以示分享:


使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。
Method swizzling 可以帮助我们写出更好的,更高效的,易维护的代码。但是如果滥用它,也将会导致难以排查的bug。 


背景


好比设计模式,如果我们摸清了一个模式的门道,使用该模式与否我们自己心里有数。单例模式就是一个很好的例子,它饱受争议但是许多人依旧使用它。Method Swizzling也是一样,一旦你真正理解它的优势和弊端,使用它与否你应该就有你自己的观点。


讨论


这里是一些 Method Swizzling的陷阱:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug



我将逐一分析这些点,增进对Method Swizzling的理解的同时,并搞懂如何应对。


Method swizzling is not atomic


我所见过的使用method swizzling实现的方法在并发使用时基本都是安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在 +(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。


Changes behavior of un-owned code


这是swizzling的一个问题。我们的目标是改变某些代码。swizzling方法是一件灰常灰常重要的事,当你不只是对一个NSButton类的实例进行了修改,而是程序中所有的NSButton实例。因此在swizzling时应该多加小心,但也不用总是去刻意避免。

想象一下,如果你重写了一个类的方法,而且没有调用父类的这个方法,这可能会引起问题。大多数情况下,父类方法期望会被调用(至少文档是这样说的)。如果你在swizzling实现中也这样做了,这会避免大部分问题。还是调用原始实现吧,如若不然,你会费很大力气去考虑代码的安全问题。



Possible naming conflicts


命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名:
 

  1. @interface NSView : NSObject  
  2. - (void)setFrame:(NSRect)frame;  
  3. @end  
  4.   
  5.   
  6. @implementation NSView (MyViewAdditions)  
  7.   
  8.   
  9. - (void)my_setFrame:(NSRect)frame {  
  10.     // do custom work   
  11.     [self my_setFrame:frame];  
  12. }  
  13.   
  14.   
  15. + (void)load {  
  16.     [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];  
  17. }  
  18.   
  19.   
  20. @end  
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end


@implementation NSView (MyViewAdditions)


- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
}


+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}


@end



 

这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢?

这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:

  1. @implementation NSView (MyViewAdditions)  
  2.   
  3.   
  4. static void MySetFrame(id self, SEL _cmd, NSRect frame);  
  5. static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);  
  6.   
  7.   
  8. static void MySetFrame(id self, SEL _cmd, NSRect frame) {  
  9.     // do custom work   
  10.     SetFrameIMP(self, _cmd, frame);  
  11. }  
  12.   
  13.   
  14. + (void)load {  
  15.     [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];  
  16. }  
  17.   
  18.   
  19. @end  
@implementation NSView (MyViewAdditions)


static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);


static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}


+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}


@end


看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。 


最后给出一个较完美的swizzle方法的定义:

  1. typedef IMP *IMPPointer;  
  2.   
  3.   
  4. BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {  
  5.     IMP imp = NULL;  
  6.     Method method = class_getInstanceMethod(class, original);  
  7.     if (method) {  
  8.         const char *type = method_getTypeEncoding(method);  
  9.         imp = class_replaceMethod(class, original, replacement, type);  
  10.         if (!imp) {  
  11.             imp = method_getImplementation(method);  
  12.         }  
  13.     }  
  14.     if (imp && store) { *store = imp; }  
  15.     return (imp != NULL);  
  16. }  
  17.   
  18.   
  19. @implementation NSObject (FRRuntimeAdditions)  
  20. + (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {  
  21.     return class_swizzleMethodAndStore(self, original, replacement, store);  
  22. }  
  23. @end  
typedef IMP *IMPPointer;


BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}


@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end




 

Swizzling changes the method's arguments



 

我认为这是最大的问题。想正常调用method swizzling 将会是个问题。

  1. [self my_setFrame:frame];  
[self my_setFrame:frame];


直接调用my_setFrame: , runtime做的是

  1. objc_msgSend(self, @selector(my_setFrame:), frame);  
objc_msgSend(self, @selector(my_setFrame:), frame);


runtime去寻找my_setFrame:的方法实现, _cmd参数为 my_setFrame: ,但是事实上runtime找到的方法实现是原始的 setFrame: 的。

一个简单的解决办法:使用上面介绍的swizzling定义。




The order of swizzles matters



多个swizzle方法的执行顺序也需要注意。假设 setFrame: 只定义在NSView中,想像一下按照下面的顺序执行:

  1. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];  
  2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];  
  3. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];  
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];


What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).

When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.

But what if the order were:

  1. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];  
  2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];  
  3. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];  
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];


Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?

Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!


这段贴了原文,硬翻译太拗口……总结一下就是:多个有继承关系的类的对象swizzle时,从子类对象开始 。 如果先swizzle父类对象,那么后面子类对象swizzle时就无法拿到真正的原始方法实现了。 


(感谢评论中 qq373127202 的提醒,在此更正一下,十分感谢

多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。



Difficult to understand (looks recursive)


(新方法的实现)看起来像递归,但是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂.
这个问题是已完全解决的了!




Difficult to debug


debug时打出的backtrace,其中掺杂着被swizzle的方法名,一团糟啊!上面介绍的swizzle方案,使backtrace中打印出的方法名还是很清晰的。但仍然很难去debug,因为很难记住swizzling影响过什么。给你的代码写好文档(即使只有你一个人会看到)。养成一个好习惯,不会比调试多线程问题还难的。




结论


如果使用恰当,Method swizzling 还是很安全的.一个简单安全的方法是,仅在load中swizzle。 和许多其他东西一样,它也是有危险性的,但理解它了也就可以正确恰当的使用它了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值