OC中Method Swizzling的原理及应用

一、前言:

我们知道如果要在没有一个类的实现源码的情况下,要改变某方法的实现,可以通过:
(1)继承该类然后用子类重写的方法,这个比较常用;
(2)也可以为该类添加分类(category),实现一个同名方法覆盖原方法,这个很少用,因为这样也会影响该类的子类调用它的方法。
这里要介绍第三种方法,就是利用Objective-C强大的runtime特性实现的——Method Swizzling。

二、原理:

在OC中调用一个对象的某个方法,其实是给一个对象发送消息,这个消息在运行时才被对象接收到.
对象查找消息的顺序是:缓存(如果之前调用过同样的方法的话) –> 所属的类 –> 所属的类的父类 –> 所属的类的父类的父类 –> … —> NSObject,如果最后没找到,则报错。
那这个只有消息(方法)名,怎么找到相应实现代码呢? 原来该对象的类(或其父类等)都有方法的列表,存放着selectore的名字和方法实现(IMP)的映射关系,而这些IMP正是指向方法实现代码块的指针(类似函数指针).所以对象查找拿到方法名后将其包装成SEL类型的selector,然后拿着这个selector,到通过isa指针找到所属的类(或其父类等)去匹配,找到名字相同的selector则匹配成功,最后执行对应的实现代码.

简单来说,从接收到消息到执行的过程是: 把方法名包装成SEL à 用名字匹配找到对应的selector à 由selector执行对应的方法实现代码块,而Method Swizzling 则是通过改动最后一个过程,调换selector对应的方法实现,从而达到改变某个方法的实现.
如函数 method_exchangeImplementations(Method m1, Method m2) 的原子性(atomic)版的实现过程是:

1
IMP imp1 = method_getImplementation(m1);
2
IMP imp2 = method_getImplementation(m2);
3
method_setImplementation(m1, imp2);
4
method_setImplementation(m2, imp1);

method_getImplementation是获取某方法的IMP, method_setImplementation 是设置某方法的IMP.虽然Method Swizzling是非原子性的,为了解决安全性和并发性问题,只要将其放在类的load方法里实现就可以了,因为一个类的load只会执行一次.

三、应用

那这些方法有什么用处呢?
下面我们来设置一个需求:如果你之前做的某个软件的3按钮通过下面代码设置了3个图片:

1
- (void)setupImage{
2
    self.starImageView.image = [UIImage imageNamed:@"star"];
3
    self.circleImageView.image = [UIImage imageNamed:@"circle"];
4
    self.rectImageView.image = [UIImage imageNamed:@"rect"];
5
}

而后来你希望用户能通过自己的喜好来选择软件的风格,即换肤,所以你想提供红绿蓝三种风格.

一种比较简单粗暴的做法是分别设置3种风格下的三个图片:

1
- (void)viewDidLoad {
2
    [super viewDidLoad];
3
    
4
    [self setImagesWithRedSkin];
5
}
6
- (IBAction)changeSkinsBtnClick:(UIButton *)sender {
7
    switch (sender.tag) {
8
        case 1:
9
            [self setImagesWithRedSkin];
10
            break;
11
        case 2:
12
            [self setImagesWithGreenSkin];
13
            break;
14
        case 3:
15
            [self setImagesWithBlueSkin];
16
            break;
17
        default:
18
            break;
19
    }
20
}
21
22
/* remember choose "Create folder references" when dragging the images into the project */
23
- (void)setImagesWithRedSkin{
24
    self.starImageView.image = [UIImage imageNamed:@"skin/red/star"];
25
    self.circleImageView.image = [UIImage imageNamed:@"skin/red/circle"];
26
    self.rectImageView.image = [UIImage imageNamed:@"skin/red/rect"];
27
}
28
- (void)setImagesWithGreenSkin{
29
    self.starImageView.image = [UIImage imageNamed:@"skin/green/star"];
30
    self.circleImageView.image = [UIImage imageNamed:@"skin/green/circle"];
31
    self.rectImageView.image = [UIImage imageNamed:@"skin/green/rect"];
32
}
33
- (void)setImagesWithBlueSkin{
34
    self.starImageView.image = [UIImage imageNamed:@"skin/blue/star"];
35
    self.circleImageView.image = [UIImage imageNamed:@"skin/blue/circle"];
36
    self.rectImageView.image = [UIImage imageNamed:@"skin/blue/rect"];
37
}

现在看起来还不是太麻烦,但如果要有10种风格,每种风格要用到10张图片,岂不是要修改10*10次?如果更多呢?
这是可以考虑用Method Swizzling,保持原来的设置图片的代码:

1
- (void)setupImage{
2
    self.starImageView.image = [UIImage imageNamed:@"star"];
3
    self.circleImageView.image = [UIImage imageNamed:@"circle"];
4
    self.rectImageView.image = [UIImage imageNamed:@"rect"];
5
}

在用户点击变换风格的时候选择风格,然后在执行设置图片时要判断用户选择了哪种风格,然后根据不同风格设置不同路径的图片

1
- (IBAction)changeSkinSBtnClick:(UIButton *)sender {
2
    [YGImage setMode:YGImageModeSkin];
3
    
4
    // select a style according to the sender
5
    switch (sender.tag) {
6
        case 1:
7
            [YGSkin setStyel:YGSkinStyleRed];
8
            break;
9
        case 2:
10
            [YGSkin setStyel:YGSkinStyleGreen];
11
            break;
12
        case 3:
13
            [YGSkin setStyel:YGSkinStyleBlue];
14
            break;
15
        default:
16
            break;
17
    }
18
    
19
    [self setupImage];
20
}

其中
[YGImage setMode:YGImageModeSkin] 是用来表示进入换肤模式,
[YGSkin setStyel :YGSkinStyleRed] 是表示具体选择了哪种风格的皮肤.

而新的执行方法和调换过程则放在UIImage的分类 UIImage+Swizzle 中:

1
@implementation UIImage (Swizzle)
2
 
3
/* we can exchange custom methods and system methods in class method "+ load" */
4
+ (void)load{
5
    
6
    static dispatch_once_t oneToken;
7
    dispatch_once(&oneToken, ^{
8
        
9
        Method originalMethod = class_getClassMethod([self class], @selector(imageNamed:));
10
        Method newMethod = class_getClassMethod([self class], @selector(newImageNamed:));
11
        
12
        method_exchangeImplementations(originalMethod, newMethod);
13
    });
14
}
15
 
16
/* new method used to be exchanged with the older one */
17
+ (UIImage *)newImageNamed:(NSString *)imageName{
18
    
19
    NSString *fullImageName = imageName;
20
    
21
    if ([YGImage mode] == YGImageModeSkin) {
22
        switch ([YGSkin style]) {
23
            case YGSkinStyleRed:
24
                fullImageName = [NSString stringWithFormat:@"skin/red/%@", imageName];
25
                break;
26
            case YGSkinStyleGreen:
27
                fullImageName = [NSString stringWithFormat:@"skin/green/%@", imageName];
28
                break;
29
            case YGSkinStyleBlue:
30
                fullImageName = [NSString stringWithFormat:@"skin/blue/%@", imageName];
31
                break;
32
            default:
33
                break;
34
        }
35
    }
36
    
37
    return [UIImage newImageNamed:fullImageName];   // actually call - imageNamed:
38
}
39
@end

注意到Method Swizzling应放在某个类的类方法+(void)load中,并在应用程序的一开始就执行, 并用dispatch_once确保代码只被执行一次(貌似也可以不需要).
可能你会好奇:在newImageNamed方法中最后调用自身不会形成无限循环么?其实这时已经完成了方法调换,所以调用是原方法 imageNamed 呢.
这样用Method Swizzling实现换肤功能就完成了.

四、其他例子

(1) github上的 JRSwizzle 把封装了起来,使用比较方便,只需要:

1
NSError *error =nil;
2
[UIImage jr_swizzleClassMethod:@selector(imageNamed:) withClassMethod:@selector(newImageNamed:) error:&error];

即可交换两方法.

其中的实现也是用到了:

1
Method origMethod = class_getInstanceMethod(self, origSel_);
2
Method altMethod = class_getInstanceMethod(self, altSel_);
3
4
class_addMethod(self,
5
					origSel_,
6
					class_getMethodImplementation(self, origSel_),
7
					method_getTypeEncoding(origMethod));
8
class_addMethod(self,
9
					altSel_,
10
					class_getMethodImplementation(self, altSel_),
11
					method_getTypeEncoding(altMethod));
12
13
method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));

(2) github上还有个库 UIViewController-Swizzled ,是用来用新的方法调换了原viewController的viewDidAppear方法,打印当前的viewController的名字和深度,有助于开发者处理项目大量的复杂的viewController,其中交换方法的代码是:

1
static void swizzInstance(Class class, SEL originalSelector, SEL swizzledSelector)
2
{
3
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
4
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
5
    
6
    BOOL didAddMethod =
7
    class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
8
    
9
    if (didAddMethod)
10
    {
11
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
12
    }
13
    else
14
    {
15
        method_exchangeImplementations(originalMethod, swizzledMethod);
16
    }
17
}

源代码见:我的Demo

延伸阅读:

Objective-C的hook方案(一): Method Swizzling

Method Swizzling

Objective-C Runtime 运行时之四:Method Swizzling

Method Swizzling 和 AOP 实践