前言
如何把這個世界變得美好湃鹊?把你自己變得更美好
我們這篇博客繼續(xù)來介紹Runtime在開發(fā)中的實際應(yīng)用儒喊,通過開源庫Aspects來對runtime有更好的認識和理解。
一币呵、Aspects庫
這個庫是iOS基于AOP編程思想的開源庫怀愧,用于跟蹤修改一個指定的類的某個方法執(zhí)行前/替換/后,同時可以自定義添加一段代碼塊.對這個類的所有對象都會起作用。
所有的調(diào)用都會是線程安全的.Aspects 使用了Runtime的消息轉(zhuǎn)發(fā)機制以及method swizzling,會有一定的性能消耗.所有對于過于頻繁的調(diào)用,不建議使用 Aspects.Aspects更適用于視圖/控制器相關(guān)的等余赢。并不適用于每秒調(diào)用不超過1000次的代碼.
二芯义、AOP
基本概念:
AOP(Aspect Oriented Programming)是面向切面編程。通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)的編程思想妻柒。
利用AOP可以對業(yè)務(wù)邏輯的各個部分進行隔離扛拨,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性举塔,同時提高了開發(fā)的效率绑警。
主要功能:
- 日志記錄求泰,性能統(tǒng)計,安全控制计盒,事務(wù)處理渴频,異常處理,事務(wù)的處理等等北启。
主要意圖:
- 將日志記錄卜朗,性能統(tǒng)計,安全控制暖庄,事務(wù)處理聊替,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來,通過對這些行為的分離培廓,我們希望可以將它們獨立到非指導(dǎo)業(yè)務(wù)邏輯的方法中,進而改變這些行為的時候不影響業(yè)務(wù)邏輯的代碼春叫。
說明:
我們在開發(fā)一個應(yīng)用程序的時候肩钠,把系統(tǒng)分為很多模塊(如:首頁、分類暂殖、購物車价匠、我的等模塊)。我們從立體上看就是一個并列的樹形結(jié)構(gòu)呛每,屬于縱向切入系統(tǒng)踩窖。也就是OOP的的編程思想(面向?qū)ο缶幊趟枷耄鳤OP就屬于橫向切入系統(tǒng)晨横,把整個系統(tǒng)的重復(fù)操的的部分提取出來(Log打印洋腮、日志記錄、應(yīng)用系統(tǒng)的異常捕捉及處理等等)手形,由此可見啥供,AOP是OOP的一個有效補充。
假設(shè)把應(yīng)用程序想成一個立體結(jié)構(gòu)的話库糠,OOP的利刃是縱向切入系統(tǒng)伙狐,把系統(tǒng)劃分為很多個模塊(如:用戶模塊,文章模塊等等)瞬欧,而AOP的利刃是橫向切入系統(tǒng)贷屎,提取各個模塊可能都要重復(fù)操作的部分(如:權(quán)限檢查,日志記錄等等)艘虎。
注意:
- AOP不是一種具體代碼實現(xiàn)的技術(shù)唉侄,實際上是編程思想。凡是符合AOP思想的技術(shù)編程顷帖,都可以看成是AOP的實現(xiàn)美旧。
三渤滞、解析Aspects庫
我們經(jīng)常用的Method Swizzling就是一種AOP思想實現(xiàn),Aspects是比較很棒的基于AOP編程思想的開源庫榴嗅,
由于Aspects的代碼較多妄呕,我們只是來閱讀Aspects的核心實現(xiàn)思路和流程。
1.Aspects的基本模塊
-
AspectInfo
- Aspect信息類:用來保存信息的嗽测,存放被hook的實例绪励、方法、參數(shù)等
-
AspectIdentifier
- Aspect標(biāo)識類:用來追蹤一個唯一的aspect唠粥,AspectIdentifier對應(yīng)的實例疏魏,里面會包含了單個的 Aspect 的具體信息,包括執(zhí)行時機晤愧,要執(zhí)行 block 所需要用到的具體信息:包括方法簽名大莫、參數(shù)等等。初始化AspectIdentifier的過程實質(zhì)是把我們傳入的block打包成AspectIdentifier官份。
-
AspectsContainer
- 是一個用來存儲所有的aspect的容器只厘,可能存儲實例方法/類方法。所以會有兩種容器
-
AspectTracker
- AspectTracker來跟蹤要被hook的類
上面這些模塊都是用來輔助核心思想實現(xiàn)的舅巷,使開源庫模塊清晰羔味、較高容錯率钠右、職責(zé)明確等等赋元,這些模塊還是比較好理解的飒房,就不一一閱讀了。其實很多優(yōu)秀開源庫都會有類似的模塊(比如信息情屹、容器坪仇、唯一標(biāo)識等等)。下面我們主要了解Aspects的核心思想以及流程垃你。
2.小插曲
為什么大多數(shù)開源庫都會有這些模塊椅文?舉個例子:
和女朋友一起去溜一堆狗......
我們怎么來控制一堆狗不亂跑呢惜颇?那就用繩子把狗拴起來,把繩子握在手里或者你想綁在腿上凌摄,這時候手或腿就是來控制所有狗的工具羡蛾,也就是我們所說的容器(用來保存所有的個體信息)。
我們怎么來看這個狗的名字锨亏、品種痴怨、年齡忙干?我們可以制作一個標(biāo)簽掛在狗脖子上浪藻,上面寫著狗的基本信息捐迫。也就是我們所說的信息模塊(用來存儲個體基本信息)爱葵。
現(xiàn)在女朋友要溜xx這條狗施戴,怎么給她萌丈?就需要一個唯一標(biāo)識狗個體的工具,那我們可以在繩子上有個編號便簽辆雾,也就是我們所說的唯一標(biāo)識模塊(標(biāo)識某個個體,包含基本信息以及其他輔助信息)乾颁。這樣我們可以通過編號就可以找到對應(yīng)的狗涂乌,而且不會找錯英岭,這樣就可以愉快的來遛狗湿右。
上面這些工作都是來輔助我們遛狗(核心模塊)
3.Aspects對外接口以及基本說明
通過源代碼Aspects中可以看到下面兩個對外公開接口用于hook selector
@interface NSObject (Aspects)
/***********************
第一個參數(shù)selector:是要給它增加切面的原方法
第二個參數(shù)是AspectOptions:是代表這個切片增加在原方法的before / instead / after
第三個入?yún)lock:這個block復(fù)制了正在被hook的方法的簽名signature類型
第一個參數(shù)selector將返回一個遵循<AspectInfo>的id對象,這個對象繼承了方法的所有參數(shù)毅人,
這些參數(shù)都會被填充到匹配的block的簽名里
你也可以使用一個空block,或者一個簡單的id<AspectInfo>
不支持hook靜態(tài)static方法的
返回一個可以用來撤銷aspect的token
***********************/
//hook類方法丈莺,hook一個類的所有實例對應(yīng)的一個方法
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
//hook實例方法,hook類的一個具體實例對應(yīng)的一個方法
//為一個具體實例的seletor的執(zhí)行 之前/或者被替換/之后 添加一個block代碼
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
@end
方法說明:
-
第一個方法為類方法:也就是說接受者是一個要被hook的類缔俄,也就是說hook一個類的所有實例對應(yīng)的一個方法。會對類進行消息轉(zhuǎn)發(fā)和method swizzling俐载。會對類中methodLists的兩個方法進行修改:
1.forwardInvocation:
forwardInvocation:
的IMP(方法實現(xiàn))被替換為:__ASPECTS_ARE_BEING_CALLED__
,這個函數(shù)內(nèi)部具體執(zhí)行被hook的selector和切入操作的實現(xiàn)遏佣。forwardInvocation:
的本來的IMP被保存在__aspects_forwardInvocation:
中。在調(diào)用aspect_hookClass()
函數(shù)會進行forwardInvocation:
的替換2.要被hook的selector:要被hook的selector的IMP被替換為:
_objc_msgForward/_objc_msgForward_stret
状婶,這個函數(shù)用于直接觸發(fā)消息轉(zhuǎn)發(fā)機制馅巷,而不會在methodLists查找函數(shù)來執(zhí)行草姻。被hook的selector的原始IMP被保存在方法aspects_selector
中钓猬。在調(diào)用aspect_prepareClassAndHookSelector()
函數(shù)會進行selector的替換3.當(dāng)我們調(diào)用
[objc message];
時就直接觸發(fā)消息轉(zhuǎn)發(fā)機制調(diào)用forwardInvocation:
方法碴倾,而實現(xiàn)就是__ASPECTS_ARE_BEING_CALLED__
這個函數(shù)(真正執(zhí)行的函數(shù)實體),從而實現(xiàn)selector的hook跌榔。
-
第二個方法為實例方法:也就是說接受者是一個要被hook的類的實例對象,也就是說hook類的一個具體實例對應(yīng)的一個方法僧须。這里跟上面區(qū)別主要是新建子類用來操作hook,具體步驟如下:
-
1.新建一個要被hook的類的子類:
xxx__Aspects_
-
2.把要hook的實例的isa指向上面新生成的子類
xxx__Aspects_
担平,也就是說當(dāng)前這個實例變成了子類xxx__Aspects_
的實例對象。 -
3.對上面新建子類
xxx__Aspects_
進行消息轉(zhuǎn)發(fā)和method swizzling暂论。具體思路就是和上面的類方法流程一樣面褐。
-
1.新建一個要被hook的類的子類:
上面是Aspects的核心思想以及流程的簡單說明,下面我們對這些核心代碼進行梳理介紹取胎。
注意:
- Aspects并不能hook類的類方法
- Aspects不能hook靜態(tài)方法
- Aspects不能hook類中不存在或者未實現(xiàn)的方法
4.Aspects核心代碼解析
1.上圖說明:
- 不管調(diào)用hook類的的實例方法還是類方法展哭,在函數(shù)內(nèi)都會調(diào)起私有c函數(shù)
static id aspect_add()
來進行統(tǒng)一處理 - 使用自旋鎖來保證線程安全執(zhí)行
- 然后會進行前期準(zhǔn)備工作處理(如黑名單排除、生成標(biāo)識實例以及添加匪傍、block塊的函數(shù)簽名和轉(zhuǎn)換處理等等),這些通過源碼還是比較好理解的
- 把前期準(zhǔn)備工作做完后役衡,就會調(diào)用函數(shù)
aspect_prepareClassAndHookSelector(self, selector, error);
來進行核心模塊實現(xiàn)
下面我們對函數(shù)aspect_prepareClassAndHookSelector(self, selector, error);
來進行查看源碼
2.核心函數(shù)aspect_prepareClassAndHookSelector()
函數(shù)aspect_prepareClassAndHookSelector()的具體實現(xiàn)如下:
- 函數(shù)主要分類兩部分處理:
- 一個是hook Class的處理薪棒,這一部分主要實現(xiàn)上面提到的
forwardInvocation:
函數(shù)的替換具體過程 - 一個是hook selector的處理手蝎,這一部分主要實現(xiàn)上面提到的要把被hook的selector的函數(shù)實現(xiàn)替換成
_objc_msgForward/_objc_msgForward_stret
盗尸,直接觸發(fā)消息轉(zhuǎn)發(fā)機制調(diào)用forwardInvocation:
- 一個是hook Class的處理薪棒,這一部分主要實現(xiàn)上面提到的
3.hook Class過程
我們先來看一下核心函數(shù)aspect_prepareClassAndHookSelector()
中的第一部分hook Class過程,這個會調(diào)用aspect_hookClass
函數(shù)
上面根據(jù)self獲取類對象/元類的Class
先對當(dāng)前類進行篩選泼各,如果當(dāng)前Class是以
xxx_Aspects_
后綴結(jié)尾的名稱,說明這個Class已經(jīng)被hook過了,不需要再進行下面重復(fù)處理及塘,直接返回當(dāng)前Class锐极,去執(zhí)行hook selector的過程(后面會說)然后再看baseClass是不是元類笙僚,
object_getClass(self)
獲取self的isa指針灵再。如果當(dāng)前是類對象,則class_isMetaClass(baseClass)
是元類翎迁,說明當(dāng)前hook的是某一個類的所有實例的對應(yīng)方法。直接調(diào)用函數(shù)aspect_swizzleClassInPlace()
(后面介紹)來method swizzling函數(shù)forwardInvocation:
判斷當(dāng)前實例對象的isa指向statedClass和baseClass汪榔,按理說當(dāng)self為實例變量時,object_getClass(self)與[self class]輸出結(jié)果一直痴腌,均獲得isa指針,即指向類對象的指針士聪。但是這里判斷相不相等?我們上一篇博客說過KVO過的對象的isa會指向一個中間類
NSKVONotifying_XXX
剥悟,所以說不相等時,說明這個實例對象是被KVO觀察的對象懦胞。直接調(diào)用函數(shù)aspect_swizzleClassInPlace()
來method swizzling函數(shù)forwardInvocation:
-
上面情況都排除了凉泄,說明hook的是某一個類的實例的對應(yīng)方法,下面就是hook類方法和實例方法的區(qū)別了
- 1.新建當(dāng)前self的所屬類的子類:xxx__Aspects_
- 2.調(diào)用
aspect_swizzleForwardInvocation()
替換函數(shù)forwardInvocation:
的實現(xiàn) - 3.調(diào)用函數(shù)
aspect_hookedGetClass()
后众,把新建子類xxx__Aspects_的isa指向self的所屬類,把新建子類xxx__Aspects_的元類的isa指向self的所屬類 - 4.上面完成后蒂誉,注冊新類說明新建子類創(chuàng)建完畢
- 5.把當(dāng)前self的isa指向新建子類xxx__Aspects_,成功的把self hook成了其子類 xxx_Aspects_右锨,也就是self所屬類是xxx_Aspects_,而不再是原始類xxx了。
上面就是整個hook Class的過程讥电,流程圖如下:
上面都會調(diào)用有一個函數(shù)aspect_swizzleClassInPlace
轧抗,這個函數(shù)的作用就是我們來替換Class系統(tǒng)方法forwardInvocation:
的實現(xiàn)恩敌,源代碼如下
//替換類的快速消息轉(zhuǎn)發(fā)方法横媚,并把類添加到交換類的集合中
static Class aspect_swizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
NSString *className = NSStringFromClass(klass);
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
if (![swizzledClasses containsObject:className]) {
//不包含,就調(diào)用aspect_swizzleForwardInvocation()方法灯蝴,并把className加入到Set集合里面。
aspect_swizzleForwardInvocation(klass);
[swizzledClasses addObject:className];
}
});
return klass;
}
//類的forwardInvocation方法替換為__ASPECTS_ARE_BEING_CALLED__的實現(xiàn)绽乔,返回新函數(shù)imp
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
//替換類中已有方法的實現(xiàn),返回原來函數(shù)imp
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
//originalImplementation不為空的話說明原方法有實現(xiàn)折砸,添加一個新方法保存原來類的ForwardInvocation方法實現(xiàn)
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
- 從上面代碼中看出,把傳入的Class加入到集合中用于hook完成后若需移除時用到睦授,同時調(diào)用函數(shù)
aspect_swizzleForwardInvocation()
- 函數(shù)
aspect_swizzleForwardInvocation()
中可以看到:使用class_replaceMethod
方法把Class的forwardInvocation:
函數(shù)實現(xiàn)替換成了函數(shù)__ASPECTS_ARE_BEING_CALLED__
,這個才是真正的函數(shù)執(zhí)行入口。同時把類的原始forwardInvocation:
函數(shù)實現(xiàn)保存在了__aspects_forwardInvocation:
,用于后面hook selector不成功時怖辆,調(diào)用原始的forwardInvocation:
函數(shù)來執(zhí)行或者拋出異常等。
hook Class總結(jié):到這里就把hook Class的過程解析完成了竖螃,說到底過程就是處理要被hook的類,同時把類的消息轉(zhuǎn)發(fā)方法forwardInvocation:
替換成__ASPECTS_ARE_BEING_CALLED__
函數(shù)特咆。
4.hook selector過程
在核心函數(shù)aspect_prepareClassAndHookSelector()
中的hook Class處理在上面已經(jīng)解析完成,現(xiàn)在我們來繼續(xù)往下解析hook selector腻格。這一部分源碼如下:
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
//當(dāng)前imp不是消息轉(zhuǎn)發(fā)方法
//獲取當(dāng)前原始的selector對應(yīng)的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);
}
//我們使用消息轉(zhuǎn)發(fā)forwardInvocation來進行hook
//把當(dāng)前的sel方法的替換成forwardInvocation方法菜职,selector被執(zhí)行的時候旗闽,直接會觸發(fā)消息轉(zhuǎn)發(fā)從而進入forwardInvocation
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
1.獲取要被hook的方法Method以及函數(shù)實現(xiàn)IMP指針
2.判斷當(dāng)前方法實現(xiàn)是不是消息轉(zhuǎn)發(fā)方法蜜另,如果是直接返回不作處理,hook不成功蚕钦。不是繼續(xù)往下執(zhí)行3
3.獲取原始的selector對應(yīng)的IMP的方法編碼typeEncoding鹅很,以及給原始方法添加一個前綴名"aspects__XX"嘶居,獲取SEL促煮,這個sel保存了原始方法的實現(xiàn)。這樣才hook的過程中菠齿,如果不成功會調(diào)用這個sel,走原始代碼绳匀,不會影響正常函數(shù)。
4.如果沒有找到這個方法就調(diào)用
class_addMethod
給這個Class添加一個新方法.5.調(diào)用
class_replaceMethod
函數(shù)來把selector的函數(shù)實現(xiàn)替換成forwardInvocation:
,這樣調(diào)用selector時就回走forwardInvocation:
函數(shù)疾棵,而這個函數(shù)在hook Class的過程中也被替換成__ASPECTS_ARE_BEING_CALLED__
函數(shù),真正實現(xiàn)的函數(shù)入口就是這個是尔。
hook selector總結(jié):說到底這個過程就是處理要被hook的selector,把selector方法的實現(xiàn)替換成的消息轉(zhuǎn)發(fā)方法forwardInvocation:
拟枚。
5.舉例說明核心流程:
- 新建類A
- 給類A添加一個對象方法
method
以及實現(xiàn) - 初始化一個A類實例對象a
我們準(zhǔn)備hook初始化階段調(diào)用下面代碼:
[a aspect_hookSelector:@selector(method) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo>aspectInfo,){
NSLog(@"arguments = %@",aspectInfo.arguments);
} error:NULL];
- 1.因為hook的是實例方法,所以在hook Class的時候會新建子類:A__Aspects_(中間類)
- 2.調(diào)用
aspect_swizzleForwardInvocation()
把A__Aspects_類的forwardInvocation:
函數(shù)實現(xiàn)替換成__ASPECTS_ARE_BEING_CALLED__
恩溅, - 3.把A__Aspects_的isa指向A,也就是把A__Aspects_類和A類一模一樣脚乡。
- 4.把self,也就是a的所屬類變成A__Aspects_類每窖。
- 5.給A__Aspects_類的添加一個和原始要被hook的方法一樣的函數(shù)弦悉,用于保存原始方法的實現(xiàn)
- 6.替換A__Aspects_類的要被hook的方法,替換成
forwardInvocation:
函數(shù)
我們在hook執(zhí)行過程調(diào)用下面代碼:
[a method];
- 因為在hook初始化階段時稽莉,把method替換成了
forwardInvocation:
函數(shù)。forwardInvocation:
函數(shù)又被替換成了__ASPECTS_ARE_BEING_CALLED__
函數(shù)。所以[a method]的函數(shù)實現(xiàn)就是__ASPECTS_ARE_BEING_CALLED__
函數(shù)劈猪。 -
__ASPECTS_ARE_BEING_CALLED__
函數(shù)就是我們hook切入的具體操作。如果我們hook沒有成功時战得,也會調(diào)用原始的selector方法或者拋出異常,不會影響正常函數(shù)的實現(xiàn)常侦。
5.Aspects總結(jié)
Aspects核心思想就是通過runtime的消息轉(zhuǎn)發(fā)機制和method swizzling生成中間類來替換函數(shù)實現(xiàn)。這種思想和上一篇KVO的底層實現(xiàn)很相似聋亡。可以仔細閱讀里面的代碼坡倔,學(xué)習(xí)相關(guān)的實現(xiàn)思想以及優(yōu)秀的代碼片段。
我沒有把所有的代碼都一一解析罪塔,如果想看代碼注釋的,博客最后會有注釋的項目地址垢袱。我對Aspects的庫的中文注釋以及理解說明,有興趣的可以下載看下请契。
有注釋的Aspects地址:https://git.coding.net/Dely/JYAOPDemo.git