iOS開發 面向切面程式設計之 Aspects 原始碼解析

GabrielPanda發表於2019-03-03

1、面向切面程式設計應用在統計上 業務邏輯和統計邏輯經常耦合在一起,一方面影響了正常的業務邏輯,同時也很容易搞亂打點邏輯,而且要檢視打點情況的時候也很分散。在 web 程式設計時候,這種場景很早就有了很成熟的方案,也就是所謂的AOP 程式設計(面向切面程式設計),其原理也就是在不更改正常的業務處理流程的前提下,通過生成一個動態代理類,從而實現對目標物件嵌入附加的操作。在 iOS 中,要想實現相似的效果也很簡單,利用 oc 的動態性,通過 swizzling method 改變目標函式的 selector 所指向的實現,然後在新的實現中實現附加的操作,完成之後再回到原來的處理邏輯。 開源框架Aspects是一個非常好的框架。 Aspects

2、基本原理

原理1.png

每一個物件都有一個指向其所屬類的isa指標,通過該指標找到所屬的類,然後會在所屬類中的方法列表中尋找方法的實現,如果在方法列表中查到了和選擇子名稱相符的方法就會跳轉到他的方法實現,如果找不到會向其父類的方法列表中查詢,以此類推,直到NSObject類,如果還是查詢不到就會執行“訊息轉發”操作。 另外為了保證訊息機制的效率,每一個類都設定一個快取方法列表,快取列表中包含了當前類的方法以及繼承自父類的方法,在查詢方法列的時候,都會先查詢本類的快取列表,再去查詢方法類別。這樣當一個方法已經被呼叫過一次,下次呼叫就會很快的查詢到並呼叫。

方法呼叫的過程
1.在物件自己快取的方法列表中去找要呼叫的方法,找到了就直接執行其實現。
2.快取裡沒找到,就去上面說的它的方法列表裡找,找到了就執行其實現。
3.還沒找到,說明這個類自己沒有了,就會通過isa去向其父類裡執行1、2。
4.如果找到了根類還沒找到,那麼就是沒有了,會轉向一個攔截呼叫的方法,我們可以自己在攔截呼叫方法裡面做一些處理。
5.如果沒有在攔截呼叫裡做處理,那麼就會報錯崩潰。
複製程式碼

從上面我們可以發現,在發訊息的時候,如果 selector 有對應的 IMP,則直接執行,如果沒有就進行查詢,如果最後沒有查詢到。OC 給我們提供了幾個可供補救的機會,依次有 resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。

Aspects 之所以選擇在 forwardInvocation 這裡處理是因為,這幾個階段特性都不太一樣:

resolvedInstanceMethod 適合給類/物件動態新增一個相應的實現forwardingTargetForSelector 適合將訊息轉發給其他物件處理 forwardInvocation 是裡面最靈活,最能符合需求的

因此 Aspects 的方案就是,對於待 hook 的 selector,將其指向 objc_msgForward,同時生成一個新的 aliasSelector 指向原來的 IMP,並且 hook 住 forwardInvocation 函式,使他指向自己的實現。按照上面的思路,當被 hook 的 selector 被執行的時候,首先根據 selector 找到了 objc_msgForward ,而這個會觸發訊息轉發,從而進入 forwardInvocation。同時由於 forwardInvocation 的指向也被修改了,因此會轉入新的 forwardInvocation 函式,在裡面執行需要嵌入的附加程式碼,完成之後,再轉回原來的 IMP。

Aspects hook的過程

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(^{

//首先判斷
       if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) 
     {
           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);

           }//if (identifier) 

       }//if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) 

   });
   return identifier;
}

複製程式碼

在沒有hook之前,ViewController的SEL與IMP關係如下

hook之前.png

hook之後.png

最初的viewWillAppear: 指向了_objc_msgForward
增加了aspects_viewWillAppear:,指向最初的viewWillAppear:的IMP
最初的forwardInvocation:指向了Aspect提供的一個C方法__ASPECTS_ARE_BEING_CALLED__
動態增加了__aspects_forwardInvocation:,
指向最初的forwardInvocation:的IMP
複製程式碼

然後,我們再來看看hook後,一個viewWillAppear:的實際呼叫順序:

2.object收到selector(viewWillAppear:)的訊息
2.找到對應的IMP:_objc_msgForward,執行後觸發訊息轉發機制。
3.object收到forwardInvocation:訊息
4.找到對應的IMP:__ASPECTS_ARE_BEING_CALLED__,執行IMP 

複製程式碼
//__ASPECTS_ARE_BEING_CALLED__中的邏輯
1.向object物件傳送aspects_viewWillAppear:執行最初的viewWillAppear方法的IMP
2.執行插入的block程式碼
3.如果ViewController無法響應aspects_viewWillAppear,則向object物件傳送__aspects_forwardInvocation:來執行最初的forwardInvocation IMP
複製程式碼

1、判斷能否hook 對Class和MetaClass進行進行合法性檢查,判斷能否hook,規則如下 1).retain,release,autorelease,forwoardInvocation:不能被hook 2).dealloc只能在方法前hook 3).類的繼承關係中,同一個方法只能被hook一次

2.建立AspectsContainer物件, 以"aspects_ "+ SEL為key,作為關聯物件依附到被hook 的物件上

3.建立AspectIdentifier物件,並且新增到AspectsContainer物件裡儲存起來。這個過程分為兩步 生成block的方法簽名NSMethodSignature 對比block的方法簽名和待hook的方法簽名是否相容(引數個數,按照順序的型別) 4.根據hook例項物件/類物件/類元物件的方法做不同處理。

A)類方法來hook的時候,分為兩步

1.hook類物件的forwoardInvocation:方法,指向一個靜態的C方法,
2.並且建立一個aspects_ forwoardInvocation:動態新增到之前的類中

3.hook類物件的viewWillAppear:方法讓其指向_objc_msgForward,
4.動態新增aspects_viewWillAppear:指向最初的viewWillAppear:實現
複製程式碼

B)Hook例項的方法

Aspects支援只hook一個物件的例項方法

只不過在第4步略有出入,當hook一個物件的例項方法的時候:

1.新建一個子類,_Aspects_ViewController,並且按照上述的方式hook forwoardInvocation:

2.hook _Aspects_ViewController的class方法,讓其返回ViewController
hook _Aspects_ViewController_MetaClass,讓其返回ViewController

3.呼叫objc_setClass來修改ViewController的類為_Aspects_ViewController

這樣做,就可以通過object_getClass(self)獲得類名,然後看看是否有字首類名來判斷是否被hook過了
複製程式碼

hook例項方法詳解

TestClass *testObj = [[TestClass alloc] init];

    [testObj aspect_hookSelector:NSSelectorFromString(@"testSelector")
                     withOptions:AspectPositionBefore
                      usingBlock:^(id<AspectInfo> aspectInfo) {
             
                            NSLog(@"Hook testSelector");
                                                                       }
                           error:NULL];
    [testObj testSelector];
複製程式碼

hook之前例項的狀態.png

hook的過程: 1、通過statedClass = self.class獲取self本來的class (class方法被重寫了,用來獲取self被hook之前的Class(Target))

2、通過Class baseClass = object_getClass(self)獲取self的isa指標實際指向的class (self在執行時實際的class,表面上看這是一個西瓜(statedClass),實際上這是一個蘋果(basedClass))

3、如果baseClass(實際指向的class)已經是被hook過的子類,則返回baseClass。

4.如果baseClass是MetaClass或者被KVO過的Class,則不必再生成subClass,直接在其自身上進行method swizzling。

5.如果不是上述3、4 所述情況,預設情況下需要對被hook的Class進行”isa swizzling”:

1)通過subclass = objc_allocateClassPair(baseClass, subclassName, 0)動態建立一個被hook類(TestClass)的子類(TestClass_Aspects); 2)然後對子類(TestClass_Aspects)的forwardInvocation:進行method swizzling,替換為_ASPECTS_ARE_BEING_CALLED_,進行訊息轉發時,實際執行的是_ASPECTS_ARE_BEING_CALLED_中的方法; 3)重寫子類(TestClass_Aspects)的獲取類名的方法class,使其返回被hook之前的類的類名(TestClass); 4)將self(TestObj)的isa指標指向子類(TestClass_Aspects)

object_setClass(self, subclass)
//object_setClass將一個物件設定為別的類型別,返回原來的Class
複製程式碼

class被hook後的情況:

hook後.png

相關文章