轉(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)性的可能崖飘,試想一下榴捡,如果在運行時,動態(tài)的改變了 selector 和 IMP 的對應(yīng)關(guān)系朱浴,那么就能使得原來的[receiver message]進入到新的函數(shù)實現(xiàn)了吊圾。
那么具體怎么實現(xiàn)這樣的動態(tài)替換了?
直觀的一種方案是提供一個統(tǒng)一入口,如 commonImp ,將所有需要 hook 的函數(shù)都指向這個函數(shù)翰蠢,然后在這里项乒,提取相關(guān)信息進行轉(zhuǎn)發(fā),JSPatch 實現(xiàn)原理詳解對此方案的可行性有進行分析梁沧,對于64位機器可能會有點問題檀何。另外一個方法就是利用 oc 自己的消息轉(zhuǎn)發(fā)機制進行轉(zhuǎn)發(fā),Aspects 的大體思路,基本上是順著這個來的频鉴。為了更好的解釋這個過程栓辜,我們先來看一下消息具體是怎么找到對應(yīng)的 imp 的,見下圖(此圖并非原創(chuàng))垛孔。?
從上面我們可以發(fā)現(xiàn)藕甩,在發(fā)消息的時候,如果 selector 有對應(yīng)的 IMP ,則直接執(zhí)行周荐,如果沒有狭莱,oc 給我們提供了幾個可供補救的機會,依次有?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ā)碍岔,從而進入?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;
兩者的主要原理基本差不多鸵赫,這里不做一一介紹衣屏,只是以實例方法為例進行說明。在介紹之前奉瘤,先介紹里面幾個重要的數(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í)行的時機勾拉,也就是額外操作的執(zhí)行時機煮甥,在我的應(yīng)用場景中就是打點邏輯的執(zhí)行時機,它可以在原始函數(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í)行時機双霍,要執(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 進行 swizzling,另一個是對傳入的 selector 進行 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ù)里面進行的陶贼。對于對象實例而言,源代碼中并沒有直接 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 進行 hook 叁幢,這里的做法是,將 selector 指向了轉(zhuǎn)發(fā) IMP 坪稽,同時生成一個 aliasSelector 曼玩,指向了原來的 IMP ,同時為了放在重復(fù) hook ,做了一個判斷,如果發(fā)現(xiàn) selector 已經(jīng)指向了轉(zhuǎn)發(fā) IMP ,那就就不需要進行交換了窒百,代碼如下
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),沒有找到原來的邏輯渤滞,進行消息轉(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),還需要進行轉(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 库北,因此會進入消息轉(zhuǎn)發(fā)過程爬舰,同時由于?forwardInvocation?被 aspects 所 hook ,最終會進入到 aspects 的處理邏輯?__ASPECTS_ARE_BEING_CALLED__?中來。讓我們回過頭去看看3.2.2中的分析寒瓦,由于找不到 aliasSelector 的 IMP 實現(xiàn)情屹,因此會在此進行消息轉(zhuǎn)發(fā)。而在 3.2.2.1 的分析中我們知道杂腰,子類并沒有實現(xiàn)?NSSelectorFromString(AspectsForwardInvocationSelectorName)?垃你,所以這里的流程就會進入?doesNotRecognizeSelector,從而拋出異常喂很。
出現(xiàn)上訴問題的原因在于惜颇,當(dāng) aliasSelector 沒有被找到的時候,我們沒能將消息正常的轉(zhuǎn)發(fā)少辣,也就是沒有實現(xiàn)一個?NSSelectorFromString(AspectsForwardInvocationSelectorName)凌摄, 使得消息有機會重新轉(zhuǎn)發(fā)回去的方法。因此解決方案也就呼之欲出了漓帅,我的做法是在對子類的?forwardInvocation?方法進行交換而不僅僅是替換锨亏,實現(xiàn)邏輯如下,強制生成一個?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í)行之前進行。這樣就解決了 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 為例懈玻,你先進入了 A 的一個實例 A1 巧婶,hook 住了方法 selector1 ,然后涂乌,并沒有銷毀這個實例的時候艺栈,通過其他路徑又進入類 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í)行缔俄,在進入?__ASPECTS_ARE_BEING_CALLED__?由于這個沒有響應(yīng)的 aspects ,其實會直接跳到原來的處理邏輯,并不會有其他附加影響蚪战。
還有一個問題就是牵现,aspects 的 remove 操作只能支持單個的 remove 操作,不支持一次性刪除一個對象的所有 aspects 。這里邀桑,也做了一個擴展瞎疼,對原來的 aspects 進行擴展,實現(xiàn)了一次性 remove 一個對象所有 aspects 的方法壁畸。