一、背景
網(wǎng)絡(luò)監(jiān)控主要用于監(jiān)控應(yīng)用的網(wǎng)絡(luò)請(qǐng)求,獲取網(wǎng)絡(luò)請(qǐng)求相關(guān)的性能參數(shù),方便開(kāi)發(fā)、測(cè)試、產(chǎn)品等人員對(duì)應(yīng)用進(jìn)行分析。
二、指標(biāo)
一般監(jiān)控的指標(biāo)主要有:成功率、狀態(tài)碼、流量、網(wǎng)絡(luò)響應(yīng)時(shí)間、HTTP與HTTPS的 DNS 解析、TCP握手、SSL握手(HTTP除外)、首包時(shí)間等。
三、思路
iOS中常見(jiàn)的網(wǎng)絡(luò)請(qǐng)求
蘋果官方:

第三方框架:

NSURLConnection在iOS9之后,已經(jīng)被蘋果廢棄,取而代之的是iOS7之后出現(xiàn)的NSURLSession。
現(xiàn)在各方案的使用率大概如下:

從圖中可以看出,現(xiàn)在iOS中使用得最多的網(wǎng)絡(luò)請(qǐng)求依次是AFNetworking、NSURLSession、NSURLConnection。
iOS網(wǎng)絡(luò)框架層級(jí)關(guān)系如下所示:

從圖中可以看出,iOS網(wǎng)絡(luò)框架由4層組成。最底層的即是系統(tǒng)層的BSD Sockets。中間的框架層又分為兩級(jí),CFNetwork是純C語(yǔ)言實(shí)現(xiàn)的,NSURLConnection、NSURLSession、UIWebView是用Objective-C實(shí)現(xiàn)的,而且都調(diào)用的CFNetwork。最上面應(yīng)用層的AFNetworking是第三方框架,是基于NSURLConnection和NSURLSession實(shí)現(xiàn)的。
iOS AOP編程主要方式
- Method Swizzling
每一個(gè)NSObject類都包含一個(gè)isa指針,指向objc_class結(jié)構(gòu)體,而每一個(gè)objc_class結(jié)構(gòu)體又包含一個(gè)methodLists指針,指向objc_method_list結(jié)構(gòu)體數(shù)組,在objc_method_list里又包含一個(gè)objc_method結(jié)構(gòu)體成員,且每一個(gè)objc_method包含一個(gè)method_imp指針,指向方法實(shí)現(xiàn)。因此,只要能修改method_imp的值,就能替換原有的實(shí)現(xiàn)。
- Proxy
在Objective-C里,NSProxy是除NSObject外唯一的根類。NSProxy是一個(gè)實(shí)現(xiàn)了NSObject協(xié)議的抽象類,它的正常運(yùn)作需要子類override-methodSignatureForSelector:方法為sel提供方法簽名,以及-forwardInvocation:方法來(lái)完成調(diào)用的轉(zhuǎn)發(fā)。使用Proxy來(lái)注入NSURLConnection、NSURLSession等對(duì)delegate的回調(diào)。具體來(lái)說(shuō),在delegate proxy收到消息時(shí),如果不是目標(biāo)協(xié)議方法,則通過(guò)消息轉(zhuǎn)發(fā)機(jī)制,轉(zhuǎn)發(fā)給原delegate;如果是目標(biāo)協(xié)議方法,則直接調(diào)用proxy實(shí)現(xiàn),在proxy實(shí)現(xiàn)中委托調(diào)用原delegate;此外,多數(shù)協(xié)議和協(xié)議方法都是可選的,因此,在proxy的實(shí)現(xiàn)中需要實(shí)現(xiàn)-conformsToProtocol:和-respondsToSelector:方法來(lái)聲明proxy額外加入的協(xié)議和方法。
- FishHook
使用fishhook來(lái)替換動(dòng)態(tài)鏈接庫(kù)中的C函數(shù)實(shí)現(xiàn),具體來(lái)說(shuō)是CFNetwork和CoreFoundation中的相關(guān)函數(shù)。在程序運(yùn)行時(shí),動(dòng)態(tài)鏈接的C函數(shù)dynamic(...)地址記錄在DATA segment下的la_symbol_ptr中;初始時(shí),程序只知道dynamic函數(shù)的符號(hào)名而不知道函數(shù)的實(shí)現(xiàn)地址;首次調(diào)用時(shí),程序通過(guò)TEXT segment中的stub_helper取得綁定信息,通過(guò)dyld_stub_binder來(lái)更新la_symbol_ptr中的符號(hào)實(shí)現(xiàn)地址;這樣,再次調(diào)用時(shí),就可以通過(guò)la_symbol_ptr直接找到dynamic函數(shù)的實(shí)現(xiàn);如果我們需要替換dynamic函數(shù)的實(shí)現(xiàn),只需要修改_la_symbol_ptr即可。
- NSURLProtocol
NSURLProtocol是iOS中URL Loading System的一部分。如果開(kāi)發(fā)者自定義的一個(gè)NSURLProtocol并且注冊(cè)到app中,那么在這個(gè)自定義的NSURLProtocol中我們可以攔截UIWebView,基于系統(tǒng)的NSURLConnection或者NSURLSession進(jìn)行封裝的網(wǎng)絡(luò)請(qǐng)求,然后做到自定義的response返回。
四、實(shí)現(xiàn)
根據(jù)前面的分析和比較,可以得出網(wǎng)絡(luò)監(jiān)控的實(shí)現(xiàn)方案。
監(jiān)聽(tīng)的網(wǎng)絡(luò)框架
可能需要監(jiān)聽(tīng)的網(wǎng)絡(luò)框架包括:AFNetworking、NSURLConnection、NSURLSession、CFNetwork。
首先,由于CFNetwork位于較低層,在iOS10之后,很多方法hook不了,因此放棄對(duì)CFNetwork的監(jiān)控。
其次,AFNetworking位于最上層的應(yīng)用層,是基于NSURLConnection、NSURLSession實(shí)現(xiàn)的,因此只要監(jiān)控了NSURLConnection、NSURLSession,就相當(dāng)于監(jiān)控了AFNetworking。因此,選擇監(jiān)控的網(wǎng)絡(luò)框架為NSURLSession和NSURLConnection。
使用的AOP方式
因?yàn)镹SURLConnection和NSURLSession使用的是Objective-C實(shí)現(xiàn)的,所以用不上fishhook,但是要用到Method Swizzling、Proxy以及URLProtocol。
對(duì)于網(wǎng)絡(luò)時(shí)間的監(jiān)控
單獨(dú)把網(wǎng)絡(luò)時(shí)間的監(jiān)控提出來(lái),是因?yàn)榫W(wǎng)絡(luò)時(shí)間監(jiān)控稍微復(fù)雜。因?yàn)樵诟邔泳W(wǎng)絡(luò)框架中,對(duì)于各階段執(zhí)行的時(shí)間節(jié)點(diǎn)都被封裝起來(lái)了。需要到BSDSocket級(jí)別才能監(jiān)控到這些時(shí)間。但是經(jīng)過(guò)實(shí)驗(yàn),在iOS10之后,可能是系統(tǒng)進(jìn)行了加固,BSDSocket中有些方法沒(méi)有辦法進(jìn)行hook了。在iOS10之后,NSURLSession中有API提供了對(duì)應(yīng)的時(shí)間數(shù)據(jù)。
從蘋果官網(wǎng)獲取當(dāng)前蘋果手機(jī)系統(tǒng)版本使用占比如下圖所示:

其中iOS11約占65%,iOS10約占28%,低于iOS10的約占7%。可見(jiàn)iOS10以上系統(tǒng)占了93%,覆蓋了絕大部分用戶。因此,對(duì)于網(wǎng)絡(luò)時(shí)間的監(jiān)控,放棄hook底層BSDSocket,直接使用NSURLSession提供的數(shù)據(jù)。
需要hook的方法
對(duì)于NSURLSession:

對(duì)于NSURLConnection:

整體設(shè)計(jì)
如下圖所示為SDK的整體結(jié)構(gòu):

下圖為SDK整體時(shí)序:

五、實(shí)例
iOS端網(wǎng)絡(luò)監(jiān)控SDK及Demo見(jiàn)github地址:
https://github.com/frog78/NetworkMonitor
六、參考
http://www.cocoachina.com/ios/20170302/18815.html
http://www.cocoachina.com/ios/20170629/19680.html
http://www.cocoachina.com/ios/20170630/19683.html
https://juejin.im/post/5b1602906fb9a01e3542f08c