(這篇文章在微信讀書Weread團隊博客中也有發表)ios
最近在作項目的打點統計的時候,發現業務邏輯和打點邏輯常常耦合在一塊兒,這樣一方面影響了正常的業務邏輯,同時也很容易搞亂打點邏輯,並且要查看打點狀況的時候也很分散,所以想着如何將二者解耦,並將打點邏輯集中起來。其實在 web 編程時候,這種場景很早就有了很成熟的方案,也就是所謂的 aop 編程(面向切面編程),其原理也就是在不更改正常的業務處理流程的前提下,經過生成一個動態代理類,從而實現對目標對象嵌入附加的操做。在 ios 中,要想實現類似的效果也很簡單,利用 oc 的動態性,經過 swizzling method 改變目標函數的 selector 所指向的實現,而後在新的實現中實現附加的操做,完成以後再回到原來的處理邏輯。想明白這些以後,我就打算動手實現,固然並無重複造輪子,我在 github 發現了一個基於 swizzling method 的開源框架 Aspects 。這個庫的代碼量比較小,總共就一個類文件,使用起來也比較方便,好比你想統計某個 controller 的 viewwillappear 的調用次數,你只須要引入 Aspect.h 頭文件,而後在合適的地方初始化以下代碼便可。git
- (void)addKvLogAspect { //想法tab打開 [self wr_Aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{ KVLog_ReviewTimeline(ReviewTimeline_Open_Tab); }error:NULL]; }
這篇文章主要是介紹 aspect 源碼以及其思路,以及我在實際應用中遇到的一些問題。對 swizzling method 不瞭解的同窗能夠先去網上了解一下,下面的內容是基於你們對 swizzling method 有必定的瞭解的基礎上的。github
咱們知道 oc 是動態語言,咱們執行一個函數的時候,實際上是在發一條消息:[ receiver message ],這個過程就是根據 message 生成 selector,而後根據 selector 尋找指向函數具體實現的指針 IMP,而後找到真正的函數執行邏輯。這種處理流程給咱們提供了動態性的可能,試想一下,若是在運行時,動態的改變了 selector 和 IMP 的對應關係,那麼就能使得原來的 [ receiver message ] 進入到新的函數實現了。web
那麼具體怎麼實現這樣的動態替換了?編程
直觀的一種方案是提供一個統一入口,如 commonImp,將全部須要 hook 的函數都指向這個函數,而後在這裏,提取相關信息進行轉發,JSPatch 實現原理詳解對此方案的可行性有進行分析,對於64位機器可能會有點問題。另一個方法就是利用 oc 本身的消息轉發機制進行轉發,aspect 的大致思路,基本上是順着這個來的。爲了更好的解釋這個過程,咱們先來看一下消息具體是怎麼找到對應的 imp 的,見下圖(此圖並不是原創)。微信
從上面咱們能夠發現,在發消息的時候,若是 selector 有對應的 IMP,則直接執行,若是沒有,oc 給咱們提供了幾個可供補救的機會,依次有 resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。Aspects 之因此選擇在 forwardInvocation 這裏處理是由於,這幾個階段特性都不太同樣:resolvedInstanceMethod 適合給類/對象動態添加一個相應的實現,forwardingTargetForSelector 適合將消息轉發給其餘對象處理,相對而言,forwardInvocation 是裏面最靈活,最能符合需求的。所以 aspects 的方案就是,對於待 hook 的 selector,將其指向 objc_msgForward / _objc_msgForward_stret
,同時生成一個新的 aliasSelector 指向原來的 IMP,而且 hook 住 forwardInvocation 函數,使他指向本身的實現。按照上面的思路,當被 hook 的 selector 被執行的時候,首先根據 selector 找到了 objc_msgForward / _objc_msgForward_stret
,而這個會觸發消息轉發,從而進入 forwardInvocation。同時因爲 forwardInvocation 的指向也被修改了,所以會轉入新的 forwardInvocation 函數,在裏面執行須要嵌入的附加代碼,完成以後,再轉回原來的 IMP。數據結構
介紹完大體思路以後,下面將從代碼層來來具體分析。從頭文件中能夠看到使用aspects有兩種使用方式app
類方法框架
實例方法jsp
+ (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;
二者的主要原理基本差很少,這裏不作一一介紹,只是以實例方法爲例進行說明。在介紹以前,先介紹裏面幾個重要的數據結構:
typedef NS_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 執行的時機,也就是額外操做的執行時機,在個人應用場景中就是打點邏輯的執行時機,它能夠在原始函數執行以前,也能夠是執行以後,甚至能夠徹底替換掉原來的邏輯。
一個對象或者類的全部的 Aspects 總體狀況
// 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
一個 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
這裏主要包含了單個的 aspect 的具體信息,包括執行時機,要執行 block 所須要用到的具體信息:包括方法簽名、參數等等
一個 Aspect 執行環境,主要是 NSInvocation 信息。
@interface AspectInfo : NSObject <AspctInfo> - (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
有了上面的瞭解,咱們就能更好的分析整個 apsects 的執行流程。添加一個 aspect 的關鍵流程以下圖所示:
從代碼來看,要想使用 aspects ,首先要添加一個 aspect ,能夠經過上面介紹的類/實例方法。關鍵代碼實現以下:
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { ... __block AspectIdentifier *identifier = nil; aspect_performLocked(^{ if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {//1判斷可否hook ...//2 記錄數據結構 aspect_prepareClassAndHookSelector(self, selector, error);//3 swizzling } }); return identifier; }
這個過程基本和上面的流程圖一致:這裏重點介紹幾個關鍵部分
1)判斷可否被 hook:對於對象實例而言,這裏主要是根據黑名單,好比 retain forwardInvocation 等這些方法在外部是不能被 hook(對於類對象還要確保同一個類繼承關係層級中,只能被 hook 一次,所以這裏須要判斷子類,父類有沒有被 hook,之因此作這樣的實現,主要是爲了不出現死循環的出現,這裏有相關的討論)。若是可以 hook,則繼續下面的步驟。
2)swizzling method:這是真正的核心邏輯
swizzling method 主要有兩部分,一個是對對象的 forwardInvocation 進行 swizzling,另外一個是對傳入的 selector 進行 swizzling.
static void aspect_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 ...// } }
aspect_hookClass 函數主要 swizzling 類/對象的 forwardInvocation 函數,aspects 的真正的處理邏輯都是在 forwradInvocation 函數裏面進行的。對於對象實例而言,源代碼中並無直接 swizzling 對象的 forwardInvocation 方法,而是動態生成一個當前對象的子類,並將當前對象與子類關聯,而後替換子類的 forwardInvocation 方法(這裏具體方法就是調用了 object_setClass(self, subclass) ,將當前對象 isa 指針指向了 subclass ,同時修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回當前對象的 class。,這個地方特別繞,它的原理有點相似 kvo 的實現,它想要實現的效果就是,將當前對象變成一個 subclass 的實例,同時對於外部使用者而言,又能把它繼續當成原對象在使用,並且全部的 swizzling 操做都發生在子類,這樣作的好處是你不須要去更改對象自己的類,也就是,當你在 remove aspects 的時候,若是發現當前對象的 aspect 都被移除了,那麼,你能夠將 isa 指針從新指回對象自己的類,從而消除了該對象的 swizzling ,同時也不會影響到其餘該類的不一樣對象)。對於每個對象而言,這樣的動態對象只會生成一次,這裏 aspect_swizzlingForwardInvocation 將使得 forwardInvocation 方法指向 aspects 本身的實現邏輯 ,具體代碼以下:
static Class aspect_hookClass(NSObject *self, NSError **error) { ... //生成動態子類,並swizzling forwardInvocation方法 subclass = objc_allocateClassPair(baseClass, subclassName, 0); aspect_swizzleForwardInvocation(subclass);//swizzling forwardinvation方法 objc_registerClassPair(subclass); ... object_setClass(self, subclass);//將當前self設置爲子類,這裏其實只是更改了self的isa指針而已 return subclass; } ... static void aspect_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@:@") } ... }
因爲子類自己並無實現 forwardInvocation ,隱藏返回的 originalImplementation 將爲空值,因此也不會生成 NSSelectorFromString(AspectsForwardInvocationSelectorName) 。
當 forwradInvocation 被 hook 以後,接下來,將對傳入的 selector 進行 hook ,這裏的作法是,將 selector 指向了轉發 IMP ,同時生成一個 aliasSelector ,指向了原來的 IMP ,同時爲了放在重複 hook ,作了一個判斷,若是發現 selector 已經指向了轉發 IMP ,那就就不須要進行交換了,代碼以下
static void aspect_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]) { __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); } class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);// point to _objc_msgForward ... } }
基於上面的代碼分析知道,轉發最終的邏輯代碼最終轉入 ASPECTS_ARE_BEING_CALLED 函數的處理中。這裏,須要處理的部分包括額外處理代碼(如打點代碼)以及最終從新轉會原來的 selector 所指向的函數,其實現代碼以下:
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { ... // Before hooks. 原來邏輯以前執行 aspect_invoke(classContainer.beforeAspects, info); aspect_invoke(objectContainer.beforeAspects, info); // Instead hooks. BOOL respondsToAlias = 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];//根據aliasSelector找到原來的邏輯並執行 break; } }while (!respondsToAlias && (klass = class_getSuperclass(klass))); } // After hooks. 原來邏輯以後執行 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實現,沒有找到原來的邏輯,進行消息轉發 invocation.selector = originalSelector; SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); if ([self respondsToSelector:originalForwardInvocationSEL]) { ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); } else { [self doesNotRecognizeSelector:invocation.selector]; } } ... }
依次處理 before/instead/after hook 以及真正函數實現。若是沒有找到原始的函數實現,還須要進行轉發操做。
以上就是 apsects 的實現了,接下來會介紹在實際應用過程當中遇到的一些問題以及個人解決方案。
咱們的項目中引入了 jspatch 做爲咱們的 hot fix方案。 jspatch 也會 hook 住對象的 forwradInvocation 方法,而且 swizzling 相應的 method,使其指向轉發 IMP,因爲 aspects 也是基於這二者實現的,那麼會不會致使問題呢(其實相似的問題也會發生在對象提早被 kvo 了,會不會有影響)?
回過頭去看3.2.1 咱們先是 hook了 類的 forwardInvocation 使其指向了 __ASPECTS_ARE_BEING_CALLED__
,而後在 swizzling method 那裏, aspect 有作一個判斷,若是傳入的 selector 指向了轉發 IMP,那麼咱們什麼也不作。所以可想而知,若是傳入的 selector 先被 jspatch hook,那麼,這裏咱們將不會再處理,也就不會生成 aliasSelector 。
這會致使什麼問題了?設想一下,當 selector 被觸發的時候,因爲 selector 指向了轉發 IMP,所以會進入消息轉發過程,同時因爲 forwardInvocation 被 aspects 所 hook,最終會進入到 aspects 的處理邏輯 ASPECTS_ARE_BEING_CALLED 中來。讓咱們回過頭去看看3.2.2中的分析,因爲找不到 aliasSelector 的 IMP 實現,所以會在此進行消息轉發。而在3.2.1.1的分析中咱們知道,子類並無實現 NSSelectorFromString(AspectsForwardInvocationSelectorName),因此這裏的流程就會進入 doesNotRecognizeSelector,從而拋出異常。
出現上訴問題的緣由在於,當 aliasSelector 沒有被找到的時候,咱們沒能將消息正常的轉發,也就是沒有實現一個 NSSelectorFromString(AspectsForwardInvocationSelectorName),使得消息有機會從新轉發回去的方法。所以解決方案也就呼之欲出了,個人作法是在對子類的 forwardInvocation 方法進行交換而不只僅是替換,實現邏輯以下,強制生成一個 NSSelectorFromString(AspectsForwardInvocationSelectorName) 指向原對象的 forwardInvocation 的實現。
static Class 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 的執行順序(也就是前面介紹的 before hooks / instead hooks / after hooks ),這裏須要將這段代碼提早到 after hooks 執行以前進行。這樣就解決了 forwardInvocation 在外面已經被 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); } }
當你對某個 aspect 執行 remove 操做的時候,它會直接 replace 這個 selector 的 IMP,這個操做是對整個類的全部實例都生效的,這會致使什麼問題呢?
以類 A 爲例,你先進入了 A 的一個實例 A1,hook 住了方法 selector1,而後,並無銷燬這個實例的時候,經過其餘路徑又進入類 A 的另外一個實例 A2,固然也 hook 了 selector1,而後這個時候,若是你 A2 中執行了這個 aspect 的 remove 操做,按照上面的邏輯,類 A 的 selector1 將會恢復正常,可像而知,當你退回 A1 的時候, A1 的 aspect 將會失效。這裏其實個人解決思路很簡單,由於在執行 remove 操做的時候,其實和這個對象相關的數據結構都已經被清除了,即便不去恢復 selector1 的執行,在進入 ASPECTS_ARE_BEING_CALLED 因爲這個沒有響應的 aspects,其實會直接跳到原來的處理邏輯,並不會有其餘附加影響。
還有一個問題就是, aspects 的 remove 操做只能支持單個的 remove 操做,不支持一次性刪除一個對象的全部 aspects 。這裏,也作了一個擴展,對原來的 aspects 進行擴展,實現了一次性 remove 一個對象全部 aspects 的方法。