Runtime窺探 (六)| AOP與Aspects核心源碼

前言

如何把這個世界變得美好?把你自己變得更美好

秋天來了

我們這篇博客繼續來介紹Runtime在開發中的實際應用,通過開源庫Aspects來對runtime有更好的認識和理解。

一、Aspects庫

這個庫是iOS基于AOP編程思想的開源庫,用于跟蹤修改一個指定的類的某個方法執行前/替換/后,同時可以自定義添加一段代碼塊.對這個類的所有對象都會起作用。

所有的調用都會是線程安全的.Aspects 使用了Runtime的消息轉發機制以及method swizzling,會有一定的性能消耗.所有對于過于頻繁的調用,不建議使用 Aspects.Aspects更適用于視圖/控制器相關的等。并不適用于每秒調用不超過1000次的代碼.

二、AOP

基本概念:

  • AOP(Aspect Oriented Programming)是面向切面編程。通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術的編程思想。

  • 利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

主要功能:

  • 日志記錄,性能統計,安全控制,事務處理,異常處理,事務的處理等等。

主要意圖:

  • 將日志記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的代碼。

說明:

  • 我們在開發一個應用程序的時候,把系統分為很多模塊(如:首頁、分類、購物車、我的等模塊)。我們從立體上看就是一個并列的樹形結構,屬于縱向切入系統。也就是OOP的的編程思想(面向對象編程思想),而AOP就屬于橫向切入系統,把整個系統的重復操的的部分提取出來(Log打印、日志記錄、應用系統的異常捕捉及處理等等),由此可見,AOP是OOP的一個有效補充。

  • 假設把應用程序想成一個立體結構的話,OOP的利刃是縱向切入系統,把系統劃分為很多個模塊(如:用戶模塊,文章模塊等等),而AOP的利刃是橫向切入系統,提取各個模塊可能都要重復操作的部分(如:權限檢查,日志記錄等等)。

注意:

  • AOP不是一種具體代碼實現的技術,實際上是編程思想。凡是符合AOP思想的技術編程,都可以看成是AOP的實現。

三、解析Aspects庫

我們經常用的Method Swizzling就是一種AOP思想實現,Aspects是比較很棒的基于AOP編程思想的開源庫,
由于Aspects的代碼較多,我們只是來閱讀Aspects的核心實現思路和流程。

1.Aspects的基本模塊

  • AspectInfo

    • Aspect信息類:用來保存信息的,存放被hook的實例、方法、參數等
  • AspectIdentifier

    • Aspect標識類:用來追蹤一個唯一的aspect,AspectIdentifier對應的實例,里面會包含了單個的 Aspect 的具體信息,包括執行時機,要執行 block 所需要用到的具體信息:包括方法簽名、參數等等。初始化AspectIdentifier的過程實質是把我們傳入的block打包成AspectIdentifier。
  • AspectsContainer

    • 是一個用來存儲所有的aspect的容器,可能存儲實例方法/類方法。所以會有兩種容器
  • AspectTracker

    • AspectTracker來跟蹤要被hook的類

上面這些模塊都是用來輔助核心思想實現的,使開源庫模塊清晰、較高容錯率、職責明確等等,這些模塊還是比較好理解的,就不一一閱讀了。其實很多優秀開源庫都會有類似的模塊(比如信息、容器、唯一標識等等)。下面我們主要了解Aspects的核心思想以及流程。

2.小插曲

為什么大多數開源庫都會有這些模塊?舉個例子:

和女朋友一起去溜一堆狗......

  • 我們怎么來控制一堆狗不亂跑呢?那就用繩子把狗拴起來,把繩子握在手里或者你想綁在腿上,這時候手或腿就是來控制所有狗的工具,也就是我們所說的容器(用來保存所有的個體信息)。

  • 我們怎么來看這個狗的名字、品種、年齡?我們可以制作一個標簽掛在狗脖子上,上面寫著狗的基本信息。也就是我們所說的信息模塊(用來存儲個體基本信息)。

  • 現在女朋友要溜xx這條狗,怎么給她?就需要一個唯一標識狗個體的工具,那我們可以在繩子上有個編號便簽,也就是我們所說的唯一標識模塊(標識某個個體,包含基本信息以及其他輔助信息)。這樣我們可以通過編號就可以找到對應的狗,而且不會找錯,這樣就可以愉快的來遛狗。

  • 上面這些工作都是來輔助我們遛狗(核心模塊)

3.Aspects對外接口以及基本說明

通過源代碼Aspects中可以看到下面兩個對外公開接口用于hook selector

@interface NSObject (Aspects)

/***********************
第一個參數selector:是要給它增加切面的原方法
第二個參數是AspectOptions:是代表這個切片增加在原方法的before / instead / after
第三個入參block:這個block復制了正在被hook的方法的簽名signature類型
第一個參數selector將返回一個遵循<AspectInfo>的id對象,這個對象繼承了方法的所有參數,
這些參數都會被填充到匹配的block的簽名里
你也可以使用一個空block,或者一個簡單的id<AspectInfo>
不支持hook靜態static方法的
返回一個可以用來撤銷aspect的token
***********************/

//hook類方法,hook一個類的所有實例對應的一個方法
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;
//hook實例方法,hook類的一個具體實例對應的一個方法
//為一個具體實例的seletor的執行 之前/或者被替換/之后 添加一個block代碼
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

@end

方法說明:

  • 第一個方法為類方法:也就是說接受者是一個要被hook的類,也就是說hook一個類的所有實例對應的一個方法。會對類進行消息轉發和method swizzling。會對類中methodLists的兩個方法進行修改:

    • 1.forwardInvocation:
      forwardInvocation:的IMP(方法實現)被替換為:__ASPECTS_ARE_BEING_CALLED__,這個函數內部具體執行被hook的selector和切入操作的實現。forwardInvocation: 的本來的IMP被保存在__aspects_forwardInvocation:中。在調用aspect_hookClass()函數會進行forwardInvocation:的替換

    • 2.要被hook的selector:要被hook的selector的IMP被替換為:_objc_msgForward/_objc_msgForward_stret,這個函數用于直接觸發消息轉發機制,而不會在methodLists查找函數來執行。被hook的selector的原始IMP被保存在方法aspects_selector中。在調用aspect_prepareClassAndHookSelector()函數會進行selector的替換

    • 3.當我們調用[objc message];時就直接觸發消息轉發機制調用forwardInvocation:方法,而實現就是__ASPECTS_ARE_BEING_CALLED__這個函數(真正執行的函數實體),從而實現selector的hook。

  • 第二個方法為實例方法:也就是說接受者是一個要被hook的類的實例對象,也就是說hook類的一個具體實例對應的一個方法。這里跟上面區別主要是新建子類用來操作hook,具體步驟如下:
    • 1.新建一個要被hook的類的子類:xxx__Aspects_
    • 2.把要hook的實例的isa指向上面新生成的子類xxx__Aspects_,也就是說當前這個實例變成了子類xxx__Aspects_的實例對象。
    • 3.對上面新建子類xxx__Aspects_進行消息轉發和method swizzling。具體思路就是和上面的類方法流程一樣。

上面是Aspects的核心思想以及流程的簡單說明,下面我們對這些核心代碼進行梳理介紹。

注意:

  • Aspects并不能hook類的類方法
  • Aspects不能hook靜態方法
  • Aspects不能hook類中不存在或者未實現的方法

4.Aspects核心代碼解析

對外接口源碼

1.上圖說明:

  • 不管調用hook類的的實例方法還是類方法,在函數內都會調起私有c函數static id aspect_add()來進行統一處理
  • 使用自旋鎖來保證線程安全執行
  • 然后會進行前期準備工作處理(如黑名單排除、生成標識實例以及添加、block塊的函數簽名和轉換處理等等),這些通過源碼還是比較好理解的
  • 把前期準備工作做完后,就會調用函數aspect_prepareClassAndHookSelector(self, selector, error);來進行核心模塊實現

下面我們對函數aspect_prepareClassAndHookSelector(self, selector, error);來進行查看源碼

2.核心函數aspect_prepareClassAndHookSelector()

函數aspect_prepareClassAndHookSelector()的具體實現如下:

aspect_prepareClassAndHookSelector
  • 函數主要分類兩部分處理:
    • 一個是hook Class的處理,這一部分主要實現上面提到的forwardInvocation:函數的替換具體過程
    • 一個是hook selector的處理,這一部分主要實現上面提到的要把被hook的selector的函數實現替換成_objc_msgForward/_objc_msgForward_stret,直接觸發消息轉發機制調用forwardInvocation:

3.hook Class過程

我們先來看一下核心函數aspect_prepareClassAndHookSelector()中的第一部分hook Class過程,這個會調用aspect_hookClass函數

aspect_hookClass
  • 上面根據self獲取類對象/元類的Class

  • 先對當前類進行篩選,如果當前Class是以xxx_Aspects_后綴結尾的名稱,說明這個Class已經被hook過了,不需要再進行下面重復處理,直接返回當前Class,去執行hook selector的過程(后面會說)

  • 然后再看baseClass是不是元類,object_getClass(self)獲取self的isa指針。如果當前是類對象,則class_isMetaClass(baseClass)是元類,說明當前hook的是某一個類的所有實例的對應方法。直接調用函數aspect_swizzleClassInPlace()(后面介紹)來method swizzling函數forwardInvocation:

  • 判斷當前實例對象的isa指向statedClass和baseClass,按理說當self為實例變量時,object_getClass(self)與[self class]輸出結果一直,均獲得isa指針,即指向類對象的指針。但是這里判斷相不相等?我們上一篇博客說過KVO過的對象的isa會指向一個中間類NSKVONotifying_XXX,所以說不相等時,說明這個實例對象是被KVO觀察的對象。直接調用函數aspect_swizzleClassInPlace()來method swizzling函數forwardInvocation:

  • 上面情況都排除了,說明hook的是某一個類的實例的對應方法,下面就是hook類方法和實例方法的區別了

    • 1.新建當前self的所屬類的子類:xxx__Aspects_
    • 2.調用aspect_swizzleForwardInvocation()替換函數forwardInvocation:的實現
    • 3.調用函數aspect_hookedGetClass(),把新建子類xxx__Aspects_的isa指向self的所屬類,把新建子類xxx__Aspects_的元類的isa指向self的所屬類
    • 4.上面完成后,注冊新類說明新建子類創建完畢
    • 5.把當前self的isa指向新建子類xxx__Aspects_,成功的把self hook成了其子類 xxx_Aspects_,也就是self所屬類是xxx_Aspects_,而不再是原始類xxx了。

上面就是整個hook Class的過程,流程圖如下:

hook Class

上面都會調用有一個函數aspect_swizzleClassInPlace,這個函數的作用就是我們來替換Class系統方法forwardInvocation:的實現,源代碼如下

//替換類的快速消息轉發方法,并把類添加到交換類的集合中
static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    NSString *className = NSStringFromClass(klass);

    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        if (![swizzledClasses containsObject:className]) {
            //不包含,就調用aspect_swizzleForwardInvocation()方法,并把className加入到Set集合里面。
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}

//類的forwardInvocation方法替換為__ASPECTS_ARE_BEING_CALLED__的實現,返回新函數imp
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);

    //替換類中已有方法的實現,返回原來函數imp
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    
    if (originalImplementation) {
        //originalImplementation不為空的話說明原方法有實現,添加一個新方法保存原來類的ForwardInvocation方法實現
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
  • 從上面代碼中看出,把傳入的Class加入到集合中用于hook完成后若需移除時用到,同時調用函數aspect_swizzleForwardInvocation()
  • 函數aspect_swizzleForwardInvocation()中可以看到:使用class_replaceMethod方法把Class的forwardInvocation:函數實現替換成了函數__ASPECTS_ARE_BEING_CALLED__,這個才是真正的函數執行入口。同時把類的原始forwardInvocation:函數實現保存在了__aspects_forwardInvocation:,用于后面hook selector不成功時,調用原始的forwardInvocation:函數來執行或者拋出異常等。

hook Class總結:到這里就把hook Class的過程解析完成了,說到底過程就是處理要被hook的類,同時把類的消息轉發方法forwardInvocation:替換成__ASPECTS_ARE_BEING_CALLED__函數。

4.hook selector過程

在核心函數aspect_prepareClassAndHookSelector()中的hook Class處理在上面已經解析完成,現在我們來繼續往下解析hook selector。這一部分源碼如下:

Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
    //當前imp不是消息轉發方法
    //獲取當前原始的selector對應的IMP的方法編碼typeEncoding
    const char *typeEncoding = method_getTypeEncoding(targetMethod);
    
    //給原始方法添加一個前綴名"aspects__XX"
    SEL aliasSelector = aspect_aliasForSelector(selector);
    
    if (![klass instancesRespondToSelector:aliasSelector]) {
        //沒有找到新方法"aspects__XX",就添加一個新方法
        __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
    }
    
    //我們使用消息轉發forwardInvocation來進行hook
    //把當前的sel方法的替換成forwardInvocation方法,selector被執行的時候,直接會觸發消息轉發從而進入forwardInvocation
    class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
  • 1.獲取要被hook的方法Method以及函數實現IMP指針

  • 2.判斷當前方法實現是不是消息轉發方法,如果是直接返回不作處理,hook不成功。不是繼續往下執行3

  • 3.獲取原始的selector對應的IMP的方法編碼typeEncoding,以及給原始方法添加一個前綴名"aspects__XX",獲取SEL,這個sel保存了原始方法的實現。這樣才hook的過程中,如果不成功會調用這個sel,走原始代碼,不會影響正常函數。

  • 4.如果沒有找到這個方法就調用class_addMethod給這個Class添加一個新方法.

  • 5.調用class_replaceMethod函數來把selector的函數實現替換成forwardInvocation:,這樣調用selector時就回走forwardInvocation:函數,而這個函數在hook Class的過程中也被替換成__ASPECTS_ARE_BEING_CALLED__函數,真正實現的函數入口就是這個。

hook selector總結:說到底這個過程就是處理要被hook的selector,把selector方法的實現替換成的消息轉發方法forwardInvocation:

5.舉例說明核心流程:

  • 新建類A
  • 給類A添加一個對象方法method以及實現
  • 初始化一個A類實例對象a

我們準備hook初始化階段調用下面代碼:

[a aspect_hookSelector:@selector(method) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo>aspectInfo,){
        NSLog(@"arguments = %@",aspectInfo.arguments);    
} error:NULL];
  • 1.因為hook的是實例方法,所以在hook Class的時候會新建子類:A__Aspects_(中間類)
  • 2.調用aspect_swizzleForwardInvocation()把A__Aspects_類的forwardInvocation:函數實現替換成__ASPECTS_ARE_BEING_CALLED__
  • 3.把A__Aspects_的isa指向A,也就是把A__Aspects_類和A類一模一樣。
  • 4.把self,也就是a的所屬類變成A__Aspects_類。
  • 5.給A__Aspects_類的添加一個和原始要被hook的方法一樣的函數,用于保存原始方法的實現
  • 6.替換A__Aspects_類的要被hook的方法,替換成forwardInvocation:函數

我們在hook執行過程調用下面代碼:

[a method];
  • 因為在hook初始化階段時,把method替換成了forwardInvocation:函數。forwardInvocation:函數又被替換成了__ASPECTS_ARE_BEING_CALLED__函數。所以[a method]的函數實現就是__ASPECTS_ARE_BEING_CALLED__函數。
  • __ASPECTS_ARE_BEING_CALLED__函數就是我們hook切入的具體操作。如果我們hook沒有成功時,也會調用原始的selector方法或者拋出異常,不會影響正常函數的實現。

5.Aspects總結

Aspects核心思想就是通過runtime的消息轉發機制和method swizzling生成中間類來替換函數實現。這種思想和上一篇KVO的底層實現很相似。可以仔細閱讀里面的代碼,學習相關的實現思想以及優秀的代碼片段。

我沒有把所有的代碼都一一解析,如果想看代碼注釋的,博客最后會有注釋的項目地址。我對Aspects的庫的中文注釋以及理解說明,有興趣的可以下載看下。

有注釋的Aspects地址:https://git.coding.net/Dely/JYAOPDemo.git

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容