前言
在iOS日常開發中,對某些方法進行hook是很常見的操作。最常見的是使用Category在+load
中進行方法swizzle,它是針對類的,會改變這個類所有實例的行為。但是有時候我們只想針對單個實例進行hook,這種方法就顯得無力了。而Aspects
框架可以搞定這個問題。 它的原理是通過Runtime
動態的創建子類,把實例的isa
指針指向新創建的子類,然后在子類中對hook的方法進行處理,這樣就支持了對單個實例的hook。Aspects
框架支持對類和實例的hook,API很易用,可以方便的讓你在任何地方進行hook,是線程安全的。但是Aspects
框架也有一些缺陷,一不小心就會掉坑里面,我會通過源碼解析進行說明。
源碼解析
我主要使用圖示對Aspects
的源碼進行說明,建議參考源碼一起查看。要看懂這些內容,需要對isa指針
,消息轉發機制
,runtime
有一定的了解,本文中不會對這些內容展開來講,因為要把這些東西講清楚,每一項都需要單獨寫一篇文章了。
主要流程解析
- 它第一個流程是使用關聯對象添加
Container
,在這個過程中會進行一些前置條件的判斷,例如這個方法是否支持被hook等,如果條件驗證通過,就會把這次hook的信息保存起來,在方法調用的時候,查詢出來使用。 - 第二個流程是動態創建子類,如果是針對類的hook,則不會走這一步。
- 第三步是替換這個類的
forwardInvocation:
方法為__ASPECTS_ARE_BEING_CALLED__
,這個方法內部會查找到之前創建的Container,然后根據Container中的邏輯進行實際的調用。 - 第四步是將原有方法的
IMP
改為_objc_msgForward
,改完后當調用原有方法時,就會調用_objc_msgForward
,從而觸發forwardInvocation:
方法。
我對它的流程做了一個簡化的圖示,標有每個流程的序號,后面會對每個流程進行解析。流程如下:
圖示中的取出對象類型
,是指的調用hook的對象的類型,如果是實例對象
,那么就走類
路徑;如果是類
對象,則走元類
路徑;如果是kvo
等實際類型不一致的情況,則走其它子類
路徑。
①添加Container流程
這個流程中,把hook的邏輯封裝成Container,並使用關聯對象進行保存。這個過程中會判斷hook的方法是否被支持、判斷被hook類的繼承關系、驗證回調block正確性等操作。具體圖示如下:
關鍵代碼如下:
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { ... aspect_performLocked(^{ // 加鎖 // hook前置條件判斷 if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { // 用selector作key,通過關聯對象獲得Container對象。 AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); // 內部會判斷block與hook的selector是否匹配,不匹配返回nil。 identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; if (identifier) { // 添加identifier,包含了hook的類型和回調。 [aspectContainer addAspect:identifier withOptions:options]; // Modify the class to allow message interception. aspect_prepareClassAndHookSelector(self, selector, error); } } }); return identifier; } static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) { static NSSet *disallowedSelectorList; static dispatch_once_t pred; dispatch_once(&pred, ^{ disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil]; }); // 這里對不支持hook的方法進行過濾 NSString *selectorName = NSStringFromSelector(selector); if ([disallowedSelectorList containsObject:selectorName]) { NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName]; AspectError(AspectErrorSelectorBlacklisted, errorDescription); return NO; } // dealloc只支持AspectPositionBefore類型下調用 AspectOptions position = options&AspectPositionFilter; if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) { NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc."; AspectError(AspectErrorSelectorDeallocPosition, errorDesc); return NO; } // 判斷是否存在這個方法 if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) { NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName]; AspectError(AspectErrorDoesNotRespondToSelector, errorDesc); return NO; } // 這里禁止有繼承關系的類hook同一個方法,代碼量較多,不是關鍵內容,這里不貼出 if (class_isMetaClass(object_getClass(self))) { ... } return YES; } /// AspectsContainer內部添加AspectIdentifier的實現。 /// 這里可以看出對同一個方法的多次hook都會被調用,不會出現后面hook的覆蓋前面的情況。 - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options { NSParameterAssert(aspect); NSUInteger position = options&AspectPositionFilter; switch (position) { case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break; case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break; case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break; } } 復制代碼
- 從源碼中可以看到,不支持的hook方法有
[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
。其中retain
,release
,autorelease
在arc下是被禁用的,框架本身是hook
了forwardInvocation:
進行實現的,所以對它的hook也不支持。 dealloc
只支持AspectPositionBefore
類型,使用AspectPositionInstead
會導致系統默認的dealloc
操作被替換無法執行而出現問題。AspectPositionAfter
類型,調用時對象可能已經已經被釋放了,從而引發野指針錯誤。Aspects
禁止有繼承關系的類hook同一個方法,具體可以參見它的一個issue,它報告了這樣操作會導致死循環,我會在文章后面再進行說明。Aspects
使用block
進行hook的調用,涉及到方法參數的傳遞和返回值問題,所以其中會對block進行校驗。
②runtime創建子類
iOS中的KVO
就是通過runtime
動態創建子類,然后在子類中重寫對應的setter
方法來實現的,Aspects
支持對單個實例的hook原理與此有一些類似。圖示如下:具體說明請查看源碼中的注釋
// 執行hook static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { NSCParameterAssert(selector); // 針對實例類型,會通過runtime動態創建子類。類類型則直接hook。 Class klass = aspect_hookClass(self, error); ... } static Class aspect_hookClass(NSObject *self, NSError **error) { NSCParameterAssert(self); Class statedClass = self.class; Class baseClass = object_getClass(self); NSString *className = NSStringFromClass(baseClass); // 已經被hook過的類,直接返回 if ([className hasSuffix:AspectsSubclassSuffix]) { return baseClass; // 是元類(MetaClass),則代表是對類進行hook。(非單個實例) }else if (class_isMetaClass(baseClass)) { // 內部是將類的forwardInvocation:方法替換為__ASPECTS_ARE_BEING_CALLED__ return aspect_swizzleClassInPlace((Class)self); // 可能是一個KVO對象等情況,傳入實際的類型進行hook。 }else if (statedClass != baseClass) { return aspect_swizzleClassInPlace(baseClass); } // 單個實例的情況,動態創建子類進行hook. const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; Class subclass = objc_getClass(subclassName); if (subclass == nil) { subclass = objc_allocateClassPair(baseClass, subclassName, 0); if (subclass == nil) { NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); return nil; } // 內部是將類的forwardInvocation:方法替換為__ASPECTS_ARE_BEING_CALLED__ aspect_swizzleForwardInvocation(subclass); // 重寫class方法,返回之前的類型,而不是新創建的子類。避免hook后,類型判斷出現問題。 aspect_hookedGetClass(subclass, statedClass); aspect_hookedGetClass(object_getClass(subclass), statedClass); objc_registerClassPair(subclass); } object_setClass(self, subclass); return subclass; } 復制代碼
③替換forwardInvocation:
這部分就是把原有的forwardInvocation:
替換為自定義的實現:__ASPECTS_ARE_BEING_CALLED__
。源碼如下:
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; static void aspect_swizzleForwardInvocation(Class klass) { NSCParameterAssert(klass); // If there is no method, replace will act like class_addMethod. IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); if (originalImplementation) { class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); } AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); } 復制代碼
替換后的對應關系圖示如下:
④hook方法交換IMP:
圖示如下:
第③步和第④步可能有些同學會感到疑惑,為什么要替換forwardInvocation
以及為什么要將hook的方法的IMP
替換為_objc_msgForward
,這個和iOS的消息轉發機制
有關,可以自行查找相關資料,這里就不做說明了。需要注意的是有些框架也是通過iOS的消息發送機制來做一些操作,例如JSPatch
,使用的時候需要注意,避免發生沖突。
被hook方法的調用流程
當hook注入后,對hook方法進行調用時,調用流程就會發生變化。圖示如下:
從上述解析過程中,我們可以看到Aspects
這個框架是設計的很巧妙的,從中可以看到非常多runtime
知識的應用。但是作者並不推薦在實際項目中進行使用:
因為Apsects對類的底層進行了修改,這種修改是基礎方面的修改,需要考慮到各種場景和邊界問題,一旦某方面考慮不周,就會引發出一些未知問題。另外這個框架是有缺陷的,很久沒有進行更新了,我對它的已知問題點進行了總結,在下面進行說明。如果有未總結到位的,歡迎補充。
問題點
基於類的hooking,同一條繼承鏈條上的所有類,一個方法只能被hook一次,后hook的無效。
之前這樣會出現死循環,后面作者進行了修改,對這個行為進行了禁止
並加了錯誤提示。詳見這個issue
@interface A : NSObject - (void)foo; @end @implementation A - (void)foo { NSLog(@"%s", __PRETTY_FUNCTION__); } @end @interface B : A @end @implementation B - (void)foo { NSLog(@"%s", __PRETTY_FUNCTION__); [super foo]; // 導致死循環的代碼 } @end int main(int argc, char *argv[]) { [B aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) { NSLog(@"before -[B foo]"); }]; [A aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) { NSLog(@"before -[A foo]"); }]; B *b = [[B alloc] init]; [b foo]; // 調用后死循環 } 復制代碼
我們都知道,super
是從它的父類開始查找方法,然后傳入self
進行調用。 根據我們之前對源碼的解析,在這里調用[super foo]
后會從父類查找foo
的IMP
,查到后發現父類的IMP
已經被替換為_objc_msgForward
,然后傳入self
調用。 因為是傳入的self
,所以實際會調用到它自身的forwardInvocation:
,這樣就導致了死循環。
針對單個實例的hook,hook后使用kvo沒問題,使用kvo后hook會出現問題。
這里通過代碼進行說明,以Animal對象為例:
@interface Animal : NSObject @property(strong, nonatomic) NSString * name; @end @implementation Animal - (void)testKVO { [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; self.name = @"Animal"; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"observeValueForKeyPath keypath:%@ name:%@", keyPath, self.name); } - (void)dealloc { [self removeObserver:self forKeyPath:@"name"]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Animal *animal = [[Animal alloc] init]; [animal testKVO]; // 這里如果改為針對類進行hook,則不會存在問題,因為類hook修改的是Animal類,而實例hook修改的是NSKVONotifying_Animal類 [animal aspect_hookSelector:@selector(setName:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){ NSLog(@"aspects hook setName"); } error:nil]; // 這里會crash animal.name = @"ChangedAnimalName"; } } 復制代碼
異常原因分析圖示如下:
上面是繼承鏈和方法調用流程的圖示,可以看出,_NSSetObjectValueAndNotify
是被aspects__setName:
調用的,_NSSetObjectValueAndNotify
的內部實現邏輯是取調用它的selector
,去父類查找方法,即aspects__setName:
方法,而Animal
對象並沒有這個方法的實現,這就導致了crash。
與category的共存問題
先用aspects
進行hook,再使用category
進行hook,會導致crash。反之則沒有問題。樣例代碼如下:
@interface Animal : NSObject @property(strong, nonatomic) NSString * name; @end @implementation Animal - (void)setName:(NSString *)name { NSLog(@"%s", __func__); _name = name; } @end @interface Animal(hook) + (void)categoryHook; @end @implementation Animal(hook) + (void)categoryHook { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [super class]; SEL originalSelector = @selector(setName:); SEL swizzledSelector = @selector(lx_setName:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); }); } - (void)lx_setName:(NSString *)name { NSLog(@"%s", __func__); [self lx_setName:name]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Animal *animal = [[Animal alloc] init]; [Animal aspect_hookSelector:@selector(setName:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){ NSLog(@"aspects hook setName"); } error:nil]; [Animal categoryHook]; // 調用后crash:[Animal lx_setName:]: unrecognized selector sent to instance 0x100608dc0 animal.name = @"ChangedAnimalName"; } } 復制代碼
這個與__ASPECTS_ARE_BEING_CALLED__
的內部邏輯有關,里面會對調用的方法添加前綴aspect__
進行調用,以調用到原始的IMP
,但是category
hook后破壞了這個流程。圖示如下:
根據上述圖示,實際只有aspects__setName
,沒有aspects__lx_setName
,導致找不到方法而crash
基於類的hook,如果對同一個類同時hook類方法和實例方法,那么后hook的方法調用時會crash。樣例代碼如下:
@interface Animal : NSObject - (void)testInstanceMethod; + (void)testClassMethod; @end @implementation Animal - (void)testInstanceMethod { NSLog(@"%s", __func__); } + (void)testClassMethod { NSLog(@"%s", __func__); } @end int main(int argc, const char * argv[]) { @autoreleasepool { Animal *animal = [[Animal alloc] init]; [Animal aspect_hookSelector:@selector(testInstanceMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){ NSLog(@"aspects hook testInstanceMethod"); } error:nil]; [object_getClass([Animal class]) aspect_hookSelector:@selector(testClassMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){ NSLog(@"aspects hook testClassMethod"); } error:nil]; [animal testInstanceMethod]; // crash: "+[Animal testClassMethod]: unrecognized selector sent to class 0x1000114a0" [Animal testClassMethod]; } } 復制代碼
這樣的調用在日常開發中非常正常,但是它會導致crash。它是由於aspect_swizzleClassInPlace
方法中的邏輯缺陷導致的。
static Class aspect_swizzleClassInPlace(Class klass) { NSCParameterAssert(klass); // Animal類對象與Animal元類對象會得到同一個字符串。 NSString *className = NSStringFromClass(klass); NSLog(@"aspect_swizzleClassInPlace %@ %p", klass, object_getClass(klass)); _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { // 類對象和元類對象得到同一個className,這里后加入的會被錯誤的過濾掉。 if (![swizzledClasses containsObject:className]) { aspect_swizzleForwardInvocation(klass); [swizzledClasses addObject:className]; } }); return klass; } 復制代碼
從上述代碼可以看到,它的去重邏輯只是簡單的字符串判斷,取Animal的元類
和類
名得到同一個字符串Animal
,導致后添加的被過濾,當調用后被hook的方法后,執行_objc_msgForward
,因為后hook的aspect_swizzleForwardInvocation
被過濾了沒有執行,所以找不到forwardInvocation:
的IMP
,導致了crash。
_objc_msgForward會出現沖突的問題
內部是通過消息轉發機制來實現的,使用時要注意,避免與其它使用_objc_msgForward
或相關邏輯的框架發生沖突。
性能問題
hook后的方法,通過原有消息機制找到IMP
后,並不會直接調用。而是會進行消息轉發進入到__ASPECTS_ARE_BEING_CALLED__
方法,內部再通過key取出相應的Coantiner
進行調用,相對於未hook之前,額外增加了調用成本。所以不建議對頻繁調用的方法和在項目中大量使用。
線程問題
框架內部為了保證線程安全,有進行加鎖,但是使用的是自旋鎖OSSpinLock
,存在線程反轉的問題,在iOS10已經被標記為棄用。
對類方法的hook,需要使用object_getClass來獲取元類對象進行hook
這個不是框架問題,而是有些同學不知道如何對類方法
進行hook,這里進行說明。
@interface Animal : NSObject + (void)testClassMethod; @end // 需要通過object_getClass來獲取元類對象進行hook [object_getClass(Animal) aspect_hookSelector:@selector(testClassMethod) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){ NSLog(@"aspects hook setName"); } error:null];
https://juejin.cn/post/7025783407540076581