最近希望在業(yè)務(wù)中實(shí)現(xiàn)一套基于 AOP 的埋點(diǎn)方案,調(diào)研過程中,我花了些時(shí)間閱讀了一下 Aspects 的源碼,對于 Aspects 設(shè)計(jì)有了一些更深入的理解。因此,通過本文記錄我在閱讀源碼后的一些收獲和思考,以供后續(xù)進(jìn)行回顧。
概述
Aspects 是一款輕量且簡易的面向切面編程的框架,其基于 Objective-C Runtime 原理實(shí)現(xiàn)。Aspects 允許我們對 類的所有實(shí)例的實(shí)例方法 或 單個(gè)實(shí)例的實(shí)例方法 添加額外的代碼,并且支持設(shè)置代碼的執(zhí)行時(shí)機(jī),包括:before、instead、after 三種。
注意:Aspects 無法為類方法提供面向切面編程的能力。
| 對象類型 | 目標(biāo)方法類型 | Aspects 是否支持 hook | hook 效果 |
|---|---|---|---|
| 類對象(UIViewController) | 類方法(“+”開頭的方法) | 不支持 | - |
| 類對象(UIViewController) | 實(shí)例方法(“-”開頭的方法) | 支持 | 對類的所有實(shí)例對象生效 |
| 實(shí)例對象(vc) | 類方法(“+”開頭的方法) | 不支持 | - |
| 實(shí)例對象(vc) | 實(shí)例方法(“-”開頭的方法) | 支持 | 對單個(gè)實(shí)例對象生效 |
這里我們提出第一個(gè)問題:為什么 Aspects 僅支持對實(shí)例方法進(jìn)行 hook?
另一方面,Aspects 的作者在框架的 README 中明確表示不要在生產(chǎn)環(huán)境中使用 Aspects。這里我們提出第二個(gè)問題:在項(xiàng)目中使用 Aspects 進(jìn)行 hook 是否有什么坑?
基礎(chǔ)
Aspects 巧妙利用了 Objective-C 的消息傳遞和消息轉(zhuǎn)發(fā)機(jī)制,實(shí)現(xiàn)了一套與 KVO 類似的技術(shù)方案。為了能夠更加清晰地理解 Aspects 的設(shè)計(jì),這里我們簡單地回顧一下 Objective-C 的消息傳遞和消息轉(zhuǎn)發(fā)機(jī)制。
消息傳遞
Objective-C 是一門動(dòng)態(tài)語言,其 方法調(diào)用 在底層的實(shí)現(xiàn)是 消息傳遞(Message Passing)。本質(zhì)上,消息發(fā)送是 沿著一條引用鏈依次查找不同的對象,判斷該對象是否能夠處理消息。在 Objective-C 中,一切都是對象,包括類、元類,消息就是在這些對象之間進(jìn)行傳遞的。
因此,我們需要了解這些對象之間的關(guān)系。下圖所示,為 Objective-C 對象在內(nèi)存中的引用關(guān)系圖。
在 Objective-C 中,涉及消息傳遞的方法主要有兩種:實(shí)例方法、類方法。下面,我們來分別介紹。
實(shí)例方法
對于實(shí)例方法,消息傳遞時(shí),根據(jù)當(dāng)前實(shí)例對象的 isa 指針,找到其所屬的類對象,并在類對象的方法列表中查找。如果找到,則執(zhí)行;否則,根據(jù) superclass 指針,找到類對象的超類對象,并在超類對象的方法列表中查找,以此類推,如下所示。
類方法
雖然 Aspects 不支持 hook 類方法,但是為了方便進(jìn)行對照,這里我們也介紹一下類方法的查找。
對于類方法,消息傳遞時(shí),根據(jù)當(dāng)前類對象的 isa 指針,找到其所屬的元類對象,并在元類對象的方法列表中查找。如果找到,則執(zhí)行;否則,根據(jù) superclass 指針,找到元類對象的元超類對象,并在元超類對象的方法列表中查找,以此類推,如下所示。
消息轉(zhuǎn)發(fā)
如果消息傳遞無法找到可以處理消息的對象,那么,Objective-C runtime 將進(jìn)入消息轉(zhuǎn)發(fā)(Message Forwarding)。
消息轉(zhuǎn)發(fā)包含三個(gè)階段:
- 動(dòng)態(tài)消息解析
- 備用接收者
- 完整消息轉(zhuǎn)發(fā)
動(dòng)態(tài)消息解析
當(dāng)對象接收到未知消息時(shí),首先會(huì)調(diào)用所屬類的實(shí)例方法 + (BOOL)resolveInstanceMethod:(SEL)sel 或類方法 + (BOOL)resolveClassMethod:(SEL)sel。我們可以在方法內(nèi)部動(dòng)態(tài)添加一個(gè)“處理方法”,通過 class_addMethod 函數(shù)動(dòng)態(tài)添加到類中。比如:
void dynamicMethodIMP(id self, SEL _cmd) {
// 方法實(shí)現(xiàn)
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
備用接收者
如果上一步無法處理消息,則 runtime 會(huì)繼續(xù)調(diào)用 forwardingTargetForSelector: 方法。
如果一個(gè)對象實(shí)現(xiàn)了這個(gè)方法,并返回一個(gè)非 nil(也不能是 self) 的對象,則這個(gè)對象會(huì)作為消息的新接收者,消息會(huì)被分發(fā)到這個(gè)對象。比如:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString * selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"walk"]) {
return self.otherObject;
}
return [super forwardingTargetForSelector:aSelector];
}
這一步合適于我們只想將消息轉(zhuǎn)發(fā)到另一個(gè)能處理該消息的對象上。但這一步無法對消息進(jìn)行處理,如操作消息的參數(shù)和返回值。
完整消息轉(zhuǎn)發(fā)
如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。
這步調(diào)用 methodSignatureForSelector: 進(jìn)行方法簽名,這可以將函數(shù)的參數(shù)類型和返回值進(jìn)行封裝。如果返回 nil,則說明消息無法處理并報(bào)錯(cuò) unrecognized selector sent to instance。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){
return [NSMethodSignature signatureWithObjcTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
如果返回 methodSignature,則進(jìn)入 forwardInvocation。對象會(huì)創(chuàng)建一個(gè)表示消息的 NSInvocation 對象,把與尚未處理的消息有關(guān)的全部細(xì)節(jié)都封裝在 anInvocation 中,包括 selector,target,參數(shù)。在這個(gè)方法中可以修改實(shí)現(xiàn)方法,修改響應(yīng)對象等,如果方法調(diào)用成功,則結(jié)束。如果依然不能正確響應(yīng)消息,則報(bào)錯(cuò) unrecognized selector sent to instance。
- (void)forwardInvovation:(NSInvocation)anInvocation {
[anInvocation invokeWithTarget:_helper];
[anInvocation setSelector:@selector(run)];
[anInvocation invokeWithTarget:self];
}
核心原理
Aspects 的核心原理主要包括三個(gè)部分:
-
注冊關(guān)聯(lián)對象:在 hook 實(shí)例方法時(shí),均會(huì)注冊關(guān)聯(lián)對象
AspectsContainer。 - 創(chuàng)建動(dòng)態(tài)類:只有在 hook 實(shí)例對象的實(shí)例方法時(shí),才會(huì)創(chuàng)建動(dòng)態(tài)類。
- 核心方法交換:在 hook 實(shí)例方法時(shí),均會(huì)對核心方法的實(shí)現(xiàn)進(jìn)行交換。
注冊關(guān)聯(lián)對象
當(dāng) hook 實(shí)例方法時(shí),Aspects 會(huì)為 實(shí)例對象 或 類對象 注冊關(guān)聯(lián)對象 AspectsContainer。AspectsContainer 保存了用戶 hook 的目標(biāo)方法、執(zhí)行閉包、閉包參數(shù)、執(zhí)行時(shí)機(jī)等信息。下圖所示,為 AspectsContainer 引用關(guān)系圖。
關(guān)聯(lián)對象注冊的目標(biāo)分兩種情況,這種設(shè)計(jì)策略是有原因的:
- 在實(shí)例對象中注冊關(guān)聯(lián)對象,可以實(shí)現(xiàn)讓每個(gè)實(shí)例對象單獨(dú)管理 aspects,從而保證實(shí)例之間相互不影響。
- 在類對象中注冊關(guān)聯(lián)對象,可以實(shí)現(xiàn)讓類的每個(gè)實(shí)例對象共享 aspects,從而實(shí)現(xiàn)影響所有實(shí)例對象。
創(chuàng)建動(dòng)態(tài)類
當(dāng)且僅當(dāng) hook 實(shí)例對象的實(shí)例方法時(shí),Aspects 會(huì)為實(shí)例的所屬類 TestClass 創(chuàng)建一個(gè)子類 TestClass_Aspects_(同時(shí)創(chuàng)建對應(yīng)的元類),并修改實(shí)例的 isa 指針,使其指向 TestClass_Aspects_ 子類,同時(shí) hook TestClass_Aspects_ 的 class 方法,使其返回實(shí)例的所屬類 TestClass,如下圖所示。
整體的實(shí)現(xiàn)方式與 KVO 原理一致,尤其是修改動(dòng)態(tài)類 class 方法的實(shí)現(xiàn),使得在外部看來,實(shí)例的所屬類并沒有發(fā)生任何變化。
這里,我們可以思考一下第三個(gè)問題:為什么在 hook 實(shí)例對象的實(shí)例方法時(shí)要?jiǎng)?chuàng)建動(dòng)態(tài)類?
核心方法交換
當(dāng) hook 實(shí)例方法時(shí),最重要的一步是對 動(dòng)態(tài)創(chuàng)建的類對象(下文簡稱:動(dòng)態(tài)類對象) 或 原始繼承鏈中的類對象(下文簡稱:目標(biāo)類對象) 的兩個(gè)核心方法與 Aspects 提供的方法進(jìn)行交換。這兩個(gè)方法分別是:目標(biāo) selector 和 forwardInvocation:。具體的交換邏輯如下圖所示。
Aspects 會(huì)將目標(biāo) selector 的實(shí)現(xiàn)設(shè)置為 Aspects 提供的 aspect_getMsgForwardIMP 方法的返回值。aspects_getMsgForwardIMP 的返回值本質(zhì)上是一個(gè)能夠直接觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制的方法。更加特殊的地方在于,這里會(huì)直接進(jìn)入消息轉(zhuǎn)發(fā)的最后一步 forwardInvocation:。
與此同時(shí),Aspects 會(huì)將動(dòng)態(tài)類對象或目標(biāo)類對象的 forwardInvocation: 的實(shí)現(xiàn)設(shè)置為 Aspects 提供的 __ASPECTS_ARE_BEING_CALLED__ 方法實(shí)現(xiàn)。__ASPECTS_ARE_BEING_CALLED__ 內(nèi)部會(huì)從 實(shí)例對象 或 類對象 中取出關(guān)聯(lián)對象 AspectsContainer,并根據(jù)其所保存的 hook 信息執(zhí)行閉包和目標(biāo) selector 的原始實(shí)現(xiàn)。
注意:對于核心方法交換,Aspects 支持冪等。即如果對同一個(gè)實(shí)例方法 hook 多次,Aspects 會(huì)保證對這兩個(gè)方法只交換一次。
具體實(shí)現(xiàn)
下面,我們來通過源碼,具體分析一下 Aspects 中的設(shè)計(jì)細(xì)節(jié)。
數(shù)據(jù)結(jié)構(gòu)
首先,簡要介紹一下 Aspects 定義的數(shù)據(jù)結(jié)構(gòu),主要包括三種數(shù)據(jù)結(jié)構(gòu):
AspectsContainerAspectIdentifierAspectInfo
AspectsContainer
如下所示,為 AspectsContainer 的數(shù)據(jù)結(jié)構(gòu)定義。AspectsContainer 是 Aspects 所有信息的根容器,其包含了三個(gè)數(shù)組,用于保存三種類型的 AspectIdentifier。
-
beforeAspects:用于保存執(zhí)行時(shí)機(jī)為AspectPositionBefore的AspectIdentifier。 -
insteadAspects:用于保存執(zhí)行時(shí)機(jī)為AspectPositionInstead的AspectIdentifier。 -
afterAspects:用于保存執(zhí)行時(shí)機(jī)為AspectPositionAfter的AspectIdentifier。
除此之外,AspectsContainer 還提供了對于數(shù)組進(jìn)行增刪操作的方法。
// Tracks all aspects for an object/class.
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end
AspectIdentifer
如下所示,為 AspectIdentifier 的數(shù)據(jù)結(jié)構(gòu)定義。AspectIdentifier 是用于表示一個(gè) aspect 的相關(guān)信息,其包含了目標(biāo) selector、執(zhí)行閉包、閉包簽名、目標(biāo)對象、執(zhí)行時(shí)機(jī)等。
// Tracks a single aspect.
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end
AspectInfo
如下所示,為 AspectInfo 的數(shù)據(jù)結(jié)構(gòu)定義。AspectInfo 的作用是保存目標(biāo) selector 的原始實(shí)現(xiàn)的執(zhí)行環(huán)境。由于目標(biāo) selector 會(huì)被交換方法實(shí)現(xiàn),因此 originalInvocation 的 selector 其實(shí)是 Aspects 交換的 selector,即 aspects__SEL。
@interface AspectInfo : NSObject <AspectInfo>
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end
代碼流程
如下所示,Aspects 對外提供兩個(gè)接口,分別用于 hook 類方法和實(shí)例方法,即添加 aspect。
/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
兩者的內(nèi)部實(shí)現(xiàn)都只調(diào)用了同一個(gè)方法 aspect_add,其內(nèi)部實(shí)現(xiàn)邏輯如下所示。
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
NSCParameterAssert(self);
NSCParameterAssert(selector);
NSCParameterAssert(block);
__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
// 判斷是否允許 add aspect
// 如果允許,會(huì)順帶構(gòu)建 tracker 鏈。
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
// 加載或創(chuàng)建 container,每個(gè) selector 對應(yīng)一個(gè) container。
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
在 aspect_add 方法內(nèi)部實(shí)現(xiàn)中,首先通過 aspect_isSelectorAllowedAndTrack 方法判斷是否允許添加 aspect。如果允許,則初始化 AspectsContainer,并將其設(shè)置成實(shí)例對象或類對象的關(guān)聯(lián)對象。一個(gè) selector 對應(yīng)一個(gè) container,一個(gè)實(shí)例對象或類對象可包含多個(gè) container。最后通過 aspect_prepareClassAndHookSelector 執(zhí)行核心方法交換,對于實(shí)例對象,還會(huì)創(chuàng)建動(dòng)態(tài)類。
aspect_isSelectorAllowedAndTrack
Aspects 通過 aspect_isSelectorAllowedAndTrack 方法來判斷是否允許添加 aspect,如果允許則進(jìn)行追蹤。具體實(shí)現(xiàn)邏輯如下所示。
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
// part 1
// 靜態(tài)變量,作為黑名單
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
// 不允許添加 aspect 的方法黑名單
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});
// 檢查方法是否屬于黑名單
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}
// 對于 dealloc 方法,只允許在 before 階段進(jìn)行 hook
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;
}
// 不能 hook 不存在的實(shí)例方法
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;
}
// part 2
// 如果 hook 目標(biāo)是類對象,必須保證類繼承鏈上,只允許對一個(gè)方法進(jìn)行一次 hook
if (class_isMetaClass(object_getClass(self))) {
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
// 檢查繼承鏈中是否 hook 過目標(biāo)類方法
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
// 依次遍歷超類直到根類,根據(jù)類對應(yīng)的 track 進(jìn)行判斷
// 如果類方法已經(jīng) hook 過,則進(jìn)一步判斷
if ([tracker.selectorNames containsObject:selectorName]) {
if (tracker.parentEntry) {
// 如果父類中 hook 過,則不允許 hook
// 同時(shí)查找最頂層 tracker,打印日志
AspectTracker *topmostEntry = tracker.parentEntry;
while (topmostEntry.parentEntry) {
topmostEntry = topmostEntry.parentEntry;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
} else if (klass == currentClass) {
// 如果當(dāng)前類的方法已經(jīng) hook 過,則允許 hook
return YES;
}
}
} while ((currentClass = class_getSuperclass(currentClass)));
// 如果繼承鏈上沒有類對目標(biāo)方法hook過,則允許 hook,并記錄 tracker
// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}
[tracker.selectorNames addObject:selectorName];
// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
}
return YES;
}
aspect_isSelectorAllowedAndTrack 的內(nèi)部邏輯可以分為兩部分:方法黑名單檢查、對象類型檢查。
對于方法黑名單檢查,可細(xì)分為三個(gè)步驟:
- 判斷目標(biāo)方法是不是
retain、release、autorelease等,如果是,則不允許 hook。 - 如果目標(biāo)方法是
dealloc,則只允許 hookbefore時(shí)機(jī),其他時(shí)機(jī),則不允許 hook。 - 進(jìn)一步確認(rèn) hook 的目標(biāo)方法是否存在,如果不存在,則不允許 hook。
對于對象類型檢查,如果對象類型是實(shí)例對象,則允許 hook。如果對象類型是類對象,則進(jìn)一步判斷。根據(jù)目標(biāo)類對象,遍歷繼承鏈,對于繼承鏈中的每一個(gè)類對象,從全局字典 swizzledClassesDict 中讀取對應(yīng)的追蹤器 AspectTracker。根據(jù)追蹤器的記錄,我們可以處理兩種情況:
- 如果目標(biāo)類沒有 hook 過目標(biāo)方法,但其父類 hook 過,則不允許 hook。
- 如果目標(biāo)類 hook 過父類方法,但其子類沒有 hook 過,則允許 hook。
如下圖所示,為追蹤器工作原理示意圖。
當(dāng)對 SubClass 類對象 hook 實(shí)例方法 SEL01 時(shí),Aspects 會(huì)從 SubClass 類對象開始,遍歷其繼承鏈,讀取繼承鏈上的每一個(gè)類對象所對應(yīng)的追蹤器(如果沒有則創(chuàng)建),將目標(biāo)方法 SEL01 保存至其內(nèi)部的 selectorNames 數(shù)組中作為記錄。
后續(xù),如果對 Class 類對象 hook 實(shí)例方法 SEL01 時(shí),由于其子類 SubClass 已經(jīng) hook 過同名方法,則不允許 Class 對其再次 hook。根據(jù)消息傳遞的原理,對 Class 進(jìn)行 hook 是不會(huì)生效的,因?yàn)樽宇?SubClass 會(huì)在消息傳遞鏈中提前返回 SEL01。所以,Aspects 的設(shè)計(jì)不允許在這種情況下再次 hook 同名方法。
當(dāng)然,如果對 Class 類對象 hook 實(shí)例方法 SEL02 時(shí),由于所有其子類均沒有 hook 過同名方法,因此允許 Class 對其再次 hook。
本質(zhì)上,Aspects 利用了 正向的類對象繼承鏈 和 反向的追蹤器鏈,通過全局字典 swizzledClassDict 進(jìn)行綁定,形成了一個(gè)雙向鏈表,便于判斷是否允許對類對象的實(shí)例方法進(jìn)行 hook。
aspect_prepareClassAndHookSelector
如下所示,為 aspect_prepareClassAndHookSelector 的實(shí)現(xiàn)邏輯。
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
// Aspects_Class_
Class klass = aspect_hookClass(self, error);
// 讀取 Aspects_Class_ 的 selector 方法
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// IMP 不能是 _objc_msgForward 或 _objc_msgForward_stret
const char *typeEncoding = method_getTypeEncoding(targetMethod);
// aspects__SEL
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
// 如果不存在 aspects__SEL,即沒有被交換過,則新增一個(gè) aspects__SEL 方法,其實(shí)現(xiàn)指向 selector IMP
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}
// 將 selector 方法的實(shí)現(xiàn)指向 _objc_msgForward,從而直接觸發(fā)消息轉(zhuǎn)發(fā)
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
其中 aspect_hookClass 將判斷對象類型,如果是實(shí)例對象,則創(chuàng)建一個(gè)動(dòng)態(tài)類對象返回;如果是類對象,則返回對應(yīng)的類對象。
基于 aspect_hookClass 返回的對象,Aspects 將修改該對象的兩個(gè)方法,使其指向 Aspects 的兩個(gè)方法實(shí)現(xiàn),即上述我們介紹的 核心方法交換。
在 aspect_prepareClassAndHookSelector 的實(shí)現(xiàn)中,Aspects 會(huì)在進(jìn)行方法交換之前進(jìn)行檢查,避免重復(fù)交換,從而實(shí)現(xiàn)冪等。
aspect_hookClass
如下所示,為 aspect_hookClass 的實(shí)現(xiàn)邏輯。
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class; // 其所聲明的類
Class baseClass = object_getClass(self);// isa
NSString *className = NSStringFromClass(baseClass);
if ([className hasSuffix:AspectsSubclassSuffix]) {
// 如果是實(shí)例對象,且實(shí)例對象已經(jīng) hook 過方法,即其 isa 指向的是動(dòng)態(tài)類對象 Aspects_Class_,則直接復(fù)用動(dòng)態(tài)類對象
return baseClass;
} else if (class_isMetaClass(baseClass)) {
// 如果是類對象,則將該類對象 forwardInvocation: 的實(shí)現(xiàn)設(shè)置為 _aspects_forwardInvocation:。
// 方法交換完成后,返回該類對象。
return aspect_swizzleClassInPlace((Class)self);
} else if (statedClass != baseClass) {
// 如果是被 KVO 的實(shí)例對象。
// baseClass 為 KVO 所創(chuàng)建的動(dòng)態(tài)類,則直接對 KVO 創(chuàng)建的動(dòng)態(tài)類對象進(jìn)行方法交換,交換 forwardInvocation: 與 _aspects_forwardInvocation: 的方法實(shí)現(xiàn)。
return aspect_swizzleClassInPlace(baseClass);
}
// 如果是實(shí)例對象,且實(shí)例對象未 hook 過方法,則創(chuàng)建動(dòng)態(tài)子類 Aspects_Class_
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;
}
// 將動(dòng)態(tài)類對象的 forwardInvocation: 與 _aspects_forwardInvocation: 進(jìn)行方法交換
aspect_swizzleForwardInvocation(subclass);
// 將動(dòng)態(tài)類對象的 class 設(shè)置成 statedClass
aspect_hookedGetClass(subclass, statedClass);
// 將動(dòng)態(tài)類對象的元類的 class 設(shè)置成 stateClass
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
// 設(shè)置 isa 指針,指向 subclass
object_setClass(self, subclass);
return subclass;
}
aspect_hookClass 方法主要用于 選擇對哪個(gè)對象的目標(biāo)方法執(zhí)行 hook,這里面包含了 4 種具體的情況,依次為:
- 如果目標(biāo)對象是實(shí)例對象,且實(shí)例對象曾經(jīng) hook 過方法,則直接返回已創(chuàng)建的動(dòng)態(tài)類對象。
- 如果目標(biāo)對象是類對象,則對類對象的
forwardInvocation:方法的實(shí)現(xiàn)設(shè)置為 Aspects 提供的_aspects_forwardInvocation:,并返回該類對象。 - 如果目標(biāo)對象是被 KVO 的對象,則直接復(fù)用 KVO 所創(chuàng)建的動(dòng)態(tài)類,并對動(dòng)態(tài)類對象的
forwardInvocation:方法的實(shí)現(xiàn)設(shè)置為 Aspects 提供的_aspects_forwardInvocation:,并返回 KVO 的動(dòng)態(tài)類對象。 - 如果目標(biāo)對象是實(shí)例對象,且實(shí)例對象沒有 hook 過方法,則創(chuàng)建一個(gè)動(dòng)態(tài)類對象
Aspects_Class_,同時(shí)包括元類對象,并對動(dòng)態(tài)類對象的forwardInvocation:方法執(zhí)行方法交換,并且設(shè)置動(dòng)態(tài)類與原始類之間的關(guān)系,最終返回動(dòng)態(tài)類對象。
相關(guān)問題
本節(jié),我們將來介紹上文所提出的幾個(gè)問題。
問題一:為什么 Aspects 僅支持對實(shí)例方法進(jìn)行 hook?
在 Aspects 的實(shí)現(xiàn)中,在判斷能夠添加 aspect 的邏輯中,會(huì)通過 aspect_isCompatibleBlockSignature 方法來判斷 block 與 selector 的方法簽名是否匹配,如下所示。其中,它會(huì)通過類的 instanceMethodSignatureForSelector 方法獲取 selector 的方法簽名。對于類方法,通過這種方式必然返回 nil,從而導(dǎo)致判斷條件無法滿足,因此無法 hook 類方法。
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
NSCParameterAssert(blockSignature);
NSCParameterAssert(object);
NSCParameterAssert(selector);
BOOL signaturesMatch = YES;
// 對于類方法,通過 instanceMethodSignatureForSelector: 讀取必然返回 nil
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
} else {
...
}
...
}
問題二:在項(xiàng)目中使用 Aspects 進(jìn)行 hook 是否有什么坑?
如果我們真正理解了 Aspects 的設(shè)計(jì)原理,很容易明白為什么作者不推薦在生產(chǎn)環(huán)境中使用 Aspects。事實(shí)上,在實(shí)際的項(xiàng)目開發(fā)中,我們經(jīng)常會(huì)用到對已有方法進(jìn)行 hook。當(dāng)然,我們可以保證自己寫的代碼只使用 Aspects 進(jìn)行 hook,但是我們無法確定引入的第三方庫是否使用其他方式對方法進(jìn)行 hook。那么,這時(shí)候埋下了未知的風(fēng)險(xiǎn)。
如上圖所示,假如我們對 SEL 與 bcq_SEL 進(jìn)行了 swizzle。那么,bcq_SEL 的實(shí)現(xiàn)將指向 SEL 的實(shí)現(xiàn) aspect_getMsgForwardIMP;SEL 的實(shí)現(xiàn)將指向 bcq_SEL 的實(shí)現(xiàn) bcq_IMP。
在有些情況下,比如:hook viewWillAppear: 方法。bcq_IMP 里會(huì)再次調(diào)用 bcq_SEL,從而再次調(diào)用原始實(shí)現(xiàn)。這時(shí)候,我們調(diào)用 SEL,它最終仍然會(huì)調(diào)用 aspect_getMsgForwardIMP,Aspects 的設(shè)置不受影響。
但是有些情況下,bcq_IMP 的內(nèi)部邏輯可能只在特定條件下調(diào)用原始實(shí)現(xiàn),其他條件下調(diào)用自定義實(shí)現(xiàn)。這時(shí)候,我們調(diào)用 SEL,在某些條件下將不會(huì)觸發(fā) aspect_getMsgForwardIMP,最終導(dǎo)致 Aspects 的設(shè)置不生效。
顯而易見,在生產(chǎn)環(huán)境在使用 Aspects 的確可能會(huì)出現(xiàn)不確定的異常問題。因此,作者不建議我們在生產(chǎn)環(huán)境中使用 Aspects。
問題三:為什么在 hook 實(shí)例對象的實(shí)例方法時(shí)要?jiǎng)?chuàng)建動(dòng)態(tài)類?
對于實(shí)例對象的實(shí)例方法,我們顯然不能直接 hook 繼承鏈中的類對象,否則將影響類的所有實(shí)例的實(shí)例方法。因此,Aspects 選擇了一種類似于 KVO 的設(shè)計(jì),動(dòng)態(tài)創(chuàng)建一個(gè)子類,并將實(shí)例對象的 isa 指針指向動(dòng)態(tài)子類。動(dòng)態(tài)子類的 class 方法則指向?qū)嵗龑ο蟮穆暶黝?,從而是外部看來沒有任何變化。
這種做法,為實(shí)例對象單獨(dú)開辟了一條繼承鏈分支,如下圖所示。只有被 hook 的實(shí)例對象才會(huì)走這條分支繼承鏈,因此不影響其他實(shí)例。
如果對同一個(gè)類的多個(gè)實(shí)例進(jìn)行 Aspects,那么會(huì)怎么樣?從上圖中,我們也能猜到,Aspects 會(huì)復(fù)用動(dòng)態(tài)子類。只不過 hook 的閉包由各個(gè)實(shí)例對象自己管理而已。
總結(jié)
通過分析 Aspects 的源碼及其設(shè)計(jì)原理,我們同時(shí)加深了對于 Objective-C Runtime 的理解。從中,我們也了解到 Aspects 的局限性,引入需謹(jǐn)慎。
在 Aspects 中,我們看到了很多 Objective-C 的黑魔法 API,比如:
-
_objc_msgForward/_objc_msgForward_stret:直接觸發(fā)forwardInvocation: -
objc_allocateClassPair:動(dòng)態(tài)創(chuàng)建類對象和元類對象 -
objc_registerClassPair:注冊類對象和元類對象 -
object_setClass:設(shè)置isa指針指向。
除此之外,Aspects 使用了非常底層的方式實(shí)現(xiàn)了閉包的參數(shù)檢查與匹配,這一塊非常值得我們深入學(xué)習(xí),后續(xù)有機(jī)會(huì)我們再來研究一下。
最后,向作者表達(dá)一下敬意!如果對 Objective-C 底層原理沒有如此深刻的理解,一般人是寫不出來這樣的框架的!