OC 中可以使用 Runtime
執(zhí)行方法交換敞曹。
什么是方法交換?
方法交換就是兩個(gè)方法的實(shí)現(xiàn)進(jìn)行交換惹盼。
調(diào)用 A 方法的時(shí)候,實(shí)際上調(diào)用的是 B 方法的實(shí)現(xiàn)。
調(diào)用 B 方法的時(shí)候怔鳖,實(shí)際上是調(diào)用 A 方法的實(shí)現(xiàn)。
為什么 OC 的方法可以交換固蛾?
OC 的方法從調(diào)用層面來說结执,是給某個(gè)對(duì)象發(fā)送了一個(gè)字符串信息(SEL)。
Person *p = [Person new];
[p eat]; // 跑步
調(diào)用過程:
- 首先拿到 SEL 艾凯,也就是 @selector(eat);
- 接著拿到接受這個(gè) SEL 的對(duì)象 p
- 最后調(diào)用 objc_msgSend(p,@selector(run));
然后通過這個(gè) SEL 找到方法的實(shí)現(xiàn)献幔。
所以,對(duì)于 OC 的方法來講趾诗,它不是一個(gè)完整的整體蜡感。而是 SEL + IMP。
從上圖可知恃泪,對(duì)于 object_method 里的結(jié)構(gòu)體铸敏,中包含兩個(gè)成員字段:
- SEL
- IMP
所以,也再次說明了悟泵,OC 中的方法調(diào)用和方法實(shí)現(xiàn)并不是一個(gè)完整的整體杈笔。
這也就是 OC 方法交換的基礎(chǔ)。
一個(gè)簡(jiǎn)單的方法交換 demo
所有學(xué)習(xí)新知識(shí)點(diǎn)都是都從簡(jiǎn)單的 demo 開始的糕非。
(一上來拿具體項(xiàng)目說事的人都太天才了蒙具。)
在一個(gè) Person
類中,定義 run
和 eat
兩個(gè)方法朽肥。
@interface Person : NSObject
- (void)run;
- (void)eat;
@end
- (void)run {
NSLog(@"%@",@"跑步");
}
- (void)eat {
NSLog(@"%@",@"吃飯");
}
前提: 當(dāng)執(zhí)行 run
方法時(shí)禁筏,控制臺(tái)會(huì)輸出 跑步
。 當(dāng)執(zhí)行 eat 方法時(shí)衡招,控制臺(tái)會(huì)輸出 吃飯
篱昔。
目標(biāo): 當(dāng)執(zhí)行 run
方法時(shí),控制臺(tái)會(huì)輸出 吃飯
始腾。而當(dāng)執(zhí)行 eat
方法是州刽,控制臺(tái)會(huì)輸出 跑步
。
開始前浪箭,再次說明一下穗椅。
OC 中的方法調(diào)用,本質(zhì)上是消息發(fā)送奶栖。把 SEL 消息匹表,發(fā)給 P 對(duì)象门坷。
讓后通過 SEL 在 P 對(duì)象身上找到包含這個(gè) SEL 的 object_method。
在從這個(gè) object_method 里找到 IMP 方法實(shí)現(xiàn)袍镀。
并執(zhí)行這個(gè)方法默蚌。
所以,OC 中方法的調(diào)用和實(shí)現(xiàn)是分開的苇羡。
我們可以在 +load
方法里绸吸,里用 Runtime 來交換兩個(gè) 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 方法是在當(dāng)前類被加載打運(yùn)行時(shí)就會(huì)執(zhí)行的方法温数。
潛臺(tái)詞就是:
當(dāng)類加載到運(yùn)行時(shí)的時(shí)候绣硝,類的屬性,成員撑刺,方法鹉胖,協(xié)議等都已經(jīng)被加載好了。
然后開始調(diào)用當(dāng)前對(duì)象的 eat & run 方法够傍。
Person *p = [Person new];
[p eat]; // 跑步
[p run]; // 吃飯
控制臺(tái)輸出
2017-11-07 13:09:28.826 CodeFor方法交換[22022:19874567] 跑步
2017-11-07 13:09:28.826 CodeFor方法交換[22022:19874567] 吃飯
結(jié)論符合預(yù)期甫菠。兩個(gè)方法的實(shí)現(xiàn)的確是交換了。
方法交換一些比較使用的場(chǎng)景
在 App 開發(fā)中冕屯,經(jīng)常會(huì)使用到圖片寂诱。
有些承載圖片的 UIImageView 的 size 是確定的。
但提供的素材圖片的和 UIImageView 的 size 不一樣安聘。
于是痰洒,當(dāng)這張圖片顯示在 UIImageView 上時(shí),會(huì)出現(xiàn)拉伸和壓縮的情況浴韭。
可能有的人會(huì)說丘喻,圖片被拉就拉伸,被壓縮就壓縮唄念颈。無所謂泉粉。反正就這一張圖。又不會(huì)動(dòng)榴芳。
在上述那個(gè)場(chǎng)景里嗡靡,的確是這樣。
但是如果這是一個(gè)在 UITableViewCell 里的圖片呢窟感?
并且用戶在快速的滑動(dòng)這些 cell叽躯。
這樣就會(huì)出現(xiàn)一張被頻繁的挪動(dòng)位置 + 拉伸/壓縮。這對(duì)性能是一個(gè)很大的考驗(yàn)肌括。
出現(xiàn)上述性能問題的主要問題是什么点骑?
圖片文件的尺寸和 UIImageView 的尺寸不一致酣难。
解決思路
- 在給 UIImageView 設(shè)置 image 的時(shí)候,調(diào)用的不就是
setImage:
方法嗎黑滴? - 我們交換 UIImageView 的
setImage:
方法為我們自己的rl_setImage:
方法憨募。 - 在我們自己的方法里,生成一張和當(dāng)前 UIImageView size 一樣大的圖片袁辈。
- 然后在調(diào)用 UIImageView 原本的
setImage:
方法菜谣,把我們生成的這張 size 和 UIImageView 一樣大的圖片設(shè)置到 image 上。 - 圖片由于和 UIImageView 的 size 不一致導(dǎo)致的 拉伸/壓縮的問題不就解決了晚缩?
動(dòng)手實(shí)現(xiàn)
第一步:先創(chuàng)建一個(gè) UIImageView 的分類
為什么要?jiǎng)?chuàng)建一個(gè) UIImageView 的分類尾膊?方法交換可以是任意兩個(gè)對(duì)象之間的交換。
我隨便拿個(gè)對(duì)象和 UIImageView 的 setImage 方法交換不就好了荞彼?
因?yàn)楦粤玻?UIImageView 的分類方法里,self關(guān)鍵字表示的是當(dāng)前 UIImageView 的對(duì)象鸣皂。
我們可以通過 self 快速的拿到當(dāng)前 UIImageView 的 size抓谴。
從而快速設(shè)置手動(dòng)生成的 UIImage 的 size。
代碼實(shí)現(xiàn)
@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 是只讀的荆陆,不能修改一個(gè)真實(shí)存儲(chǔ)在的圖片的尺寸滩届。
// 畫一個(gè)大小和當(dāng)前控制器一樣大的 image。而不是使用原來的 image
// 開啟上下文
UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES, 0);
[image drawInRect:self.bounds];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
// 關(guān)閉上下文
UIGraphicsEndImageContext();
[self mySetImage:result];
// NSLog(@"圖片設(shè)置完畢");
}
運(yùn)行效果:
這里創(chuàng)建的符合 UIImageView.size 的尺寸圖片是在主線程丐吓。
經(jīng)過簡(jiǎn)單的測(cè)試發(fā)現(xiàn)生成這樣一張圖片大概耗時(shí):
2017-11-07 14:30:16.836 CodeFor方法交換[22328:19993794] 0.003812
大概 3/1000 秒。
如果對(duì)性能要求比較高的話趟据,可以把這個(gè)生成圖片數(shù)據(jù)的任務(wù)放到子線程執(zhí)行券犁。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimeInterval start = CACurrentMediaTime();
UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES, 0);
[image drawInRect:self.bounds];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
// 關(guān)閉上下文
UIGraphicsEndImageContext();
NSLog(@"%f",CACurrentMediaTime() - start);
// 回到主線程,設(shè)置圖像
dispatch_async(dispatch_get_main_queue(), ^{
[self mySetImage:result];
});
});
這種做法很適合于 UITableViewCell 中包含圖片情況的優(yōu)化汹碱。
我們可以通過交換 UIImageView 的 setImage: 方法來提高 UITableViewCell 的性能粘衬。