Method Swizzling

Objective-C 的運行時中最具爭議的黑魔法:method swizzling境析。

Method swizzling

Method swizzling 用于改變一個已經(jīng)存在的 selector 的實現(xiàn)囚枪。這項技術(shù)使得在運行時通過改變 selector 在類的消息分發(fā)列表中的映射從而改變方法的掉用成為可能。例如:我們想要在一款 iOS app 中追蹤每一個視圖控制器被用戶呈現(xiàn)了幾次: 這可以通過在每個視圖控制器的 viewDidAppear: 方法中添加追蹤代碼來實現(xiàn)劳淆,但這樣會大量重復(fù)的樣板代碼眶拉。繼承是另一種可行的方式,但是這要求所有被繼承的視圖控制器如 UIViewController, UITableViewController, UINavigationController 都在 viewDidAppear:實現(xiàn)追蹤代碼憔儿,這同樣會造成很多重復(fù)代碼。 幸運的是放可,這里有另外一種可行的方式:從 category 實現(xiàn) method swizzling 谒臼。下面是實現(xiàn)方式:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

計算機科學(xué)里,交換指針指向用來交換基于名字或者位置的指針引用耀里。如果你對 Objective-C 這方面的特性不是很了解的話蜈缤,這是很值得推薦使用的一個特性,因為 method swizzling 可以通過交換 selector 來改變函數(shù)指針的引用冯挎。

現(xiàn)在底哥,UIViewController 或其子類的實例對象在調(diào)用 viewWillAppear: 的時候會有 log 的輸出。

在視圖控制器的生命周期房官,響應(yīng)事件趾徽,繪制視圖或者 Foundation 框架的網(wǎng)絡(luò)棧等方法中插入代碼都是 method swizzling 能夠為開發(fā)帶來很好作用的例子。有很多的場景選擇method swizzling 會是很合適的解決方式翰守,這顯然也會讓 Objective-C 開發(fā)者的技術(shù)變得越來越成熟孵奶。

到此我們已經(jīng)知道為什么,應(yīng)該在哪些地方使用 method swizzling蜡峰,下面介紹如何使用 method swizzling:

+load vs +initialize

swizzling應(yīng)該只在+load中完成了袁。

在 Objective-C 的運行時中朗恳,每個類有兩個方法都會自動調(diào)用。+load 是在一個類被初始裝載時調(diào)用载绿,+initialize 是在應(yīng)用第一次調(diào)用該類的類方法或?qū)嵗椒ㄇ罢{(diào)用的粥诫。兩個方法都是可選的,并且只有在方法被實現(xiàn)的情況下才會被調(diào)用崭庸。

dispatch_once

swizzling 應(yīng)該只在 dispatch_once 中完成

由于 swizzling 改變了全局的狀態(tài)怀浆,所以我們需要確保每個預(yù)防措施在運行時都是可用的。原子操作就是這樣一個用于確保代碼只會被執(zhí)行一次的預(yù)防措施冀自,就算是在不同的線程中也能確保代碼只執(zhí)行一次揉稚。Grand Central Dispatch 的 dispatch_once 滿足了所需要的需求,并且應(yīng)該被當(dāng)做使用 swizzling 的初始化單例方法的標(biāo)準(zhǔn)熬粗。

Selectors, Methods, & Implementations

在 Objective-C 的運行時中搀玖,selectors, methods, implementations 指代了不同概念,然而我們通常會說在消息發(fā)送過程中驻呐,這三個概念是可以相互轉(zhuǎn)換的灌诅。 下面是蘋果 Objective-C Runtime Reference中的描述:

  • Selector(typedef struct objc_selector *SEL):在運行時 Selectors 用來代表一個方法的名字。Selector 是一個在運行時被注冊(或映射)的C類型字符串含末。Selector由編譯器產(chǎn)生并且在當(dāng)類被加載進內(nèi)存時由運行時自動進行名字和實現(xiàn)的映射猜拾。
  • Method(typedef struct objc_method *Method):方法是一個不透明的用來代表一個方法的定義的類型。
  • Implementation(typedef id (*IMP)(id, SEL,...)):這個數(shù)據(jù)類型指向一個方法的實現(xiàn)的最開始的地方佣盒。該方法為當(dāng)前CPU架構(gòu)使用標(biāo)準(zhǔn)的C方法調(diào)用來實現(xiàn)挎袜。該方法的第一個參數(shù)指向調(diào)用方法的自身(即內(nèi)存中類的實例對象,若是調(diào)用類方法肥惭,該指針則是指向元類對象metaclass)盯仪。第二個參數(shù)是這個方法的名字selector,該方法的真正參數(shù)緊隨其后蜜葱。

理解 selector, method, implementation 這三個概念之間關(guān)系的最好方式是:在運行時全景,類(Class)維護了一個消息分發(fā)列表來解決消息的正確發(fā)送。每一個消息列表的入口是一個方法(Method)牵囤,這個方法映射了一對鍵值對爸黄,其中鍵值是這個方法的名字 selector(SEL),值是指向這個方法實現(xiàn)的函數(shù)指針 implementation(IMP)揭鳞。 Method swizzling 修改了類的消息分發(fā)列表使得已經(jīng)存在的 selector 映射了另一個實現(xiàn) implementation炕贵,同時重命名了原生方法的實現(xiàn)為一個新的 selector。

調(diào)用 _cmd

下面代碼在正常情況下會出現(xiàn)循環(huán):

- (void)xxx_viewWillAppear:(BOOL)animated {
   [self xxx_viewWillAppear:animated]; 
   NSLog(@"viewWillAppear: %@", NSStringFromClass([self class])); 
} 

然而在交換了方法實現(xiàn)后就不會出現(xiàn)循環(huán)了野崇。好的程序員應(yīng)該對這里出現(xiàn)的方法的遞歸調(diào)用有所警覺鲁驶,這里我們應(yīng)該理清在 method swizzling 后方法的實現(xiàn)究竟變成了什么。在交換了方法的實現(xiàn)后舞骆,xxx_viewWillAppear:方法的實現(xiàn)已經(jīng)被替換為了 UIViewController -viewWillAppear:的原生實現(xiàn)钥弯,所以這里并不是在遞歸調(diào)用径荔。由于 xxx_viewWillAppear: 這個方法的實現(xiàn)已經(jīng)被替換為了 viewWillAppear: 的實現(xiàn),所以脆霎,當(dāng)我們在這個方法中再調(diào)用 viewWillAppear: 時便會造成遞歸循環(huán)总处。

記住給需要轉(zhuǎn)換的所有方法加個前綴以區(qū)別原生方法。

思考

很多人認(rèn)為交換方法實現(xiàn)會帶來無法預(yù)料的結(jié)果睛蛛。然而采取了以下預(yù)防措施后, method swizzling 會變得很可靠:

  • 在交換方法實現(xiàn)后記得要調(diào)用原生方法的實現(xiàn)(除非你非常確定可以不用調(diào)用原生方法的實現(xiàn)):APIs 提供了輸入輸出的規(guī)則雕沿,而在輸入輸出中間的方法實現(xiàn)就是一個看不見的黑盒苞七。交換了方法實現(xiàn)并且一些回調(diào)方法不會調(diào)用原生方法的實現(xiàn)這可能會造成底層實現(xiàn)的崩潰橡庞。
  • 避免沖突:為分類的方法加前綴戏仓,一定要確保調(diào)用了原生方法的所有地方不會因為你交換了方法的實現(xiàn)而出現(xiàn)意想不到的結(jié)果。
  • 理解實現(xiàn)原理:只是簡單的拷貝粘貼交換方法實現(xiàn)的代碼而不去理解實現(xiàn)原理不僅會讓 App 很脆弱客冈,并且浪費了學(xué)習(xí) Objective-C 運行時的機會旭从。閱讀 Objective-C Runtime Reference 能夠讓你更好理解實現(xiàn)原理。
  • 持續(xù)的預(yù)防:不管你對你理解 swlzzling 框架场仲,UIKit 或者其他內(nèi)嵌框架有多自信和悦,一定要記住所有東西在下一個發(fā)行版本都可能變得不再好使。做好準(zhǔn)備渠缕,在使用這個黑魔法中走得更遠鸽素,不要讓程序反而出現(xiàn)不可思議的行為。

文章轉(zhuǎn)自: Mattt Thompson撰寫 亦鳞, Daniel Hu翻譯

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末馍忽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子燕差,更是在濱河造成了極大的恐慌遭笋,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谁不,死亡現(xiàn)場離奇詭異,居然都是意外死亡徽诲,警方通過查閱死者的電腦和手機刹帕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谎替,“玉大人偷溺,你說我怎么就攤上這事∏幔” “怎么了挫掏?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秩命。 經(jīng)常有香客問我尉共,道長褒傅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任袄友,我火速辦了婚禮殿托,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘剧蚣。我一直安慰自己支竹,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布鸠按。 她就那樣靜靜地躺著礼搁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪目尖。 梳的紋絲不亂的頭發(fā)上馒吴,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音卑雁,去河邊找鬼募书。 笑死,一個胖子當(dāng)著我的面吹牛测蹲,可吹牛的內(nèi)容都是我干的莹捡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼扣甲,長吁一口氣:“原來是場噩夢啊……” “哼篮赢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起琉挖,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤启泣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后示辈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寥茫,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年矾麻,在試婚紗的時候發(fā)現(xiàn)自己被綠了纱耻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡险耀,死狀恐怖弄喘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情甩牺,我是刑警寧澤蘑志,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響急但,放射性物質(zhì)發(fā)生泄漏澎媒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一羊始、第九天 我趴在偏房一處隱蔽的房頂上張望旱幼。 院中可真熱鬧,春花似錦突委、人聲如沸柏卤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缘缚。三九已至,卻和暖如春敌蚜,著一層夾襖步出監(jiān)牢的瞬間桥滨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工弛车, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留齐媒,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓纷跛,卻偏偏與公主長得像喻括,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贫奠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容