IOS Method Swizzling 替換方法 Objective-C的hook方案

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


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

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


原文鏈接 http://blog.csdn.net/yiyaaixuexi/article/details/9374411


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

  1. #import "NSArray+Swizzle.h"  
  2.   
  3.   
  4. @implementation NSArray (Swizzle)  
  5.   
  6.   
  7. - (id)myLastObject  
  8. {  
  9.     id ret = [self myLastObject];  
  10.     NSLog(@"**********  myLastObject *********** ");  
  11.     return ret;  
  12. }  
  13. @end  

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



第二步:調換IMP
  1. #import <objc/runtime.h>  
  2. #import "NSArray+Swizzle.h"  
  3.   
  4.   
  5. int main(int argc, char *argv[])  
  6. {  
  7.     @autoreleasepool {  
  8.           
  9.         Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));  
  10.         Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));  
  11.         method_exchangeImplementations(ori_Method, my_Method);  
  12.           
  13.         NSArray *array = @[@"0",@"1",@"2",@"3"];  
  14.         NSString *string = [array lastObject];  
  15.         NSLog(@"TEST RESULT : %@",string);  
  16.           
  17.         return 0;  
  18.     }  
  19. }  


控制檯輸出Log:
  1. 2013-07-18 16:26:12.585 Hook[1740:c07] **********  myLastObject ***********   
  2. 2013-07-18 16:26:12.589 Hook[1740:c07] TEST RESULT : 3  


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




Method Swizzling 的封裝



之前在github上找到的RNSwizzle,推薦給大家,可以搜一下。
  1. //  
  2. //  RNSwizzle.m  
  3. //  MethodSwizzle  
  4.   
  5.   
  6. #import "RNSwizzle.h"  
  7. #import <objc/runtime.h>  
  8. @implementation NSObject (RNSwizzle)  
  9.   
  10.   
  11. + (IMP)swizzleSelector:(SEL)origSelector   
  12.                withIMP:(IMP)newIMP {  
  13.   Class class = [self class];  
  14.   Method origMethod = class_getInstanceMethod(class,  
  15.                                               origSelector);  
  16.   IMP origIMP = method_getImplementation(origMethod);  
  17.     
  18.   if(!class_addMethod(self, origSelector, newIMP,  
  19.                       method_getTypeEncoding(origMethod)))  
  20.   {  
  21.     method_setImplementation(origMethod, newIMP);  
  22.   }  
  23.     
  24.   return origIMP;  
  25. }  
  26. @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其實也不必過多考慮這個問題。我們只需要在原始方法命名前做小小的改動來命名就好,比如通常我們這樣命名:
 
  1. @interface NSView : NSObject  
  2. - (void)setFrame:(NSRect)frame;  
  3. @end  
  4.   
  5.   
  6. @implementation NSView (MyViewAdditions)  
  7.   
  8.   
  9. - (void)my_setFrame:(NSRect)frame {  
  10.     // do custom work  
  11.     [self my_setFrame:frame];  
  12. }  
  13.   
  14.   
  15. + (void)load {  
  16.     [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];  
  17. }  
  18.   
  19.   
  20. @end  


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

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

  1. @implementation NSView (MyViewAdditions)  
  2.   
  3.   
  4. static void MySetFrame(id self, SEL _cmd, NSRect frame);  
  5. static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);  
  6.   
  7.   
  8. static void MySetFrame(id self, SEL _cmd, NSRect frame) {  
  9.     // do custom work  
  10.     SetFrameIMP(self, _cmd, frame);  
  11. }  
  12.   
  13.   
  14. + (void)load {  
  15.     [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];  
  16. }  
  17.   
  18.   
  19. @end  

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


最後給出一個較完美的swizzle方法的定義:
  1. typedef IMP *IMPPointer;  
  2.   
  3.   
  4. BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {  
  5.     IMP imp = NULL;  
  6.     Method method = class_getInstanceMethod(class, original);  
  7.     if (method) {  
  8.         const char *type = method_getTypeEncoding(method);  
  9.         imp = class_replaceMethod(class, original, replacement, type);  
  10.         if (!imp) {  
  11.             imp = method_getImplementation(method);  
  12.         }  
  13.     }  
  14.     if (imp && store) { *store = imp; }  
  15.     return (imp != NULL);  
  16. }  
  17.   
  18.   
  19. @implementation NSObject (FRRuntimeAdditions)  
  20. + (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {  
  21.     return class_swizzleMethodAndStore(self, original, replacement, store);  
  22. }  
  23. @end  



Swizzling changes the method's arguments



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

  1. [self my_setFrame:frame];  


直接調用my_setFrame: , runtime做的是

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

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

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




The order of swizzles matters



多個swizzle方法的執行順序也需要注意。假設 setFrame: 只定義在NSView中,想像一下按照下面的順序執行:
  1. [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];  
  2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];  
  3. [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:
  1. [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];  
  2. [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];  
  3. [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進行許可 

發佈了56 篇原創文章 · 獲贊 4 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章