iOS 如何實現 Aspect Oriented Programming (下)

一縷殤流化隱半邊冰霜發表於2016-10-22
111194012-362f68c0c95d15f4

(接上篇)

五. Aspects hook過程詳解

先看看函式呼叫棧的情況

從呼叫棧可以看出,Aspects hook過程主要分4個階段,hookClass,ASPECTS_ARE_BEING_CALLED,prepareClassAndHookSelector,remove。

121194012-f5d728cf23df4d91
1. hookClass

statedClass 和 baseClass是有區別的的。

statedClass 是獲取類物件,baseClass是獲取到類的isa。

先判斷是用來className是否包含hasSuffix:AspectsSubclassSuffix

如果包含了@”_Aspects_”字尾,代表該類已經被hook過了,直接return。
如果不包含@”_Aspects_”字尾,再判斷是否是baseClass是否是元類,如果是元類,呼叫aspect_swizzleClassInPlace。如果也不是元類,再判斷statedClass 和 baseClass是否相等,如果不相等,說明為KVO過的物件,因為KVO的物件isa指標會指向一箇中間類。對KVO中間類呼叫aspect_swizzleClassInPlace。

_aspect_modifySwizzledClasses會傳入一個入參為(NSMutableSet *swizzledClasses)的block,block裡面就是判斷在這個Set裡面是否包含當前的ClassName,如果不包含,就呼叫aspect_swizzleForwardInvocation()方法,並把className加入到Set集合裡面。

_aspect_modifySwizzledClasses方法裡面保證了swizzledClasses這個Set集合是全域性唯一的,並且給傳入的block加上了執行緒鎖@synchronized( ),保證了block呼叫中執行緒是安全的。

關於呼叫aspect_swizzleForwardInvocation,將原IMP指向forwardInvocation是下個階段的事情,我們先把hookClass看完。

當className沒有包含@”_Aspects_”字尾,並且也不是元類,也不是KVO的中間類,即statedClass = = baseClass 的情況,於是,預設的新建一個子類subclass。

到此,我們可以瞭解到Aspects的設計思想,hook 是在runtime中動態建立子類的基礎上實現的。所有的 swizzling 操作都發生在子類,這樣做的好處是你不需要去更改物件本身的類,也就是,當你在 remove aspects 的時候,如果發現當前物件的 aspect 都被移除了,那麼,你可以將 isa 指標重新指回物件本身的類,從而消除了該物件的 swizzling ,同時也不會影響到其他該類的不同物件)這樣對原來替換的類或者物件沒有任何影響而且可以在子類基礎上新增或者刪除aspect。

新建的類的名字,會先加上AspectsSubclassSuffix字尾,即在className後面加上@”_Aspects_”,標記成子類。再呼叫objc_getClass方法,建立這個子類。

objc_getClass會呼叫look_up_class方法。

這個方法會去檢視有沒有實現叫name的class,檢視過程中會用到rwlock_reader_t lock(runtimeLock),讀寫鎖,底層是用pthread_rwlock_t實現的。

由於是我們剛剛新建的一個子類名,很有可能是objc_getClass()返回nil。那麼我們需要新建這個子類。呼叫objc_allocateClassPair()方法。

呼叫objc_allocateClassPair會新建一個子類,它的父類是入參superclass。

如果新建的子類subclass = = nil,就會報錯,objc_allocateClassPair failed to allocate class。

aspect_swizzleForwardInvocation(subclass)這是下一階段的事情,主要作用是替換當前類forwardInvocation方法的實現為__ASPECTS_ARE_BEING_CALLED__,先略過。

接著呼叫aspect_hookedGetClass( ) 方法。

aspect_hookedGetClass方法是把class的例項方法替換成返回statedClass,也就是說把呼叫class時候的isa指向了statedClass了。

這兩句的意圖我們也就明白了。

第一句是把subclass的isa指向了statedClass,第二句是把subclass的元類的isa,也指向了statedClass。

最後呼叫objc_registerClassPair( ) 註冊剛剛新建的子類subclass,再呼叫object_setClass(self, subclass);把當前self的isa指向子類subclass。

至此,hookClass階段就完成了,成功的把self hook成了其子類 xxx_Aspects_。

131194012-73519d92a9a5c214
2. ASPECTS_ARE_BEING_CALLED

在上一階段hookClass的時候,有幾處都呼叫了aspect_swizzleForwardInvocation方法。

aspect_swizzleForwardInvocation就是整個Aspects hook方法的開始。

呼叫class_replaceMethod方法,實際底層實現是呼叫_class_addMethod方法。

從上述原始碼中,我們可以看到,先_findMethodInClass(cls, name),從cls中查詢有沒有name的方法。如果有,並且能找到對應的IMP的話,就進行替換method_setImplementation((Method)m, imp),把name方法的IMP替換成imp。這種方式_class_addMethod返回的是name方法對應的IMP,實際上就是我們替換完的imp。

如果在cls中沒有找到name方法,那麼就新增該方法,在mlist -> method_list[0] 的位置插入新的name方法,對應的IMP就是傳入的imp。這種方式_class_addMethod返回的是nil。

回到aspect_swizzleForwardInvocation中,

把forwardInvocation:的IMP替換成__ASPECTS_ARE_BEING_CALLED__ 。如果在klass裡面找不到forwardInvocation:方法,就會新新增該方法。

由於子類本身並沒有實現 forwardInvocation ,隱藏返回的 originalImplementation 將為空值,所以也不會生成 NSSelectorFromString(AspectsForwardInvocationSelectorName) 。所以還需要_class_addMethod會為我們新增了forwardInvocation:方法的實現

謝謝簡書的大神 @zhao0 指點,這個坑在Aspects 1.4.1中已經修復了。

在aspect_swizzleForwardInvocation中,class_replaceMethod返回的是原方法的IMP,originalImplementation不為空的話說明原方法有實現,新增一個新方法__aspects_forwardInvocation:指向了原來的originalImplementation,在__ASPECTS_ARE_BEING_CALLED__那裡如果不能處理,判斷是否有實現__aspects_forwardInvocation,有的話就轉發。

如果originalImplementation返回的不是nil,就說明已經替換成功。替換完方法之後,我們在klass中再加入一個叫“__aspects_forwardInvocation:”的方法,對應的實現也是(IMP)__ASPECTS_ARE_BEING_CALLED__。

接下來就是整個Aspects的核心實現了:__ASPECTS_ARE_BEING_CALLED__

這一段是hook前的準備工作:

  1. 獲取原始的selector
  2. 獲取帶有aspects_xxxx字首的方法
  3. 替換selector
  4. 獲取例項物件的容器objectContainer,這裡是之前aspect_add關聯過的物件。
  5. 獲取獲得類物件容器classContainer
  6. 初始化AspectInfo,傳入self、invocation引數

呼叫巨集定義執行Aspects切片功能

之所以這裡用一個巨集定義來實現裡面的功能,是為了獲得一個更加清晰的堆疊資訊。

巨集定義裡面就做了兩件事情,一個是執行了[aspect invokeWithInfo:info]方法,一個是把需要remove的Aspects加入等待被移除的陣列中。

[aspect invokeWithInfo:info]方法在上篇裡面詳細分析過了其實現,這個函式的主要目的是把blockSignature初始化blockSignature得到invocation。然後處理引數,如果引數block中的引數大於1個,則把傳入的AspectInfo放入blockInvocation中。然後從originalInvocation中取出引數給blockInvocation賦值。最後呼叫[blockInvocation invokeWithTarget:self.block];這裡Target設定為self.block。也就執行了我們hook方法的block。

所以只要呼叫aspect_invoke(classContainer.Aspects, info);這個核心替換的方法,就能hook我們原有的SEL。對應的,函式第一個引數分別傳入的是classContainer.beforeAspects、classContainer.insteadAspects、classContainer.afterAspects就能對應的實現before、instead、after對應時間的Aspects切片的hook。

這一段程式碼是實現Instead hooks的。先判斷當前insteadAspects是否有資料,如果沒有資料則判斷當前繼承鏈是否能響應aspects_xxx方法,如果能,則直接呼叫aliasSelector。注意:這裡的aliasSelector是原方法method

這兩行是對應的執行After hooks的。原理如上。

至此,before、instead、after對應時間的Aspects切片的hook如果能被執行的,都執行完畢了。

如果hook沒有被正常執行,那麼就應該執行原來的方法。

invocation.selector先換回原來的originalSelector,如果沒有被hook成功,那麼AspectsForwardInvocationSelectorName還能再拿到原來的IMP對應的SEL。如果能相應,就呼叫原來的SEL,否則就報出doesNotRecognizeSelector的錯誤。

最後呼叫移除方法,移除hook。

141194012-45f6e609d2f15988
3. prepareClassAndHookSelector

現在又要回到上篇中提到的aspect_prepareClassAndHookSelector方法中來了。

klass是我們hook完原始的class之後得到的子類,名字是帶有_Aspects_字尾的子類。因為它是當前類的子類,所以也可以從它這裡獲取到原有的selector的IMP。

這裡是判斷當前IMP是不是_objc_msgForward或者_objc_msgForward_stret,即判斷當前IMP是不是訊息轉發。

如果不是訊息轉發,就先獲取當前原始的selector對應的IMP的方法編碼typeEncoding。

如果子類裡面不能響應aspects_xxxx,就為klass新增aspects_xxxx方法,方法的實現為原生方法的實現。

Aspects整個hook的入口就是這句話:

由於我們將slector指向_objc_msgForward 和_objc_msgForward_stret,可想而知,當selector被執行的時候,也會觸發訊息轉發從而進入forwardInvocation,而我們又對forwardInvacation進行了swizzling,因此,最終轉入我們自己的處理邏輯程式碼中。

151194012-70a37b1a25849cb3
4. aspect_remove

aspect_remove整個銷燬過程的函式呼叫棧

aspect_remove 是整個 aspect_add的逆過程。
aspect_performLocked是保證執行緒安全。把AspectsContainer都置為空,remove最關鍵的過程就是aspect_cleanupHookedClassAndSelector(self, aspect.selector);移除之前hook的class和selector。

klass是現在的class,如果是元類,就轉換成元類。

先回復MsgForward訊息轉發函式,獲得方法簽名,然後把原始轉發方法替換回我們hook過的方法。

這裡有一個需要注意的問題。

如果當前Student有2個例項,stu1和stu2,並且他們都同時hook了相同的方法study( ),stu2在執行完aspect_remove,把stu2的study( )方法還原了。這裡會把stu1的study( )方法也還原了。因為remove方法這個操作是對整個類的所有例項都生效的。

要想每個例項還原各自的方法,不影響其他例項,上述這段程式碼刪除即可。因為在執行 remove 操作的時候,其實和這個物件相關的資料結構都已經被清除了,即使不去恢復 stu2 的study( ) 的執行,在進入 __ASPECTS_ARE_BEING_CALLED__,由於這個沒有響應的 aspects ,其實會直接跳到原來的處理邏輯,並不會有其他附加影響。

還要移除AspectTracker裡面所有標記的swizzledClassesDict。銷燬全部記錄的selector。

最後,我們還需要還原類的AssociatedObject關聯物件,以及用到的AspectsContainer容器。

這個方法銷燬了AspectsContainer容器,並且把關聯物件也置成了nil。

aspect_undoSwizzleClassInPlace會再呼叫aspect_undoSwizzleForwardInvocation方法。

最後還原ForwardInvocation的Swizzling,把原來的ForwardInvocation再交換回來。

六. 關於Aspects 的一些 “坑”

161194012-321f597970d064a9

在Aspects這個庫了,用到了Method Swizzling有幾處,這幾處如果處理不好,就會掉“坑”裡了。

1.aspect_prepareClassAndHookSelector 中可能遇到的“坑”

在aspect_prepareClassAndHookSelector方法中,會把原始的selector hook成_objc_msgForward。但是如果這裡的selector就是_objc_msgForward會發生什麼呢?

其實這裡的坑在作者的程式碼註釋裡面已經隱藏的提到了。

在__ASPECTS_ARE_BEING_CALLED__方法中,最後轉發訊息的那段程式碼裡面有這樣一段註釋

看到這段註釋以後,你肯定會思考,為何到了這裡就會throw an exception呢?原因是因為找不到NSSelectorFromString(AspectsForwardInvocationSelectorName)對應的IMP。

再往上找,就可以找到原因了。在實現aspect_prepareClassAndHookSelector中,會判斷當前的selector是不是_objc_msgForward,如果不是msgForward,接下來什麼也不會做。那麼aliasSelector是沒有對應的實現的。

由於 forwardInvocation 被 aspects 所 hook ,最終會進入到 aspects 的處理邏輯__ASPECTS_ARE_BEING_CALLED__中來,此時如果沒有找不到 aliasSelector 的 IMP 實現,因此會在此進行訊息轉發。而且子類並沒有實現 NSSelectorFromString(AspectsForwardInvocationSelectorName),於是轉發就會丟擲異常。

這裡的“坑”就在於,hook的selector如果變成了_objc_msgForward,就會出現異常了,但是一般我們不會去hook _objc_msgForward這個方法,出現這個問題的原因是有其他的Swizzling會去hook這個方法。

比如說JSPatch把傳入的 selector 先被 JSPatch hook ,那麼,這裡我們將不會再處理,也就不會生成 aliasSelector 。就會出現閃退的異常了。

這裡在這篇文章中給出了一個解決辦法:

在對子類的 forwardInvocation方法進行交換而不僅僅是替換,實現邏輯如下,強制生成一個 NSSelectorFromString(AspectsForwardInvocationSelectorName)指向原物件的 forwardInvocation的實現。

注意如果 originalImplementation為空,那麼生成的 NSSelectorFromString(AspectsForwardInvocationSelectorName)
將指向 baseClass 也就是真正的這個物件的 forwradInvocation ,這個其實也就是 JSPatch hook 的方法。同時為了保證 block 的執行順序(也就是前面介紹的 before hooks / instead hooks / after hooks ),這裡需要將這段程式碼提前到 after hooks 執行之前進行。這樣就解決了 forwardInvocation 在外面已經被 hook 之後的衝突問題。

謝謝簡書的大神 @zhao0 指點,在這篇文章詳細分析了Aspect和JSPatch各種相容性問題,經過詳細的分析,最後只有4種不相容的情況。

2. aspect_hookSelector 可能出現的 “坑”

在Aspects中主要是hook selector,此時如果有多個地方會和Aspects去hook相同方法,那麼也會出現doesNotRecognizeSelector的問題。

舉個例子,比如說在NSArray中用Aspects 去hook了objectAtIndex的方法,然後在NSMutableArray中Swizzling了objectAtIndex方法。在
NSMutableArray中,呼叫objectAtIndex就有可能出錯。

因為還是在於Aspects hook 了selector之後,會把原來的selector變成_objc_msgForward。等到NSMutableArray再去hook這個方法的時候,記錄的是IMP就是_objc_msgForward這個了。如果這時objc_msgSend執行原有實現,就會出錯了。因為原有實現已經被替換為_objc_msgForward,而真的IMP由於被Aspects先Swizzling掉了,所以找不到。

解決辦法還是類似JSPatch的解決辦法:

把-forwardInvocation:也進行Swizzling,在自己的-forwardInvocation:方法中進行同樣的操作,就是判斷傳入的NSInvocation的Selector,被Swizzling的方法指向了_objc_msgForward(或_objc_msgForward_stret)如果是自己可以識別的Selector,那麼就將Selector變為原有Selector在執行,如果不識別,就直接轉發。

最後

最後用一張圖總結一下Aspects整體流程:

171194012-a778dc8827d370fe

請大家多多指教。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

iOS 如何實現 Aspect Oriented Programming (下) iOS 如何實現 Aspect Oriented Programming (下)

相關文章