OC 中可以使用 Runtime
执行方法交换。
什么是方法交换?
方法交换就是两个方法的实现进行交换。
调用 A 方法的时候,实际上调用的是 B 方法的实现。
调用 B 方法的时候,实际上是调用 A 方法的实现。
为什么 OC 的方法可以交换?
OC 的方法从调用层面来说,是给某个对象发送了一个字符串信息(SEL)。
Person *p = [Person new];
[p eat]; // 跑步
调用过程:
- 首先拿到 SEL ,也就是 @selector(eat);
- 接着拿到接受这个 SEL 的对象 p
- 最后调用 objc_msgSend(p,@selector(run));
然后通过这个 SEL 找到方法的实现。
所以,对于 OC 的方法来讲,它不是一个完整的整体。而是 SEL + IMP。
从上图可知,对于 object_method 里的结构体,中包含两个成员字段:
- SEL
- IMP
所以,也再次说明了,OC 中的方法调用和方法实现并不是一个完整的整体。
这也就是 OC 方法交换的基础。
一个简单的方法交换 demo
所有学习新知识点都是都从简单的 demo 开始的。
(一上来拿具体项目说事的人都太天才了。)
在一个 Person
类中,定义 run
和 eat
两个方法。
@interface Person : NSObject
- (void)run;
- (void)eat;
@end
- (void)run {
NSLog(@"%@",@"跑步");
}
- (void)eat {
NSLog(@"%@",@"吃饭");
}
前提: 当执行 run
方法时,控制台会输出 跑步
。 当执行 eat 方法时,控制台会输出 吃饭
。
目标: 当执行 run
方法时,控制台会输出 吃饭
。而当执行 eat
方法是,控制台会输出 跑步
。
开始前,再次说明一下。
OC 中的方法调用,本质上是消息发送。把 SEL 消息,发给 P 对象。
让后通过 SEL 在 P 对象身上找到包含这个 SEL 的 object_method。
在从这个 object_method 里找到 IMP 方法实现。
并执行这个方法。
所以,OC 中方法的调用和实现是分开的。
我们可以在 +load
方法里,里用 Runtime 来交换两个 SEL 的 IMP。
+ (void)load {
Method method1 = class_getInstanceMethod([self class], @selector(run));
Method method2 = class_getInstanceMethod([self class], @selector(eat));
// 方法交换
method_exchangeImplementations(method1, method2);
}
这里为什么用 load 方法?有什么说道吗?
load 方法是在当前类被加载打运行时就会执行的方法。
潜台词就是:
当类加载到运行时的时候,类的属性,成员,方法,协议等都已经被加载好了。
然后开始调用当前对象的 eat & run 方法。
Person *p = [Person new];
[p eat]; // 跑步
[p run]; // 吃饭
控制台输出
2017-11-07 13:09:28.826 CodeFor方法交换[22022:19874567] 跑步
2017-11-07 13:09:28.826 CodeFor方法交换[22022:19874567] 吃饭
结论符合预期。两个方法的实现的确是交换了。
方法交换一些比较使用的场景
在 App 开发中,经常会使用到图片。
有些承载图片的 UIImageView 的 size 是确定的。
但提供的素材图片的和 UIImageView 的 size 不一样。
于是,当这张图片显示在 UIImageView 上时,会出现拉伸和压缩的情况。
可能有的人会说,图片被拉就拉伸,被压缩就压缩呗。无所谓。反正就这一张图。又不会动。
在上述那个场景里,的确是这样。
但是如果这是一个在 UITableViewCell 里的图片呢?
并且用户在快速的滑动这些 cell。
这样就会出现一张被频繁的挪动位置 + 拉伸/压缩。这对性能是一个很大的考验。
出现上述性能问题的主要问题是什么?
图片文件的尺寸和 UIImageView 的尺寸不一致。
解决思路
- 在给 UIImageView 设置 image 的时候,调用的不就是
setImage:
方法吗? - 我们交换 UIImageView 的
setImage:
方法为我们自己的rl_setImage:
方法。 - 在我们自己的方法里,生成一张和当前 UIImageView size 一样大的图片。
- 然后在调用 UIImageView 原本的
setImage:
方法,把我们生成的这张 size 和 UIImageView 一样大的图片设置到 image 上。 - 图片由于和 UIImageView 的 size 不一致导致的 拉伸/压缩的问题不就解决了?
动手实现
第一步:先创建一个 UIImageView 的分类
为什么要创建一个 UIImageView 的分类?方法交换可以是任意两个对象之间的交换。
我随便拿个对象和 UIImageView 的 setImage 方法交换不就好了?
因为,在 UIImageView 的分类方法里,self关键字表示的是当前 UIImageView 的对象。
我们可以通过 self 快速的拿到当前 UIImageView 的 size。
从而快速设置手动生成的 UIImage 的 size。
代码实现
@implementation UIImageView (runtime)
+ (void)load {
Method setImage = class_getInstanceMethod([self class], @selector(setImage:));
Method mySetImage = class_getInstanceMethod([self class], @selector(mySetImage:));
method_exchangeImplementations(setImage, mySetImage);
}
- (void)mySetImage:(UIImage *)image {
// 由于,image 来自一张真是存在的图片文件。所以,他的 size 是只读的,不能修改一个真实存储在的图片的尺寸。
// 画一个大小和当前控制器一样大的 image。而不是使用原来的 image
// 开启上下文
UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES, 0);
[image drawInRect:self.bounds];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
// 关闭上下文
UIGraphicsEndImageContext();
[self mySetImage:result];
// NSLog(@"图片设置完毕");
}
运行效果:
这里创建的符合 UIImageView.size 的尺寸图片是在主线程。
经过简单的测试发现生成这样一张图片大概耗时:
2017-11-07 14:30:16.836 CodeFor方法交换[22328:19993794] 0.003812
大概 3/1000 秒。
如果对性能要求比较高的话,可以把这个生成图片数据的任务放到子线程执行。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimeInterval start = CACurrentMediaTime();
UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES, 0);
[image drawInRect:self.bounds];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
// 关闭上下文
UIGraphicsEndImageContext();
NSLog(@"%f",CACurrentMediaTime() - start);
// 回到主线程,设置图像
dispatch_async(dispatch_get_main_queue(), ^{
[self mySetImage:result];
});
});
这种做法很适合于 UITableViewCell 中包含图片情况的优化。
我们可以通过交换 UIImageView 的 setImage: 方法来提高 UITableViewCell 的性能。