為什么有這篇博文
不知道何時(shí)開始iOS面試開始流行起來詢問什么是 Runtime逃沿,于是 iOSer 一聽 Runtime 總是就提起 MethodSwizzling岳枷,開口閉口就是黑科技爪瓜。但其實(shí)如果讀者留意過C語言的 Hook 原理其實(shí)會發(fā)現(xiàn)所謂的鉤子都是框架或者語言的設(shè)計(jì)者預(yù)留給我們的工具施戴,而不是什么黑科技,MethodSwizzling 其實(shí)只是一個簡單而有趣的機(jī)制罷了煎源。然而就是這樣的機(jī)制羹铅,在日常中卻總能成為萬能藥一般的被肆無忌憚的使用。
很多 iOS 項(xiàng)目初期架構(gòu)設(shè)計(jì)的不夠健壯坐漏,后期可擴(kuò)展性差薄疚。于是 iOSer 想起了 MethodSwizzling 這個武器,將項(xiàng)目中一個正常的方法 hook 的滿天飛赊琳,導(dǎo)致項(xiàng)目的質(zhì)量變得難以?控制街夭。曾經(jīng)我也愛在項(xiàng)目中濫用 MethodSwizzling,但在踩到坑之前總是不能意識到這種糟糕的做法會讓項(xiàng)目陷入怎樣的險(xiǎn)境躏筏。于是我才明白學(xué)習(xí)某個機(jī)制要去深入的理解機(jī)制的設(shè)計(jì)板丽,而不是跟風(fēng)濫用,帶來糟糕的后果趁尼。最后就有了這篇文章埃碱。
Hook的對象
在 iOS 平臺常見的 hook 的對象一般有兩種:
- C/C++ functions
- Objective-C method
?對于 C/C+ +的 hook 常見的方式可以使用 facebook 的 fishhook
框架,具體原理可以參考深入理解Mac OS X & iOS 操作系統(tǒng)
這本書酥泞。
對于 Objective-C Methods 可能大家更熟悉一點(diǎn)砚殿,本文也只討論這個。
最常見的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情況極為普通的情況下上述代碼不會出現(xiàn)問題凡人,但是場景復(fù)雜之后上面的代碼會有很多安全隱患名党。
MethodSwizzling泛濫下的隱患
Github有一個?很健壯的庫 RSSwizzle(這也是本文推薦Swizzling的最終方式) 指出了上面代碼帶來的風(fēng)險(xiǎn)點(diǎn)。
只在 +load 中執(zhí)行 swizzling 才是安全的挠轴。
被 hook 的方法必須是當(dāng)前類自身的方法传睹,如果把繼承來的 IMP copy 到自身上面會存在問題。父類的方法應(yīng)該在調(diào)用的時(shí)候使用岸晦,而不是 swizzling 的時(shí)候 copy 到子類欧啤。
被 Swizzled 的方法如果依賴與 cmd ,hook 之后 cmd 發(fā)送了變化启上,就會有問題(一般你 hook 的是系統(tǒng)類邢隧,也不知道系統(tǒng)用沒用 cmd 這個參數(shù))。
命名如果沖突導(dǎo)致之前 hook 的失效 或者是循環(huán)調(diào)用冈在。
上述問題中第一條和第四條說的是通常的 MethodSwizzling 是在分類里面實(shí)現(xiàn)的, 而分類的 Method 是被Runtime 加載的時(shí)候追加到類的 MethodList 倒慧,如果不是在 +load
是執(zhí)行的 Swizzling 一旦出現(xiàn)重名,那么 SEL 和 IMP 不匹配致 hook 的結(jié)果是循環(huán)調(diào)用包券。
第三條是一個不容易被發(fā)現(xiàn)的問題纫谅。
我們都知道 Objective-C Method 都會有兩個隱含的參數(shù) ?self, cmd
,有的時(shí)候開發(fā)者在使用關(guān)聯(lián)屬性的適合可能懶得聲明 (void *) 的 key溅固,直接使用 cmd 變量 objc_setAssociatedObject(self, _cmd, xx, 0);
這會導(dǎo)致對當(dāng)前IMP對 cmd 的依賴付秕。
一旦此方法被 Swizzling,那么方法的 cmd 勢必會發(fā)生變化侍郭,出現(xiàn)了 bug 之后想必你一定找不到询吴,等你找到之后心里一定會問候那位 Swizzling 你的方法的開發(fā)者祖宗十八代安好的,再者如果你 Swizzling 的是系統(tǒng)的方法恰好系統(tǒng)的方法內(nèi)部用到了 cmd ..._(此處后背驚起一陣?yán)浜梗?/p>
Copy父類的方法帶來的問題
上面的第二條才是我們最容易遇見的場景亮元,并且是99%的開發(fā)者都不會注意到的問題汰寓。下面我們來做個試驗(yàn)
@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 類實(shí)現(xiàn)了 sayHello
方法,有一個 Student 繼承自 Person苹粟, 有一個Student 分類 Swizzling 了原來的? sayHello
, 還有一個 Person 的分類也 Swizzling 了原來的 sayhello
方法有滑。
當(dāng)我們生成一個 Student 類的實(shí)例并且調(diào)用 sayHello
方法,我們期望的輸出如下:
"person say hello"
"Person + swizzle say hello"
"Student + swizzle say hello"
但是輸出有可能是這樣的:
"person say hello"
"Student + swizzle say hello"
出現(xiàn)這樣的場景是由于在 build Phases
的 compile Source
順序子類分類在父類分類之前嵌削。
我們都知道在 Objective-C 的世界里父類的 +load
早于子類毛好,但是并沒有?限制父類的分類加載?會早于子類的分類的加載,實(shí)際上這取決于編譯的順序苛秕。最終會按照編譯的順序合并進(jìn) Mach-O
?的固定 section 內(nèi)肌访。
下面會分析下為什么代碼會出現(xiàn)這樣的場景。
最開始的時(shí)候父類擁有自己的 sayHello
方法艇劫,子類擁有分類添加的 s_sayHello
方法并且在 s_sayHello
方法內(nèi)部調(diào)用了 sel 為 s_sayHello
方法吼驶。
但是子類的分類在使用上面提到的 MethodSwizzling 的方法會導(dǎo)致?如下圖的變化
由于調(diào)用了 class_addMethod
方法會導(dǎo)致重新生成一份新的Method添加到 Student 類上面 但是 sel 并沒有發(fā)生變化,IMP 還是指向父類唯一的那個 IMP。
之后交換了子類兩個方法的 IMP 指針蟹演。于是方法引用變成了如下結(jié)構(gòu)风钻。
其中虛線指出的是方法的調(diào)用路徑。
單純在 Swizzling 一次的時(shí)候并沒有什么問題酒请,但是我們并不能保證同事出于某種不可告人的目的的又去 Swizzling 了父類骡技,或者是我們引入的第三庫做了這樣的操作。
于是我們在 Person 的分類里面 Swizzling 的時(shí)候會導(dǎo)致方法結(jié)構(gòu)發(fā)生如下變化羞反。
我們的代碼調(diào)用路徑就會是下圖這樣布朦,相信你已經(jīng)明白了前面的代碼執(zhí)行結(jié)果中為什么父類在子類之后 Swizzling 其實(shí)并沒有對子類 hook 到。
這只是其中一種很常見的場景昼窗,造成的影響也只是 Hook 不到父類的派生類而已是趴,?也不會造成一些嚴(yán)重的 Crash 等明顯現(xiàn)象,所以大部分開發(fā)者對此種行為是毫不知情的澄惊。
對于這種 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 使用的是宏作為代碼包裝的入口缤削,并且由開發(fā)者自行保證方法的參數(shù)個數(shù)和參數(shù)類型的正確性窘哈,所以使用起來也較為晦澀。 可能這也是他為什么這么優(yōu)秀但是 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核心代碼其實(shí)只有一個函數(shù)
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);
}
上述代碼已經(jīng)刪除無關(guān)的加鎖滚婉,防御邏輯,簡化理解帅刀。
我們可以看到 RS 的代碼其實(shí)是構(gòu)造了一個 Block 里面裝著我們需要的執(zhí)行的代碼让腹。
然后再把我們的名字叫 originalImpProviderBloc
當(dāng)做參數(shù)傳遞到我們的block里面,這里面包含了對將要被 Swizzling 的原始 IMP 的調(diào)用扣溺。
需要注意的是使用 class_replaceMethod
的時(shí)候如果一個方法來自父類骇窍,那么就給子類 add 一個方法, 并且把這個 NewIMP 設(shè)置給他锥余,然后返回的結(jié)果是NULL腹纳。
在 originalImpProviderBloc
里面我們注意到如果 imp
是 NULL的時(shí)候,是動態(tài)的拿到父類的 Method 然后去執(zhí)行驱犹。
我們還用圖來分析代碼嘲恍。
最開始 Swizzling 第一次的時(shí)候,由于子類不存在 sayHello
方法雄驹,再添加方法的時(shí)候由于返回的原始 IMP 是 NULL佃牛,所以對父類的調(diào)用是動態(tài)獲取的,而不是通過之前的 sel 指針去調(diào)用医舆。
如果我們再次對 Student Hook俘侠,由于 Student 已經(jīng)有 sayHello
方法象缀,這次 replace 會返回原來 IMP 的指針, 然后新的 IMP 會執(zhí)被填充到 Method 的指針指向爷速。
由此可見我們的方法引用是一個鏈表形狀的央星。
同理我們在 hook 父類的時(shí)候 父類的方法引用也是一個鏈表樣式的。
相信到了這里你已經(jīng)理解 RS 來 Swizzling 方式是:
如果是父類的方法那么就動態(tài)查找遍希,如果是自身的方法就構(gòu)造方法引用鏈等曼。來保證多次 Swizzling 的穩(wěn)定性里烦,并且不會和別人的 Swizzling 沖突凿蒜。
而且 RS 的實(shí)現(xiàn)由于不是分類的方法也不用約束開發(fā)者必須在 +load
方法調(diào)用才能保證安全,并且cmd 也不會發(fā)生變化胁黑。
其他Hook方式
其實(shí)著名的 Hook 庫還有一個叫 Aspect 他利用的方法是把所有的方法調(diào)用指向 _objc_msgForward
然后自行實(shí)現(xiàn)消息轉(zhuǎn)發(fā)的步驟废封,在里面自行處理參數(shù)列表和返回值,通過 NSInvocation 去動態(tài)調(diào)用丧蘸。
國內(nèi)知名的熱修復(fù)庫 JSPatch
就是借鑒這種方式來實(shí)現(xiàn)熱修復(fù)的漂洋。
但是上面的庫要求必須是最后執(zhí)行的確保 Hook 的成功。 而且他不兼容其他 Hook 方式力喷,所以技術(shù)選型的時(shí)候要深思熟慮刽漂。
?什么時(shí)候需要Swizzling
我記得第一次學(xué)習(xí) AO P概念的時(shí)候是當(dāng)初在學(xué)習(xí) javaWeb 的時(shí)候 Serverlet 里面的 FilterChain,開發(fā)者可以實(shí)現(xiàn)各種各種的過濾器然后在過濾器中插入log弟孟, 統(tǒng)計(jì)贝咙, 緩存等無關(guān)主業(yè)務(wù)邏輯的功能行性代碼, 著名的框架 Struts2
就是這樣實(shí)現(xiàn)的拂募。
iOS 中由于 Swizzling 的 API 的簡單易用性導(dǎo)致開發(fā)者肆意濫用庭猩,影響了項(xiàng)目的穩(wěn)定性。
當(dāng)我們想要 Swizzling 的時(shí)候應(yīng)該思考下我們能不能利用良好的代碼和架構(gòu)設(shè)計(jì)來實(shí)現(xiàn)陈症,或者是深入語言的特性來實(shí)現(xiàn)蔼水。
一個利用語言特性的例子
我們都知道在iOS8下的?操作系統(tǒng)中通知中心會持有一個 __unsafe_unretained
的觀察者指針。如果?觀察者在 ?dealloc 的時(shí)候忘記從通知中心中移除录肯,之后如果觸發(fā)相關(guān)的通知就會造成 Crash趴腋。
我在設(shè)計(jì)防 Crash 工具 XXShield 的時(shí)候最初是 Hook NSObjec 的 dealloc
方法,在里面做相應(yīng)的移除觀察者操作论咏。后來一位真大佬提出這是一個非常不明智的操作于样,因?yàn)?dealloc 會影響全局的實(shí)例的釋放,開發(fā)者并不能保證代碼質(zhì)量非常有保障潘靖,一旦出現(xiàn)問題將會引起整個 APP 運(yùn)行期間大面積崩潰或異常行為穿剖。
下面我們先來看下 ObjCRuntime 源碼關(guān)于一個對象釋放時(shí)要做的事情,代碼約在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;
}
上面的邏輯中明確了寫明了一個對象在釋放的時(shí)候初了調(diào)用 dealloc
方法糊余,還需要斷開實(shí)例上綁定的觀察對象秀又, 那么我們可以在添加觀察者的時(shí)候給觀察者動態(tài)的綁定一個關(guān)聯(lián)對象,然后關(guān)聯(lián)對象可以反向持有觀察者,然后在關(guān)聯(lián)對象釋放的時(shí)候去移除觀察者贬芥,由于不能造成循環(huán)引用所以只能選擇 __weak
或者 __unsafe_unretained
的指針吐辙, 實(shí)驗(yàn)得知 __weak
的指針在 dealloc
之前就已經(jīng)被清空, 所以我們只能使用 __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);
}
需要注意的是在添加關(guān)聯(lián)者的時(shí)候一定要將代碼包含在一個自定義的 AutoreleasePool
內(nèi)昏苏。
我們都知道在 Objective-C 的世界里一個對象如果是 Autorelease 的 那么這個對象在當(dāng)前方法棧結(jié)束后才會延時(shí)釋放,在 ARC 環(huán)境下?威沫,一般一個 Autorelease 的對象會被放在一個系統(tǒng)提供的 AutoreleasePool 里面贤惯,然后AutoReleasePool drain 的時(shí)候再去釋放內(nèi)部持有的對象,通常情況下命令行程序是沒有問題的棒掠,但是在iOS的環(huán)境中 AutoReleasePool是在 Runloop 控制下在空閑時(shí)間進(jìn)行釋放的孵构,這樣可以提升用戶體驗(yàn),避免造成卡頓烟很,但是在我們這種場景中會有問題颈墅,我們嚴(yán)格依賴了觀察者?調(diào)用 dealloc 的時(shí)候關(guān)聯(lián)對象也會去 dealloc,如果系統(tǒng)的 AutoReleasePool 出現(xiàn)了延時(shí)釋放雾袱,會導(dǎo)致當(dāng)前對象被回收之后 過段時(shí)間關(guān)聯(lián)對象才會釋放恤筛,這時(shí)候前文使用的 __unsafe_unretained 訪問的?就是非法地址。
我們在添加關(guān)聯(lián)對象的時(shí)候添加一個自定義的 AutoreleasePool 保證了對關(guān)聯(lián)對象引用的單一性芹橡,保證了我們依賴的釋放順序是正確的毒坛。從而正確的移除觀察者。
參考
友情感謝
最后感謝 騎神 大佬修改我那蹩腳的文字描述僻族。