為什么有這篇博文
不知道何時開始iOS面試開始流行起來詢問什么是 Runtime,于是 iOSer 一聽 Runtime 總是就提起 MethodSwizzling,開口閉口就是黑科技。但其實如果讀者留意過C語言的 Hook 原理其實會發現所謂的鉤子都是框架或者語言的設計者預留給我們的工具,而不是什么黑科技,MethodSwizzling 其實只是一個簡單而有趣的機制罷了。然而就是這樣的機制,在日常中卻總能成為萬能藥一般的被肆無忌憚的使用。
很多 iOS 項目初期架構設計的不夠健壯,后期可擴展性差。于是 iOSer 想起了 MethodSwizzling 這個武器,將項目中一個正常的方法 hook 的滿天飛,導致項目的質量變得難以?控制。曾經我也愛在項目中濫用 MethodSwizzling,但在踩到坑之前總是不能意識到這種糟糕的做法會讓項目陷入怎樣的險境。于是我才明白學習某個機制要去深入的理解機制的設計,而不是跟風濫用,帶來糟糕的后果。最后就有了這篇文章。
Hook的對象
在 iOS 平臺常見的 hook 的對象一般有兩種:
- C/C++ functions
- Objective-C method
?對于 C/C+ +的 hook 常見的方式可以使用 facebook 的 fishhook
框架,具體原理可以參考深入理解Mac OS X & iOS 操作系統
這本書。
對于 Objective-C Methods 可能大家更熟悉一點,本文也只討論這個。
最常見的hook代碼
相信很多人使用過 JRSwizzle 這個庫,或者是看過 http://nshipster.cn/method-swizzling/ 的博文。
上述的代碼簡化如下。
+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_ {
Method origMethod = class_getInstanceMethod(self, origSel_);
if (!origMethod) {
SetNSError(error_, @"original method %@ not found for class %@", NSStringFromSelector(origSel_), [self class]);
return NO;
}
Method altMethod = class_getInstanceMethod(self, altSel_);
if (!altMethod) {
SetNSError(error_, @"alternate method %@ not found for class %@", NSStringFromSelector(altSel_), [self class]);
return NO;
}
class_addMethod(self,
origSel_,
class_getMethodImplementation(self, origSel_),
method_getTypeEncoding(origMethod));
class_addMethod(self,
altSel_,
class_getMethodImplementation(self, altSel_),
method_getTypeEncoding(altMethod));
method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));
return YES;
在?Swizzling情況極為普通的情況下上述代碼不會出現問題,但是場景復雜之后上面的代碼會有很多安全隱患。
MethodSwizzling泛濫下的隱患
Github有一個?很健壯的庫 RSSwizzle(這也是本文推薦Swizzling的最終方式) 指出了上面代碼帶來的風險點。
只在 +load 中執行 swizzling 才是安全的。
被 hook 的方法必須是當前類自身的方法,如果把繼承來的 IMP copy 到自身上面會存在問題。父類的方法應該在調用的時候使用,而不是 swizzling 的時候 copy 到子類。
被 Swizzled 的方法如果依賴與 cmd ,hook 之后 cmd 發送了變化,就會有問題(一般你 hook 的是系統類,也不知道系統用沒用 cmd 這個參數)。
命名如果沖突導致之前 hook 的失效 或者是循環調用。
上述問題中第一條和第四條說的是通常的 MethodSwizzling 是在分類里面實現的, 而分類的 Method 是被Runtime 加載的時候追加到類的 MethodList ,如果不是在 +load
是執行的 Swizzling 一旦出現重名,那么 SEL 和 IMP 不匹配致 hook 的結果是循環調用。
第三條是一個不容易被發現的問題。
我們都知道 Objective-C Method 都會有兩個隱含的參數 ?self, cmd
,有的時候開發者在使用關聯屬性的適合可能懶得聲明 (void *) 的 key,直接使用 cmd 變量 objc_setAssociatedObject(self, _cmd, xx, 0);
這會導致對當前IMP對 cmd 的依賴。
一旦此方法被 Swizzling,那么方法的 cmd 勢必會發生變化,出現了 bug 之后想必你一定找不到,等你找到之后心里一定會問候那位 Swizzling 你的方法的開發者祖宗十八代安好的,再者如果你 Swizzling 的是系統的方法恰好系統的方法內部用到了 cmd ..._(此處后背驚起一陣冷汗)。
Copy父類的方法帶來的問題
上面的第二條才是我們最容易遇見的場景,并且是99%的開發者都不會注意到的問題。下面我們來做個試驗
@implementation Person
- (void)sayHello {
NSLog(@"person say hello");
}
@end
@interface Student : Person
@end
@implementation Student (swizzle)
+ (void)load {
[self jr_swizzleMethod:@selector(s_sayHello) withMethod:@selector(sayHello) error:nil];
}
- (void)s_sayHello {
[self s_sayHello];
NSLog(@"Student + swizzle say hello");
}
@end
@implementation Person (swizzle)
+ (void)load {
[self jr_swizzleMethod:@selector(p_sayHello) withMethod:@selector(sayHello) error:nil];
}
- (void)p_sayHello {
[self p_sayHello];
NSLog(@"Person + swizzle say hello");
}
@end
上面的代碼中有一個 Person 類實現了 sayHello
方法,有一個 Student 繼承自 Person, 有一個Student 分類 Swizzling 了原來的? sayHello
, 還有一個 Person 的分類也 Swizzling 了原來的 sayhello
方法。
當我們生成一個 Student 類的實例并且調用 sayHello
方法,我們期望的輸出如下:
"person say hello"
"Person + swizzle say hello"
"Student + swizzle say hello"
但是輸出有可能是這樣的:
"person say hello"
"Student + swizzle say hello"
出現這樣的場景是由于在 build Phases
的 compile Source
順序子類分類在父類分類之前。
我們都知道在 Objective-C 的世界里父類的 +load
早于子類,但是并沒有?限制父類的分類加載?會早于子類的分類的加載,實際上這取決于編譯的順序。最終會按照編譯的順序合并進 Mach-O
?的固定 section 內。
下面會分析下為什么代碼會出現這樣的場景。
最開始的時候父類擁有自己的 sayHello
方法,子類擁有分類添加的 s_sayHello
方法并且在 s_sayHello
方法內部調用了 sel 為 s_sayHello
方法。
但是子類的分類在使用上面提到的 MethodSwizzling 的方法會導致?如下圖的變化
由于調用了 class_addMethod
方法會導致重新生成一份新的Method添加到 Student 類上面 但是 sel 并沒有發生變化,IMP 還是指向父類唯一的那個 IMP。
之后交換了子類兩個方法的 IMP 指針。于是方法引用變成了如下結構。
其中虛線指出的是方法的調用路徑。
單純在 Swizzling 一次的時候并沒有什么問題,但是我們并不能保證同事出于某種不可告人的目的的又去 Swizzling 了父類,或者是我們引入的第三庫做了這樣的操作。
于是我們在 Person 的分類里面 Swizzling 的時候會導致方法結構發生如下變化。
我們的代碼調用路徑就會是下圖這樣,相信你已經明白了前面的代碼執行結果中為什么父類在子類之后 Swizzling 其實并沒有對子類 hook 到。
這只是其中一種很常見的場景,造成的影響也只是 Hook 不到父類的派生類而已,?也不會造成一些嚴重的 Crash 等明顯現象,所以大部分開發者對此種行為是毫不知情的。
對于這種 Swizzling 方式的不確定性有一篇博文分析的更為全面玉令天下的博客Objective-C Method Swizzling
換個姿勢來Swizzling
前面提到 RSSwizzle 是另外一種更加健壯的Swizzling方式。
這里使用到了如下代碼
RSSwizzleInstanceMethod([Student class],
@selector(sayHello),
RSSWReturnType(void),
RSSWArguments(),
RSSWReplacement(
{
// Calling original implementation.
RSSWCallOriginal();
// Returning modified return value.
NSLog(@"Student + swizzle say hello sencod time");
}), 0, NULL);
RSSwizzleInstanceMethod([Person class],
@selector(sayHello),
RSSWReturnType(void),
RSSWArguments(),
RSSWReplacement(
{
// Calling original implementation.
RSSWCallOriginal();
// Returning modified return value.
NSLog(@"Person + swizzle say hello");
}), 0, NULL);
由于 RS 的方式需要提供一種 Swizzling 任何類型的簽名的 SEL,所以 RS 使用的是宏作為代碼包裝的入口,并且由開發者自行保證方法的參數個數和參數類型的正確性,所以使用起來也較為晦澀。 可能這也是他為什么這么優秀但是 star 很少的原因吧 :(。
我們將宏展開
RSSwizzleImpFactoryBlock newImp = ^id(RSSwizzleInfo *swizzleInfo) {
void (*originalImplementation_)(__attribute__((objc_ownership(none))) id, SEL);
SEL selector_ = @selector(sayHello);
return ^void (__attribute__((objc_ownership(none))) id self) {
IMP xx = method_getImplementation(class_getInstanceMethod([Student class], selector_));
IMP xx1 = method_getImplementation(class_getInstanceMethod(class_getSuperclass([Student class]) , selector_));
IMP oriiMP = (IMP)[swizzleInfo getOriginalImplementation];
((__typeof(originalImplementation_))[swizzleInfo getOriginalImplementation])(self, selector_);
//只有這一行是我們的核心邏輯
NSLog(@"Student + swizzle say hello");
};
};
[RSSwizzle swizzleInstanceMethod:@selector(sayHello)
inClass:[[Student class] class]
newImpFactory:newImp
mode:0 key:((void*)0)];;
RSSwizzle核心代碼其實只有一個函數
static void swizzle(Class classToSwizzle,
SEL selector,
RSSwizzleImpFactoryBlock factoryBlock)
{
Method method = class_getInstanceMethod(classToSwizzle, selector);
__block IMP originalIMP = NULL;
RSSWizzleImpProvider originalImpProvider = ^IMP{
IMP imp = originalIMP;
if (NULL == imp){
Class superclass = class_getSuperclass(classToSwizzle);
imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
}
return imp;
};
RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
swizzleInfo.selector = selector;
swizzleInfo.impProviderBlock = originalImpProvider;
id newIMPBlock = factoryBlock(swizzleInfo);
const char *methodType = method_getTypeEncoding(method);
IMP newIMP = imp_implementationWithBlock(newIMPBlock);
originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
}
上述代碼已經刪除無關的加鎖,防御邏輯,簡化理解。
我們可以看到 RS 的代碼其實是構造了一個 Block 里面裝著我們需要的執行的代碼。
然后再把我們的名字叫 originalImpProviderBloc
當做參數傳遞到我們的block里面,這里面包含了對將要被 Swizzling 的原始 IMP 的調用。
需要注意的是使用 class_replaceMethod
的時候如果一個方法來自父類,那么就給子類 add 一個方法, 并且把這個 NewIMP 設置給他,然后返回的結果是NULL。
在 originalImpProviderBloc
里面我們注意到如果 imp
是 NULL的時候,是動態的拿到父類的 Method 然后去執行。
我們還用圖來分析代碼。
最開始 Swizzling 第一次的時候,由于子類不存在 sayHello
方法,再添加方法的時候由于返回的原始 IMP 是 NULL,所以對父類的調用是動態獲取的,而不是通過之前的 sel 指針去調用。
如果我們再次對 Student Hook,由于 Student 已經有 sayHello
方法,這次 replace 會返回原來 IMP 的指針, 然后新的 IMP 會執被填充到 Method 的指針指向。
由此可見我們的方法引用是一個鏈表形狀的。
同理我們在 hook 父類的時候 父類的方法引用也是一個鏈表樣式的。
相信到了這里你已經理解 RS 來 Swizzling 方式是:
如果是父類的方法那么就動態查找,如果是自身的方法就構造方法引用鏈。來保證多次 Swizzling 的穩定性,并且不會和別人的 Swizzling 沖突。
而且 RS 的實現由于不是分類的方法也不用約束開發者必須在 +load
方法調用才能保證安全,并且cmd 也不會發生變化。
其他Hook方式
其實著名的 Hook 庫還有一個叫 Aspect 他利用的方法是把所有的方法調用指向 _objc_msgForward
然后自行實現消息轉發的步驟,在里面自行處理參數列表和返回值,通過 NSInvocation 去動態調用。
國內知名的熱修復庫 JSPatch
就是借鑒這種方式來實現熱修復的。
但是上面的庫要求必須是最后執行的確保 Hook 的成功。 而且他不兼容其他 Hook 方式,所以技術選型的時候要深思熟慮。
?什么時候需要Swizzling
我記得第一次學習 AO P概念的時候是當初在學習 javaWeb 的時候 Serverlet 里面的 FilterChain,開發者可以實現各種各種的過濾器然后在過濾器中插入log, 統計, 緩存等無關主業務邏輯的功能行性代碼, 著名的框架 Struts2
就是這樣實現的。
iOS 中由于 Swizzling 的 API 的簡單易用性導致開發者肆意濫用,影響了項目的穩定性。
當我們想要 Swizzling 的時候應該思考下我們能不能利用良好的代碼和架構設計來實現,或者是深入語言的特性來實現。
一個利用語言特性的例子
我們都知道在iOS8下的?操作系統中通知中心會持有一個 __unsafe_unretained
的觀察者指針。如果?觀察者在 ?dealloc 的時候忘記從通知中心中移除,之后如果觸發相關的通知就會造成 Crash。
我在設計防 Crash 工具 XXShield 的時候最初是 Hook NSObjec 的 dealloc
方法,在里面做相應的移除觀察者操作。后來一位真大佬提出這是一個非常不明智的操作,因為 dealloc 會影響全局的實例的釋放,開發者并不能保證代碼質量非常有保障,一旦出現問題將會引起整個 APP 運行期間大面積崩潰或異常行為。
下面我們先來看下 ObjCRuntime 源碼關于一個對象釋放時要做的事情,代碼約在objc-runtime-new.mm
第6240行。
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
上面的邏輯中明確了寫明了一個對象在釋放的時候初了調用 dealloc
方法,還需要斷開實例上綁定的觀察對象, 那么我們可以在添加觀察者的時候給觀察者動態的綁定一個關聯對象,然后關聯對象可以反向持有觀察者,然后在關聯對象釋放的時候去移除觀察者,由于不能造成循環引用所以只能選擇 __weak
或者 __unsafe_unretained
的指針, 實驗得知 __weak
的指針在 dealloc
之前就已經被清空, 所以我們只能使用 __unsafe_unretained
指針。
@interface XXObserverRemover : NSObject {
__strong NSMutableArray *_centers;
__unsafe_unretained id _obs;
}
@end
@implementation XXObserverRemover
- (instancetype)initWithObserver:(id)obs {
if (self = [super init]) {
_obs = obs;
_centers = @[].mutableCopy;
}
return self;
}
- (void)addCenter:(NSNotificationCenter*)center {
if (center) {
[_centers addObject:center];
}
}
- (void)dealloc {
@autoreleasepool {
for (NSNotificationCenter *center in _centers) {
[center removeObserver:_obs];
}
}
}
@end
void addCenterForObserver(NSNotificationCenter *center ,id obs) {
XXObserverRemover *remover = nil;
static char removerKey;
@autoreleasepool {
remover = objc_getAssociatedObject(obs, &removerKey);
if (!remover) {
remover = [[XXObserverRemover alloc] initWithObserver:obs];
objc_setAssociatedObject(obs, &removerKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[remover addCenter:center];
}
}
void autoHook() {
RSSwizzleInstanceMethod([NSNotificationCenter class], @selector(addObserver:selector:name:object:),
RSSWReturnType(void), RSSWArguments(id obs,SEL cmd,NSString *name,id obj),
RSSWReplacement({
RSSWCallOriginal(obs,cmd,name,obj);
addCenterForObserver(self, obs);
}), 0, NULL);
}
需要注意的是在添加關聯者的時候一定要將代碼包含在一個自定義的 AutoreleasePool
內。
我們都知道在 Objective-C 的世界里一個對象如果是 Autorelease 的 那么這個對象在當前方法棧結束后才會延時釋放,在 ARC 環境下?,一般一個 Autorelease 的對象會被放在一個系統提供的 AutoreleasePool 里面,然后AutoReleasePool drain 的時候再去釋放內部持有的對象,通常情況下命令行程序是沒有問題的,但是在iOS的環境中 AutoReleasePool是在 Runloop 控制下在空閑時間進行釋放的,這樣可以提升用戶體驗,避免造成卡頓,但是在我們這種場景中會有問題,我們嚴格依賴了觀察者?調用 dealloc 的時候關聯對象也會去 dealloc,如果系統的 AutoReleasePool 出現了延時釋放,會導致當前對象被回收之后 過段時間關聯對象才會釋放,這時候前文使用的 __unsafe_unretained 訪問的?就是非法地址。
我們在添加關聯對象的時候添加一個自定義的 AutoreleasePool 保證了對關聯對象引用的單一性,保證了我們依賴的釋放順序是正確的。從而正確的移除觀察者。
參考
友情感謝
最后感謝 騎神 大佬修改我那蹩腳的文字描述。