Aspects框架的原始碼解讀及問題解析
前言
在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__
- 第四步是將原有方法的
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
------------------越是喧囂的世界,越需要寧靜的思考------------------
合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。
積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千里;不積小流,無以成江海。騏驥一躍,不能十步;駑馬十駕,功在不捨。鍥而舍之,朽木不折;鍥而不捨,金石可鏤。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鱔之穴無可寄託者,用心躁也。