一、前言:
我们知道如果要在没有一个类的实现源码的情况下,要改变某方法的实现,可以通过:
(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