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

來源:互聯網
上載者:User
文章目錄
  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug
Objective-C的hook方案(一):  Method Swizzling

在沒有一個類的實現源碼的情況下,想改變其中一個方法的實現,除了繼承它重寫、和藉助類別重名方法暴力搶先之外,還有更加靈活的方法嗎?在Objective-C編程中,如何?hook呢?標題有點大,計劃分幾篇來總結。

本文主要介紹針對selector的hook,主角被標題劇透了———— Method Swizzling 。

Method Swizzling 原理

在Objective-C中調用一個方法,其實是向一個對象發送訊息,尋找訊息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法掛鈎的目的。

每個類都有一個方法列表,存放著selector的名字和方法實現的映射關係。IMP有點類似函數指標,指向具體的Method實現。

我們可以利用 method_exchangeImplementations 來交換2個方法中的IMP,

我們可以利用 class_replaceMethod 來修改類,

我們可以利用 method_setImplementation 來直接設定某個方法的IMP,
……

歸根結底,都是偷換了selector的IMP,如所示:

Method Swizzling 實踐

舉個例子好了,我想鉤一下NSArray的lastObject 方法,只需兩個步驟。

第一步:給NSArray加一個我自己的lastObject

#import "NSArray+Swizzle.h"@implementation NSArray (Swizzle)- (id)myLastObject{    id ret = [self myLastObject];    NSLog(@"**********  myLastObject *********** ");    return ret;}@end

乍一看,這不遞迴了嗎?別忘記這是我們準備調換IMP的selector,[self myLastObject] 將會執行真的 [self lastObject] 。

第二步:調換IMP

#import <objc/runtime.h>#import "NSArray+Swizzle.h"int main(int argc, char *argv[]){    @autoreleasepool {                Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));        Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));        method_exchangeImplementations(ori_Method, my_Method);                NSArray *array = @[@"0",@"1",@"2",@"3"];        NSString *string = [array lastObject];        NSLog(@"TEST RESULT : %@",string);                return 0;    }}

控制台輸出Log:

2013-07-18 16:26:12.585 Hook[1740:c07] **********  myLastObject *********** 2013-07-18 16:26:12.589 Hook[1740:c07] TEST RESULT : 3

結果很讓人欣喜,是不是忍不住想給UIWebView的loadRequest: 加 TODO 了呢? 

Method Swizzling 的封裝

之前在github上找到的RNSwizzle,推薦給大家,可以搜一下。

////  RNSwizzle.m//  MethodSwizzle#import "RNSwizzle.h"#import <objc/runtime.h>@implementation NSObject (RNSwizzle)+ (IMP)swizzleSelector:(SEL)origSelector                withIMP:(IMP)newIMP {  Class class = [self class];  Method origMethod = class_getInstanceMethod(class,                                              origSelector);  IMP origIMP = method_getImplementation(origMethod);    if(!class_addMethod(self, origSelector, newIMP,                      method_getTypeEncoding(origMethod)))  {    method_setImplementation(origMethod, newIMP);  }    return origIMP;}@end
Method Swizzling 危險不危險

針對這個問題,我在stackoverflow上看到了滿意的答案,這裡翻譯一下,總結記錄在本文中,以示分享:

使用 Method Swizzling 編程就好比切菜時使用鋒利的刀,一些人因為擔心切到自己所以害怕鋒利的刀具,可是事實上,使用鈍刀往往更容易出事,而利刀更為安全。
Method swizzling 可以協助我們寫出更好的,更高效的,易維護的代碼。但是如果濫用它,也將會導致難以排查的bug。 

背景

好比設計模式,如果我們摸清了一個模式的門道,使用該模式與否我們自己心裡有數。單例模式就是一個很好的例子,它飽受爭議但是許多人依舊使用它。Method Swizzling也是一樣,一旦你真正理解它的優勢和弊端,使用它與否你應該就有你自己的觀點。

討論

這裡是一些 Method Swizzling的陷阱:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

我將逐一分析這些點,增進對Method Swizzling的理解的同時,並搞懂如何應對。

Method swizzling is not atomic

我所見過的使用method swizzling實現的方法在並發使用時基本都是安全的。95%的情況裡這都不會是個問題。通常你替換一個方法的實現,是希望它在整個程式的生命週期裡有效。也就是說,你會把 method swizzling 修改方法實現的操作放在一個加號方法 +(void)load裡,並在應用程式的一開始就調用執行。你將不會碰到並發問題。假如你在 +(void)initialize初始化方法中進行swizzle,那麼……rumtime可能死於一個詭異的狀態。

Changes behavior of un-owned code

這是swizzling的一個問題。我們的目標是改變某些代碼。swizzling方法是一件灰常灰常重要的事,當你不只是對一個NSButton類的執行個體進行了修改,而是程式中所有的NSButton執行個體。因此在swizzling時應該多加小心,但也不用總是去刻意避免。

想象一下,如果你重寫了一個類的方法,而且沒有調用父類的這個方法,這可能會引起問題。大多數情況下,父類方法期望會被調用(至少文檔是這樣說的)。如果你在swizzling實現中也這樣做了,這會避免大部分問題。還是調用原始實現吧,如若不然,你會費很大力氣去考慮代碼的安全問題。

Possible naming conflicts

命名衝突貫穿整個Cocoa的問題. 我們常常在類名和類別方法名前加上首碼。不幸的是,命名衝突仍是個折磨。但是swizzling其實也不必過多考慮這個問題。我們只需要在原始方法命名前做小小的改動來命名就好,比如通常我們這樣命名:
 

@interface NSView : NSObject- (void)setFrame:(NSRect)frame;@end@implementation NSView (MyViewAdditions)- (void)my_setFrame:(NSRect)frame {    // do custom work    [self my_setFrame:frame];}+ (void)load {    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];}@end

這段代碼運行正確,但是如果my_setFrame: 在別處被定義了會發生什麼呢?

這個問題不僅僅存在於swizzling,這裡有一個替代的變通方法:

@implementation NSView (MyViewAdditions)static void MySetFrame(id self, SEL _cmd, NSRect frame);static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);static void MySetFrame(id self, SEL _cmd, NSRect frame) {    // do custom work    SetFrameIMP(self, _cmd, frame);}+ (void)load {    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];}@end

看起來不那麼Objectice-C了(用了函數指標),這樣避免了selector的命名衝突。 

最後給出一個較完美的swizzle方法的定義:

typedef IMP *IMPPointer;BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {    IMP imp = NULL;    Method method = class_getInstanceMethod(class, original);    if (method) {        const char *type = method_getTypeEncoding(method);        imp = class_replaceMethod(class, original, replacement, type);        if (!imp) {            imp = method_getImplementation(method);        }    }    if (imp && store) { *store = imp; }    return (imp != NULL);}@implementation NSObject (FRRuntimeAdditions)+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {    return class_swizzleMethodAndStore(self, original, replacement, store);}@end

Swizzling changes the method's arguments

我認為這是最大的問題。想正常調用method swizzling 將會是個問題。

[self my_setFrame:frame];

直接調用my_setFrame: , runtime做的是

objc_msgSend(self, @selector(my_setFrame:), frame);

runtime去尋找my_setFrame:的方法實現, _cmd參數為 my_setFrame: ,但是事實上runtime找到的方法實現是原始的 setFrame: 的。

一個簡單的解決辦法:使用上面介紹的swizzling定義。

The order of swizzles matters

多個swizzle方法的執行順序也需要注意。假設 setFrame: 只定義在NSView中,想像一下按照下面的順序執行:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).

When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.

But what if the order were:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?

Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!

這段貼了原文,硬翻譯太拗口……總結一下就是:多個有繼承關係的類的對象swizzle時,從子類對象開始 。 如果先swizzle父類對象,那麼後面子類對象swizzle時就無法拿到真正的原始方法實現了。 

(感謝評論中 qq373127202 的提醒,在此更正一下,十分感謝)

多個有繼承關係的類的對象swizzle時,先從父物件開始。 這樣才能保證子類方法拿到父類中的被swizzle的實現。在+(void)load中swizzle不會出錯,就是因為load類方法會預設從父類開始調用。

Difficult to understand (looks recursive)

(新方法的實現)看起來像遞迴,但是看看上面已經給出的 swizzling 封裝方法, 使用起來就很易讀懂.
這個問題是已完全解決的了!

Difficult to debug

debug時打出的backtrace,其中摻雜著被swizzle的方法名,一團糟啊!上面介紹的swizzle方案,使backtrace中列印出的方法名還是很清晰的。但仍然很難去debug,因為很難記住swizzling影響過什麼。給你的代碼寫好文檔(即使只有你一個人會看到)。養成一個好習慣,不會比調試多線程問題還難的。

結論

如果使用恰當,Method swizzling 還是很安全的.一個簡單安全的方法是,僅在load中swizzle。 和許多其他東西一樣,它也是有危險性的,但理解它了也就可以正確恰當的使用它了。

(本部落格中所有原創文章及譯文均採用知識共用署名-非商業性使用-相同方式共用 2.5進行許可 )

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.