轉(zhuǎn)摘自面向切面編程
1. 背景
最近在做項目的打點統(tǒng)計的時候,發(fā)現(xiàn)業(yè)務(wù)邏輯和打點邏輯經(jīng)常耦合在一起,這樣一方面影響了正常的業(yè)務(wù)邏輯,同時也很容易搞亂打點邏輯,而且要查看打點情況的時候也很分散,因此想著如何將兩者解耦,并將打點邏輯集中起來。其實在 web 編程時候,這種場景很早就有了很成熟的方案,也就是所謂的 aop 編程(面向切面編程),其原理也就是在不更改正常的業(yè)務(wù)處理流程的前提下,通過生成一個動態(tài)代理類,從而實現(xiàn)對目標(biāo)對象嵌入附加的操作。
在 iOS 中,要想實現(xiàn)相似的效果也很簡單,利用 OC 的動態(tài)性,通過 swizzling method 改變目標(biāo)函數(shù)的 selector 所指向的實現(xiàn),然后在新的實現(xiàn)中實現(xiàn)附加的操作,完成之后再回到原來的處理邏輯。想明白這些之后,我就打算動手實現(xiàn),當(dāng)然并沒有重復(fù)造輪子,我在 github 發(fā)現(xiàn)了一個基于 swizzling method 的開源框架?Aspects?。這個庫的代碼量比較小,總共就一個類文件,使用起來也比較方便,比如你想統(tǒng)計某個 controller 的 viewwillappear 的調(diào)用次數(shù),你只需要引入 Aspect.h 頭文件,然后在合適的地方初始化如下代碼即可。
- (void)addKvLogAspect {
[selfwr_Aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{
? ? ? ?KVLog_ReviewTimeline(ReviewTimeline_Open_Tab);
}error:NULL];
}
這篇文章主要是介紹 Aspects 源碼以及其思路,以及我在實際應(yīng)用中遇到的一些問題。對 swizzling method 不了解的同學(xué)可以先去網(wǎng)上了解一下,下面的內(nèi)容是基于大家對 swizzling method 有一定的了解的基礎(chǔ)上的。
2. 基本原理
我們知道 OC 是動態(tài)語言,我們執(zhí)行一個函數(shù)的時候,其實是在發(fā)一條消息:[receiver message],這個過程就是根據(jù) message 生成 selector,然后根據(jù) selector 尋找指向函數(shù)具體實現(xiàn)的指針 IMP,然后找到真正的函數(shù)執(zhí)行邏輯。這種處理流程給我們提供了動態(tài)性的可能,試想一下,如果在運(yùn)行時,動態(tài)的改變了 selector 和 IMP 的對應(yīng)關(guān)系,那么就能使得原來的[receiver message]進(jìn)入到新的函數(shù)實現(xiàn)了。
那么具體怎么實現(xiàn)這樣的動態(tài)替換了?
直觀的一種方案是提供一個統(tǒng)一入口,如 commonImp ,將所有需要 hook 的函數(shù)都指向這個函數(shù),然后在這里,提取相關(guān)信息進(jìn)行轉(zhuǎn)發(fā),JSPatch 實現(xiàn)原理詳解對此方案的可行性有進(jìn)行分析,對于64位機(jī)器可能會有點問題。另外一個方法就是利用 oc 自己的消息轉(zhuǎn)發(fā)機(jī)制進(jìn)行轉(zhuǎn)發(fā),Aspects 的大體思路,基本上是順著這個來的。為了更好的解釋這個過程,我們先來看一下消息具體是怎么找到對應(yīng)的 imp 的,見下圖(此圖并非原創(chuàng))。?
從上面我們可以發(fā)現(xiàn),在發(fā)消息的時候,如果 selector 有對應(yīng)的 IMP ,則直接執(zhí)行,如果沒有,oc 給我們提供了幾個可供補(bǔ)救的機(jī)會,依次有?resolveInstanceMethod?、forwardingTargetForSelector、forwardInvocation。Aspects 之所以選擇在?forwardInvocation?這里處理是因為,這幾個階段特性都不太一樣:resolvedInstanceMethod?適合給類/對象動態(tài)添加一個相應(yīng)的實現(xiàn),forwardingTargetForSelector?適合將消息轉(zhuǎn)發(fā)給其他對象處理,相對而言,forwardInvocation?是里面最靈活,最能符合需求的。因此 Aspects 的方案就是,對于待 hook 的 selector,將其指向?objc_msgForward?/?_objc_msgForward_stret?,同時生成一個新的?aliasSelector?指向原來的 IMP,并且 hook 住?forwardInvocation?函數(shù),使他指向自己的實現(xiàn)。按照上面的思路,當(dāng)被 hook 的 selector 被執(zhí)行的時候,首先根據(jù) selector 找到了?objc_msgForward?/?_objc_msgForward_stret?,而這個會觸發(fā)消息轉(zhuǎn)發(fā),從而進(jìn)入?forwardInvocation。同時由于?forwardInvocation?的指向也被修改了,因此會轉(zhuǎn)入新的?forwardInvocation?函數(shù),在里面執(zhí)行需要嵌入的附加代碼,完成之后,再轉(zhuǎn)回原來的 IMP。
3. 源碼分析
介紹完大致思路之后,下面將從代碼層來來具體分析。從頭文件中可以看到使用aspects有兩種使用方式:1)類方法 2)實例方法
+ (id)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)aspect_hookSelector:(SEL)selector? ?withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError**)error;
兩者的主要原理基本差不多,這里不做一一介紹,只是以實例方法為例進(jìn)行說明。在介紹之前,先介紹里面幾個重要的數(shù)據(jù)結(jié)構(gòu):
typedefNS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter? =0,/// Called after the original implementation (default)
AspectPositionInstead =1,/// Will replace the original implementation.
AspectPositionBefore? =2,/// Called before the original implementation.
AspectOptionAutomaticRemoval =1<<3/// Will remove the hook after the first execution.
};
這里表示了 block 執(zhí)行的時機(jī),也就是額外操作的執(zhí)行時機(jī),在我的應(yīng)用場景中就是打點邏輯的執(zhí)行時機(jī),它可以在原始函數(shù)執(zhí)行之前,也可以是執(zhí)行之后,甚至可以完全替換掉原來的邏輯。
一個對象或者類的所有的 Aspects 整體情況
// Tracks all aspects for an object/class.
@interfaceAspectsContainer: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
AspectIdentifier
一個 Aspect 的具體內(nèi)容
@interfaceAspectIdentifier:NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError**)error;
- (BOOL)invokeWithInfo:(id)info;
@property(nonatomic,assign) SEL selector;
@property(nonatomic,strong)idblock;
@property(nonatomic,strong)NSMethodSignature*blockSignature;
@property(nonatomic,weak)idobject;
@property(nonatomic,assign) AspectOptions options;
@end
這里主要包含了單個的 aspect 的具體信息,包括執(zhí)行時機(jī),要執(zhí)行 block 所需要用到的具體信息:包括方法簽名、參數(shù)等等
AspectInfo
一個 Aspect 執(zhí)行環(huán)境,主要是 NSInvocation 信息。
@interfaceAspectInfo:NSObject
- (id)initWithInstance:(__unsafe_unretainedid)instance invocation:(NSInvocation*)invocation;
@property(nonatomic,unsafe_unretained,readonly)idinstance;
@property(nonatomic,strong,readonly)NSArray*arguments;
@property(nonatomic,strong,readonly)NSInvocation*originalInvocation;
@end
3.2 代碼流程
有了上面的了解,我們就能更好的分析整個 apsects 的執(zhí)行流程。添加一個 aspect 的關(guān)鍵流程如下圖所示:
從代碼來看,要想使用 aspects ,首先要添加一個 aspect ,可以通過上面介紹的類/實例方法。關(guān)鍵代碼實現(xiàn)如下:
staticidaspect_add(idself, SEL selector, AspectOptions options,idblock,NSError**error) {
? ? ...
__block AspectIdentifier *identifier =nil;
? ? aspect_performLocked(^{
if(aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {//1判斷能否hook
...//2 記錄數(shù)據(jù)結(jié)構(gòu)
aspect_prepareClassAndHookSelector(self, selector, error);//3 swizzling
? ? ? ? }
? ? });
returnidentifier;
}
這個過程基本和上面的流程圖一致,這里重點介紹幾個關(guān)鍵部分。
對于對象實例而言,這里主要是根據(jù)黑名單,比如 retain forwardInvocation 等這些方法在外部是不能被 hook ,(對于類對象還要確保同一個類繼承關(guān)系層級中,只能被 hook 一次,因此這里需要判斷子類,父類有沒有被 hook,之所以做這樣的實現(xiàn),主要是為了避免出現(xiàn)死循環(huán)的出現(xiàn),這里有相關(guān)的討論)。如果能夠 hook,則繼續(xù)下面的步驟。
這是真正的核心邏輯,swizzling method 主要有兩部分,一個是對對象的 forwardInvocation 進(jìn)行 swizzling,另一個是對傳入的 selector 進(jìn)行 swizzling.
staticvoidaspect_prepareClassAndHookSelector(NSObject*self, SEL selector,NSError**error) {
Class klass = aspect_hookClass(self, error);//1? swizzling forwardInvocation
? ? Method targetMethod = class_getInstanceMethod(klass, selector);
? ? IMP targetMethodIMP = method_getImplementation(targetMethod);
if(!aspect_isMsgForwardIMP(targetMethodIMP)) {//2? swizzling method
...//
? ? }
}
3.2.2.1 swizzling forwardInvocation:
aspect_hookClass 函數(shù)主要 swizzling 類/對象的 forwardInvocation 函數(shù),aspects 的真正的處理邏輯都是在 forwradInvocation 函數(shù)里面進(jìn)行的。對于對象實例而言,源代碼中并沒有直接 swizzling 對象的 forwardInvocation 方法,而是動態(tài)生成一個當(dāng)前對象的子類,并將當(dāng)前對象與子類關(guān)聯(lián),然后替換子類的 forwardInvocation 方法(這里具體方法就是調(diào)用了 object_setClass(self, subclass) ,將當(dāng)前對象 isa 指針指向了 subclass ,同時修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回當(dāng)前對象的 class。,這個地方特別繞,它的原理有點類似 kvo 的實現(xiàn),它想要實現(xiàn)的效果就是,將當(dāng)前對象變成一個 subclass 的實例,同時對于外部使用者而言,又能把它繼續(xù)當(dāng)成原對象在使用,而且所有的 swizzling 操作都發(fā)生在子類,這樣做的好處是你不需要去更改對象本身的類,也就是,當(dāng)你在 remove aspects 的時候,如果發(fā)現(xiàn)當(dāng)前對象的 aspect 都被移除了,那么,你可以將 isa 指針重新指回對象本身的類,從而消除了該對象的 swizzling ,同時也不會影響到其他該類的不同對象)。對于每一個對象而言,這樣的動態(tài)對象只會生成一次,這里 aspect_swizzlingForwardInvocation 將使得 forwardInvocation 方法指向 aspects 自己的實現(xiàn)邏輯 ,具體代碼如下:
staticClass aspect_hookClass(NSObject*self,NSError**error) {
? ? ...
//生成動態(tài)子類,并swizzling forwardInvocation方法
subclass = objc_allocateClassPair(baseClass, subclassName,0);
aspect_swizzleForwardInvocation(subclass);//swizzling forwardinvation方法
? ? objc_registerClassPair(subclass);
? ? ? ...
object_setClass(self, subclass);//將當(dāng)前self設(shè)置為子類,這里其實只是更改了self的isa指針而已
returnsubclass;
}
...
staticvoidaspect_swizzleForwardInvocation(Class klass) {
? ? ...
IMP originalImplementation = class_replaceMethod(klass,@selector(forwardInvocation:),? ? (IMP)__ASPECTS_ARE_BEING_CALLED__,"v@:@");
if(originalImplementation) {
class_addMethod(klass,NSSelectorFromString(AspectsForwardInvocationSelectorName),? ? ? ? originalImplementation,"v@:@")
? ? ? }
...
}
由于子類本身并沒有實現(xiàn) forwardInvocation ,隱藏返回的 originalImplementation 將為空值,所以也不會生成 NSSelectorFromString(AspectsForwardInvocationSelectorName) 。
當(dāng) forwradInvocation 被 hook 之后,接下來,將對傳入的 selector 進(jìn)行 hook ,這里的做法是,將 selector 指向了轉(zhuǎn)發(fā) IMP ,同時生成一個 aliasSelector ,指向了原來的 IMP ,同時為了放在重復(fù) hook ,做了一個判斷,如果發(fā)現(xiàn) selector 已經(jīng)指向了轉(zhuǎn)發(fā) IMP ,那就就不需要進(jìn)行交換了,代碼如下
staticvoidaspect_prepareClassAndHookSelector(NSObject*self, SEL selector,NSError**error) {
? ? ...
? ? Method targetMethod = class_getInstanceMethod(klass, selector);
? ? IMP targetMethodIMP = method_getImplementation(targetMethod);
if(!aspect_isMsgForwardIMP(targetMethodIMP)) {
? ? ...
SEL aliasSelector = aspect_aliasForSelector(selector);//generator aliasSelector
if(![klass instancesRespondToSelector:aliasSelector]) {
__unusedBOOLaddedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
? ? }
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);// point to? _objc_msgForward
? ...
? ? }
}
3.2.3 handle ForwardInvocation
基于上面的代碼分析知道,轉(zhuǎn)發(fā)最終的邏輯代碼最終轉(zhuǎn)入?__ASPECTS_ARE_BEING_CALLED__函數(shù)的處理中。這里,需要處理的部分包括額外處理代碼(如打點代碼)以及最終重新轉(zhuǎn)會原來的 selector 所指向的函數(shù),其實現(xiàn)代碼如下:
staticvoid__ASPECTS_ARE_BEING_CALLED__(__unsafe_unretainedNSObject*self, SEL selector,NSInvocation*invocation) {
...
// Before hooks.? 原來邏輯之前執(zhí)行
? ? aspect_invoke(classContainer.beforeAspects, info);
? ? aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOLrespondsToAlias =YES;
if(objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {//是否需要替換掉原來的路基
? ? ? ? aspect_invoke(classContainer.insteadAspects, info);
? ? ? ? aspect_invoke(objectContainer.insteadAspects, info);
}else{
? ? ? ? Class klass = object_getClass(invocation.target);
do{
if((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];//根據(jù)aliasSelector找到原來的邏輯并執(zhí)行
break;
? ? ? ? ? ? ? ? }
}while(!respondsToAlias && (klass = class_getSuperclass(klass)));
? ? }
// After hooks.? 原來邏輯之后執(zhí)行
? ? aspect_invoke(classContainer.afterAspects, info);
? ? aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if(!respondsToAlias) {//找不到aliasSelector的IMP實現(xiàn),沒有找到原來的邏輯,進(jìn)行消息轉(zhuǎn)發(fā)
? ? ? ? ? invocation.selector = originalSelector;
SEL originalForwardInvocationSEL =NSSelectorFromString(AspectsForwardInvocationSelectorName);
if([selfrespondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL,NSInvocation*))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else{
[selfdoesNotRecognizeSelector:invocation.selector];
? ? ? ? }
? ? }? ? ? ? ? ? ? ? ? ?
...
}
依次處理 before/instead/after hook 以及真正函數(shù)實現(xiàn)。如果沒有找到原始的函數(shù)實現(xiàn),還需要進(jìn)行轉(zhuǎn)發(fā)操作。?
以上就是 Apsects 的實現(xiàn)了,接下來會介紹在實際應(yīng)用過程中遇到的一些問題以及我的解決方案。
我們的項目中引入了 JSPatch 作為我們的 hot fix方案。 JSPatch 也會 hook 住對象的 forwradInvocation 方法,并且 swizzling 相應(yīng)的 method ,使其指向轉(zhuǎn)發(fā) IMP ,由于 aspects 也是基于這兩者實現(xiàn)的,那么會不會導(dǎo)致問題呢(其實類似的問題也會發(fā)生在對象提前被 kvo 了,會不會有影響)?
回過頭去看3.2.1 我們先是 hook了 類的?forwardInvocation?使其指向了?__ASPECTS_ARE_BEING_CALLED__,然后在 swizzling method 那里,aspect 有做一個判斷,如果傳入的 selector 指向了轉(zhuǎn)發(fā) IMP ,那么我們什么也不做。因此可想而知,如果傳入的 selector 先被 JSPatch hook ,那么,這里我們將不會再處理,也就不會生成 aliasSelector 。
這會導(dǎo)致什么問題了?設(shè)想一下,當(dāng) selector 被觸發(fā)的時候,由于 selector 指向了轉(zhuǎn)發(fā) IMP ,因此會進(jìn)入消息轉(zhuǎn)發(fā)過程,同時由于?forwardInvocation?被 aspects 所 hook ,最終會進(jìn)入到 aspects 的處理邏輯?__ASPECTS_ARE_BEING_CALLED__?中來。讓我們回過頭去看看3.2.2中的分析,由于找不到 aliasSelector 的 IMP 實現(xiàn),因此會在此進(jìn)行消息轉(zhuǎn)發(fā)。而在 3.2.2.1 的分析中我們知道,子類并沒有實現(xiàn)?NSSelectorFromString(AspectsForwardInvocationSelectorName)?,所以這里的流程就會進(jìn)入?doesNotRecognizeSelector,從而拋出異常。
出現(xiàn)上訴問題的原因在于,當(dāng) aliasSelector 沒有被找到的時候,我們沒能將消息正常的轉(zhuǎn)發(fā),也就是沒有實現(xiàn)一個?NSSelectorFromString(AspectsForwardInvocationSelectorName), 使得消息有機(jī)會重新轉(zhuǎn)發(fā)回去的方法。因此解決方案也就呼之欲出了,我的做法是在對子類的?forwardInvocation?方法進(jìn)行交換而不僅僅是替換,實現(xiàn)邏輯如下,強(qiáng)制生成一個?NSSelectorFromString(AspectsForwardInvocationSelectorName)?指向原對象的?forwardInvocation?的實現(xiàn)。
staticClass aspect_hookClass(NSObject*self,NSError**error) {
? ? ...
subclass = objc_allocateClassPair(baseClass, subclassName,0);
? ...
IMP originalImplementation = class_replaceMethod(subclass,@selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__,"v@:@");
if(originalImplementation) {
class_addMethod(subclass,NSSelectorFromString(AspectsForwardInvocationSelectorName),? originalImplementation,"v@:@");
}else{
Method baseTargetMethod = class_getInstanceMethod(baseClass,@selector(forwardInvocation:));
? ? ? ? IMP baseTargetMethodIMP = method_getImplementation(baseTargetMethod);
if(baseTargetMethodIMP) {
class_addMethod(subclass,NSSelectorFromString(AspectsForwardInvocationSelectorName), baseTargetMethodIMP,"v@:@");
? ? ? ? }
? }
...
}
注意如果?originalImplementation?為空,那么生成的?NSSelectorFromString(AspectsForwardInvocationSelectorName)?將指向 baseClass 也就是真正的這個對象的 forwradInvocation ,這個其實也就是 JSPatch hook 的方法。同時為了保證 block 的執(zhí)行順序(也就是前面介紹的 before hooks / instead hooks / after hooks ),這里需要將這段代碼提前到 after hooks 執(zhí)行之前進(jìn)行。這樣就解決了 forwardInvocation 在外面已經(jīng)被 hook 之后的沖突問題。
單個 aspect 的 remove 貌似有個問題,先來看看源碼。
if(aspect_isMsgForwardIMP(targetMethodIMP)) {
? ? ? SEL aliasSelector = aspect_aliasForSelector(selector);
? ? ? Method originalMethod = class_getInstanceMethod(klass, aliasSelector);
? ? ? IMP originalIMP = method_getImplementation(originalMethod);
if(originalIMP) {
? ? ? ? ? ? class_replaceMethod(klass, selector, originalIMP, typeEncoding);
? ? ? }
}
當(dāng)你對某個 aspect 執(zhí)行 remove 操作的時候,它會直接 replace 這個 selector 的 IMP,這個操作是對整個類的所有實例都生效的,這會導(dǎo)致什么問題呢?
以類 A 為例,你先進(jìn)入了 A 的一個實例 A1 ,hook 住了方法 selector1 ,然后,并沒有銷毀這個實例的時候,通過其他路徑又進(jìn)入類 A 的另一個實例 A2 ,當(dāng)然也 hook 了 selector1 ,然后這個時候,如果你 A2 中執(zhí)行了這個 aspect 的 remove 操作,按照上面的邏輯,類 A 的 selector1 將會恢復(fù)正常,可像而知,當(dāng)你退回 A1 的時候, A1 的 aspect 將會失效。這里其實我的解決思路很簡單,因為在執(zhí)行 remove 操作的時候,其實和這個對象相關(guān)的數(shù)據(jù)結(jié)構(gòu)都已經(jīng)被清除了,即使不去恢復(fù) selector1 的執(zhí)行,在進(jìn)入?__ASPECTS_ARE_BEING_CALLED__?由于這個沒有響應(yīng)的 aspects ,其實會直接跳到原來的處理邏輯,并不會有其他附加影響。
還有一個問題就是,aspects 的 remove 操作只能支持單個的 remove 操作,不支持一次性刪除一個對象的所有 aspects 。這里,也做了一個擴(kuò)展,對原來的 aspects 進(jìn)行擴(kuò)展,實現(xiàn)了一次性 remove 一個對象所有 aspects 的方法。