不知道什麼時候開始iOS面試開始流行起來詢問什麼是 Runtime,因而 iOSer 一聽 Runtime 老是就提起 MethodSwizzling,開口閉口就是黑科技。但其實若是讀者留意過C語言的 Hook 原理其實會發現所謂的鉤子都是框架或者語言的設計者預留給咱們的工具,而不是什麼黑科技,MethodSwizzling 其實只是一個簡單而有趣的機制罷了。然而就是這樣的機制,在平常中卻總能成爲萬能藥通常的被肆無忌憚的使用。java
不少 iOS 項目初期架構設計的不夠健壯,後期可擴展性差。因而 iOSer 想起了 MethodSwizzling 這個武器,將項目中一個正常的方法 hook 的滿天飛,致使項目的質量變得難以控制。曾經我也愛在項目中濫用 MethodSwizzling,但在踩到坑以前老是不能意識到這種糟糕的作法會讓項目陷入怎樣的險境。因而我才明白學習某個機制要去深刻的理解機制的設計,而不是跟風濫用,帶來糟糕的後果。最後就有了這篇文章。git
在 iOS 平臺常見的 hook 的對象通常有兩種:github
對於 C/C+ +的 hook 常見的方式可使用 facebook 的 fishhook
框架,具體原理能夠參考深刻理解Mac OS X & iOS 操做系統
這本書。
對於 Objective-C Methods 可能你們更熟悉一點,本文也只討論這個。面試
相信不少人使用過 JRSwizzle 這個庫,或者是看過 nshipster.cn/method-swiz… 的博文。
上述的代碼簡化以下。緩存
+ (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狀況極爲普通的狀況下上述代碼不會出現問題,可是場景複雜以後上面的代碼會有不少安全隱患。安全
Github有一個很健壯的庫 RSSwizzle(這也是本文推薦Swizzling的最終方式) 指出了上面代碼帶來的風險點。markdown
只在 +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 ...~_~(此處後背驚起一陣冷汗)。
上面的第二條纔是咱們最容易碰見的場景,而且是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
前面提到 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 庫還有一個叫 Aspect 他利用的方法是把全部的方法調用指向 _objc_msgForward
而後自行實現消息轉發的步驟,在裏面自行處理參數列表和返回值,經過 NSInvocation 去動態調用。
國內知名的熱修復庫 JSPatch
就是借鑑這種方式來實現熱修復的。
可是上面的庫要求必須是最後執行的確保 Hook 的成功。 並且他不兼容其餘 Hook 方式,因此技術選型的時候要深思熟慮。
我記得第一次學習 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 保證了對關聯對象引用的單一性,保證了咱們依賴的釋放順序是正確的。從而正確的移除觀察者。
最後感謝 騎神 大佬修改我那蹩腳的文字描述。