本文為L(zhǎng)_Ares個(gè)人寫作,以任何形式轉(zhuǎn)載請(qǐng)表明原文出處。
一、切面編程AOP
AOP,其實(shí)這種思想在之前的Method_Swizzling用到過(guò)。
在實(shí)際開發(fā)中,你是否出現(xiàn)過(guò)一種開發(fā)需求 :
需求 :
1. 不修改原有的方法。
2. 但想要在原有的函數(shù)實(shí)現(xiàn)的最開始位置或最末尾位置添加一些代碼,又不改變?cè)泻瘮?shù)的其他代碼。
3. 或者直接想在某個(gè)地方替換掉原有函數(shù)的實(shí)現(xiàn)。
4. 又或者做埋點(diǎn)。這樣的情況下,就需要用到
切面編程思想,也是我們常說(shuō)的hook。
問題1 : 什么是AOP面向切面
AOP:
- 英文全稱 :
Aspect Oriented Programming。- 中文全稱 : 面向切面的程序設(shè)計(jì)、剖面導(dǎo)向程序設(shè)計(jì)。
- AOP是計(jì)算機(jī)科學(xué)中的一種程序設(shè)計(jì)思想,只是一種編程范式,并不是具體的某種實(shí)現(xiàn)。
切入點(diǎn): 一般情況下,在iOS中,要被hook的類或者要被hook的方法可以叫做切入點(diǎn)。
切面: 一般情況下,在iOS中,加入到切入點(diǎn)的代碼片段可以簡(jiǎn)單的理解為切面。
切面編程思想: 一般情況下,在運(yùn)行時(shí),動(dòng)態(tài)的將代碼切入到類的指定方法、指定位置上的思想可以叫做AOP面向切面思想。
實(shí)現(xiàn)方式: iOS中,多數(shù)是通過(guò)代理機(jī)制實(shí)現(xiàn)。
(1).
AOP可以理解為對(duì)OOP(面向?qū)ο笏枷?的一種補(bǔ)充,它可以解決OOP中不同類中的相同代碼造成的代碼冗余,卻不會(huì)造成高耦合。
(2).OOP和AOP組成了一個(gè)坐標(biāo)軸,OOP是X軸,AOP是Y軸。
(3).OOP在橫向的分出了很多的類進(jìn)行封裝,很好的解決了職責(zé)分配的問題。
(4).AOP在縱向上向特定的代碼中動(dòng)態(tài)的加入需要的特定代碼。
問題2 : 切面編程的常見應(yīng)用場(chǎng)景
1. 參數(shù)校驗(yàn) : 例如網(wǎng)絡(luò)請(qǐng)求發(fā)送前的參數(shù)校驗(yàn),網(wǎng)絡(luò)請(qǐng)求返回?cái)?shù)據(jù)的格式校驗(yàn)。
2. 無(wú)痕埋點(diǎn) : 統(tǒng)一處理埋點(diǎn),降低代碼的耦合度。
3. 頁(yè)面統(tǒng)計(jì) : 幫助統(tǒng)計(jì)頁(yè)面的訪問量。
4. 事務(wù)處理 : 攔截指定事件,添加觸發(fā)事件。
5. 異常處理 : 當(dāng)代碼出現(xiàn)異常報(bào)錯(cuò)時(shí),可以使用面向切面的方式提前做處理,防止crash。
6. 熱修復(fù) : AOP可以讓我們的代碼在被執(zhí)行前被替換為另一段代碼,可以根據(jù)這個(gè)思路,修復(fù)Bug。
7. 代碼分層 : 業(yè)務(wù)邏輯層和非業(yè)務(wù)邏輯層的方法進(jìn)行分層,既不影響業(yè)務(wù)邏輯層,也可以使代碼更有層次性。
二、Aspects庫(kù)的探索
1. 舉例方法進(jìn)行探索
為了方便對(duì)Aspects庫(kù)的思路理解,就通過(guò)舉例的方式進(jìn)行探索。
注意 : 因?yàn)楸菊鹿?jié)內(nèi)容并不少,會(huì)把我所有的想法和解析全部都寫上來(lái),所以這個(gè)例子可能在后面的
Aspects庫(kù)(二)、甚至可能出現(xiàn)的Aspects庫(kù)(三)中全部使用本節(jié)的例子,后面文章不再重復(fù)舉例。
操作
1.1 創(chuàng)建一個(gè)iOS的Project,并且利用cocoapods導(dǎo)入Aspects庫(kù)

1.2 打開xcworkspace文件,并且在ViewController.h文件中導(dǎo)入<Aspects/Aspects.h>

1.3 在ViewController.h中聲明方法- (void)testName:(NSString *)str Age:(int)age Sex:(NSString *)sex;并且在ViewController.m中實(shí)現(xiàn)如下圖

1.4 在viewDidLoad中利用Aspects庫(kù)的公開API對(duì)上圖中的方法進(jìn)行hook,并調(diào)用它

1.5 觀察上述操作之后的舉例運(yùn)行結(jié)果

1.6 從觀察結(jié)果可以得到結(jié)論如下
1.
Aspects庫(kù)的公開API入口是aspect_hookSelector: WithOptions: usingBlock: error方法。
2.AspectPositionBefore也就是options參數(shù)控制了block參數(shù)所持有的函數(shù)的執(zhí)行位置。
3. 被Aspects庫(kù)進(jìn)行hook的方法,它的SEL發(fā)生了改變。
1.7 探索思路
1. 首先,從
Aspects庫(kù)的公開API入口進(jìn)入Aspects庫(kù)。
2. 要知道options都有什么可以選擇的參數(shù),并且知道block參數(shù)中的函數(shù)是怎么被執(zhí)行的。
3. 為什么被hook的方法的SEL發(fā)生了改變,這是利用了什么思想和方法。
2. Aspects庫(kù)的入口之Aspects.h文件
經(jīng)過(guò)上面探索,我們可以知道,我們是利用了aspect_hookSelector: withOptions: usingBlock: error:方法對(duì)想要被hook的方法進(jìn)行的改變。
所以,就從這個(gè)方法來(lái)進(jìn)入Aspects庫(kù)的探索。
操作
進(jìn)入
Aspects庫(kù)的Aspects.h文件,查看其公開的方法和類、屬性、方法等。
2.1 AspectOptions
這是公開API中的
options參數(shù)的可選值。是一個(gè)枚舉類型,一共有4個(gè)枚舉值可選擇。詳細(xì)情況見下圖2.2.0的注釋。

2.2 AspectToken
遵循NSObject協(xié)議,是
Aspects庫(kù)的一個(gè)令牌,可以用來(lái)注銷一個(gè)aspect對(duì)象,也就是可以注銷一個(gè)hook。詳細(xì)情況見下圖2.2.1的注釋。

2.3 AspectInfo
這是
Aspect庫(kù)的協(xié)議。詳細(xì)情況見下圖2.2.2的注釋。

2.4 Aspects
這是
Aspects對(duì)象。
- 它利用了
Runtime的消息轉(zhuǎn)發(fā)機(jī)制進(jìn)行hook。Aspects對(duì)象用來(lái)hook的會(huì)使系統(tǒng)產(chǎn)生一些性能上的消耗,所以不要使用它對(duì)頻繁被調(diào)用的方法進(jìn)行hook。Aspects主要針對(duì)的對(duì)象是view和controller中的方法。Aspects對(duì)象可以利用hook后返回的AspectToken進(jìn)行注銷。Aspects對(duì)象是線程安全的。Aspects是NSObject的分類,之所以選擇做NSObject的分類,是因?yàn)镺C中大多數(shù)的類都是NSObject類的子類。Aspects不支持hook靜態(tài)方法,也就是我們常說(shuō)的類方法。

2.5 AspectErrorCode
這是
Aspects庫(kù)的公開API發(fā)生錯(cuò)誤的時(shí)候,返回的錯(cuò)誤代碼。詳情見下圖2.2.4注釋。

3. Aspects庫(kù)的入口之Aspects.m文件
操作
commond+鼠標(biāo)左鍵點(diǎn)擊我們?cè)谂e例的時(shí)候使用的Aspects庫(kù)的公開API,選擇進(jìn)入它的實(shí)現(xiàn)。

3.1 公開API(id<AspectToken>)aspect_hookSelector: withOptions: usingBlock: error:
可以看到,兩個(gè)公開的API是名稱是一模一樣的,在下面的實(shí)現(xiàn)中也可以知道,兩個(gè)API的實(shí)現(xiàn)也是一摸一樣的。
Aspects庫(kù)的作者雖然不知道是不是蘋果的官方人員,但是他的這種中間層設(shè)置模式和蘋果官方的思想如出一轍。
參數(shù)
selector: 被hook的方法,也就是上面舉例中的testName:Age:Sex:。options: 枚舉值。確定block參數(shù)中的函數(shù)在selector參數(shù)所代表的方法中的執(zhí)行位置。可選值 :
AspectPositionAfter: 在原始方法實(shí)現(xiàn)后,調(diào)用block參數(shù)中的函數(shù),這是Aspects庫(kù)默認(rèn)的options。AspectPositionInstead: 用block參數(shù)中的函數(shù)替換被hook的方法的原有實(shí)現(xiàn)。AspectPositionBefore: 在原始方法實(shí)現(xiàn)前,調(diào)用block參數(shù)中的函數(shù)AspectOptionAutomaticRemoval: 在第一次執(zhí)行后,移除掉hook。也就是說(shuō),用這個(gè)參數(shù)的話,被hook的方法只能被hook一次。block: 用來(lái)插入或者替換selector參數(shù)所指向的方法的代碼。它的第一個(gè)參數(shù)一定是id<AspectInfo>,可以選擇寫與不寫,第二個(gè)參數(shù)開始就是被hook的方法的參數(shù),也就是舉例中的name、age、sex。如果選擇寫上這些參數(shù),則這些參數(shù)會(huì)被加入到block的簽名信息中。你也可以不寫block的參數(shù)或者只使用id<AspectInfo>參數(shù)。error: 該方法如果發(fā)生錯(cuò)誤的話,這里會(huì)回調(diào)錯(cuò)誤信息。
方法實(shí)現(xiàn)

可以看到,無(wú)論是實(shí)例方法還是類方法都是調(diào)用了
aspect_add()函數(shù),所以Aspects庫(kù)的公開API實(shí)現(xiàn)的本質(zhì)就是aspect_add()函數(shù)的實(shí)現(xiàn)。這里利用了蘋果的一種代碼結(jié)構(gòu)的設(shè)計(jì)思想,就是中間層模式,全部利用中間層來(lái)進(jìn)行實(shí)現(xiàn),體現(xiàn)了低耦合性。
3.2 aspect_add()函數(shù)
這個(gè)函數(shù)就是
Aspects庫(kù)的公開API的實(shí)現(xiàn)本質(zhì)。
注意 : 因?yàn)?code>Aspects庫(kù)的公開API只有兩個(gè),又全部采用了中間層的方法設(shè)計(jì)模式,所以,
aspect_add()函數(shù)要被分成幾個(gè)部分來(lái)寫,本節(jié)重點(diǎn)記錄的是Aspects庫(kù)對(duì)被hook的方法和被hook的類的校驗(yàn)邏輯。
參數(shù)
self: 調(diào)用API的類,也就是被hook的類,在舉例中就是ViewController。selector: 被hook的方法,在舉例中就是testName:Age:Sex:。options:block的執(zhí)行位置,枚舉值,上面的3.1中說(shuō)過(guò)了,不再贅述。block: hook后要執(zhí)行或者替換的代碼塊。error:aspect_add()函數(shù)的錯(cuò)誤信息。
函數(shù)實(shí)現(xiàn)
3.2.1 整體邏輯


通過(guò)
圖2.3.2的源碼可以看出,aspect_add()函數(shù)的實(shí)現(xiàn)本質(zhì)的核心都在被收縮起來(lái)的block函數(shù)塊里面。
3.2.2 aspect_performLocked
這是一把
OSSpinLock的鎖對(duì)象,之前在鎖的章節(jié)說(shuō)過(guò),它是自旋鎖,并且現(xiàn)在它的底層實(shí)現(xiàn)是基于os_unfair_lock的。

3.2.3 aspect_isSelectorAllowedAndTrack
這是驗(yàn)證利用
Aspects庫(kù)進(jìn)行hook的方法(selector)是否符合Aspects庫(kù)的標(biāo)準(zhǔn),以及對(duì)元類對(duì)象(也就是類)做改變。
步驟1 : 建立黑名單

步驟2 : 檢查黑名單

步驟3 : 額外的檢查

這里我們可以得到兩個(gè)要素 :
- 如果要hook的方法是
dealloc方法,那么,position參數(shù)只能選擇AspectPositionBefore枚舉值。- 如果要hook的方法并沒有被要hook的類聲明且實(shí)現(xiàn),是不可以被hook的。
步驟4 : 元類對(duì)象的操作
(1). 首先,判斷被hook的類是不是元類對(duì)象(類)

看圖2.3.7,
if判斷中使用的是object_getClass,這就和下面的[self class]形成了對(duì)比。
object_getClass(self): 獲取self的isa指針指向。[self class]:
- 如果
self是實(shí)例對(duì)象,也就是由類初始化構(gòu)造出來(lái)的對(duì)象,那么結(jié)果是類。- 而如果
self是類對(duì)象,也就是由元類初始化構(gòu)造出來(lái)的對(duì)象,那么結(jié)果是類本身。
(2). 如果進(jìn)入了if,那么代表被hook的對(duì)象就是元類對(duì)象。
(3). 如果沒有進(jìn)入if,則返回YES,證明通過(guò)了Aspects對(duì)被hook對(duì)象和方法的驗(yàn)證。
(4). if中,對(duì)元類操作的邏輯。
首先,利用
[self class]獲取類。
這里一定要看上面[self class]和object_getClass(self)的區(qū)別,因?yàn)檫@里無(wú)論self是實(shí)例對(duì)象還是類,獲得的klass和currentClass都是普通的類,而不是元類。
而swizzledClassesDict是一個(gè)靜態(tài)的、通過(guò)單例創(chuàng)建的字典,保證了唯一性。

其次,兩個(gè)
do{...}while()循環(huán),循環(huán)條件都是追溯currentClass的繼承鏈,也就是被hook的對(duì)象所屬的類的繼承鏈。
所以,對(duì)元類對(duì)象(類)的hook的可執(zhí)行條件,就是兩個(gè)do{...}while()循環(huán)中的邏輯。
(5). 先看第二個(gè)do{...}while()循環(huán)。
想要進(jìn)入到第二個(gè)
do{...}while()循環(huán),則必定不會(huì)在第一個(gè)do{...}while()循環(huán)的時(shí)候出現(xiàn)return的現(xiàn)象。之所以先看第二個(gè)循環(huán),是因?yàn)槲覀冃枰?code>AspectTracker到底是作為一個(gè)怎樣的類。

這里我們可以知道 :
- 父類的
tracker對(duì)象中,parentEntry屬性是子類的tracker對(duì)象。- 對(duì)于元類對(duì)象來(lái)說(shuō),只要子類被hook,其繼承鏈上的所有父類的
tracker的selectorNames集合都會(huì)存儲(chǔ)被hook的方法名稱。AspectTracker是一個(gè)追蹤器。
trackerClass表示被追蹤的類。selectorNames存儲(chǔ)被hook的類中被hook的方法的名稱,被hook的類的父類的tracker的selectorNames集合也會(huì)存儲(chǔ)被hook的子類的被hook的方法的名稱。parentEntry存放下一級(jí)別子類的追蹤器。


(6). 再看第一個(gè)do{...}while()循環(huán)。
先看第一個(gè)do{...}while()的整體結(jié)構(gòu)。

再看其中最重點(diǎn)的if結(jié)構(gòu)。

從圖2.3.13中可以得出 :
每個(gè)類的層次結(jié)構(gòu)或者說(shuō)繼承鏈中,相同的方法只能被該層次中的類hook一次,不能同時(shí)多次hook同一方法。
注釋
校驗(yàn)之后的核心內(nèi)容,將會(huì)放入下一節(jié),AOP之Aspects庫(kù)(二)進(jìn)行探索。