Aspects框架的源碼解讀及問題解析


前言

在iOS日常開發中,對某些方法進行hook是很常見的操作。最常見的是使用Category在+load中進行方法swizzle,它是針對類的,會改變這個類所有實例的行為。但是有時候我們只想針對單個實例進行hook,這種方法就顯得無力了。而Aspects框架可以搞定這個問題。 它的原理是通過Runtime動態的創建子類,把實例的isa指針指向新創建的子類,然后在子類中對hook的方法進行處理,這樣就支持了對單個實例的hook。Aspects框架支持對類和實例的hook,API很易用,可以方便的讓你在任何地方進行hook,是線程安全的。但是Aspects框架也有一些缺陷,一不小心就會掉坑里面,我會通過源碼解析進行說明。

源碼解析

我主要使用圖示對Aspects的源碼進行說明,建議參考源碼一起查看。要看懂這些內容,需要對isa指針消息轉發機制runtime有一定的了解,本文中不會對這些內容展開來講,因為要把這些東西講清楚,每一項都需要單獨寫一篇文章了。

主要流程解析

  1. 它第一個流程是使用關聯對象添加Container,在這個過程中會進行一些前置條件的判斷,例如這個方法是否支持被hook等,如果條件驗證通過,就會把這次hook的信息保存起來,在方法調用的時候,查詢出來使用。
  2. 第二個流程是動態創建子類,如果是針對類的hook,則不會走這一步。
  3. 第三步是替換這個類的forwardInvocation:方法為__ASPECTS_ARE_BEING_CALLED__,這個方法內部會查找到之前創建的Container,然后根據Container中的邏輯進行實際的調用。
  4. 第四步是將原有方法的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; } } 復制代碼
  1. 從源碼中可以看到,不支持的hook方法有[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];。其中retainreleaseautorelease在arc下是被禁用的,框架本身是hookforwardInvocation:進行實現的,所以對它的hook也不支持。
  2. dealloc只支持AspectPositionBefore類型,使用AspectPositionInstead會導致系統默認的dealloc操作被替換無法執行而出現問題。 AspectPositionAfter類型,調用時對象可能已經已經被釋放了,從而引發野指針錯誤。
  3. Aspects禁止有繼承關系的類hook同一個方法,具體可以參見它的一個issue,它報告了這樣操作會導致死循環,我會在文章后面再進行說明。
  4. 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]后會從父類查找fooIMP,查到后發現父類的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM