NSURLProtocol探究及實踐

原文見我的個人博客

初識NSURLProtocol 及 URL Loading System

Hybrid應(yīng)用逐漸普遍,對于iOS開發(fā)虱肄,NSURLProtocol為其提供了許多重要的Hybrid能力咏窿。
說到NSURLProtocol平斩,首先要提到URL Loading System绘面,后者支持著整個App訪問URL指定內(nèi)容晚凿。根據(jù)文檔配圖,其結(jié)構(gòu)大致如下:

URL Loading System

都有哪些網(wǎng)絡(luò)請求經(jīng)由URL Loading System呢肆氓? 從上圖可以看出,包括NSURLConnection、NSURLSession等均是經(jīng)由該加載系統(tǒng)鼻百。而直接使用CFNetwork的請求并不經(jīng)過此系統(tǒng)(ASIHTTPRequest使用CFNetwork)温艇,同時,WKWebView使用了WebKit堕汞,也不經(jīng)過該加載系統(tǒng)勺爱。

在整個URL Loading System中,NSURLProtocol并不負(fù)責(zé)主要處理邏輯讯检,其作為一個工具獨(dú)立于URL Loading的業(yè)務(wù)邏輯琐鲁。攔截所有經(jīng)由URL Loading System的網(wǎng)絡(luò)請求并處理,是一個存在于切面的抽象類人灼。也就是說围段,我們通過URLProtocol,可以攔截/處理URLConnection投放、URLSession奈泪、UIWebView的請求,對于WebKit(WKWebView)可以通過使用私有API實現(xiàn)攔截WKWebView的請求。同時段磨,iOS11之后提供了WKURLSchemeHandler實現(xiàn)攔截邏輯取逾。

使用URLProtocol

URL為抽象類,需要繼承并實現(xiàn)以下方法:

class func canInit(with request: URLRequest) -> Bool
class func canonicalRequest(for request: URLRequest) -> URLRequest
init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?)
func startLoading()
func stopLoading()

注冊URLProtocol

想要通過子類攔截請求苹支,我們需要注冊該類

// URLConnection砾隅、UIWebView、WKWebView使用URLProtocol的registerClass:方法
class func registerClass(_ protocolClass: AnyClass) -> Bool
// URLSession 使用 URLSessionConfiguration的protocolClasses屬性
var protocolClasses: [AnyClass]? { get set }

攔截請求

URLProtocol選擇是否攔截請求的時候债蜜,會調(diào)用如下方法:
class func canInit(with request: URLRequest) -> Bool
我們可以根據(jù)該request上下文判斷是否要處理晴埂,如判斷當(dāng)前URL scheme,從而處理我們自定義的url請求寻定,實現(xiàn)前端對本地沙盒的直接讀取儒洛。后文將會演示該實現(xiàn)方式。

處理請求

攔截請求后狼速,我們可以根據(jù)需要對該請求進(jìn)行進(jìn)一步處理琅锻。

我們可以根據(jù)請求內(nèi)容,對其重新包裝向胡,然后進(jìn)行下一步處理恼蓬。
class func canonicalRequest(for request: URLRequest) -> URLRequest
在此方法中,我們根據(jù)原request的上下文僵芹,生成一個新request并備用处硬。

上面是URLProtocol的入口方法,下面則是具體處理邏輯:
當(dāng)我們攔截了請求時拇派,系統(tǒng)將會要求我們創(chuàng)建一個URLProtocol實例荷辕,并負(fù)責(zé)所有加載邏輯。
如下方法則是根據(jù)當(dāng)前request生成一個URLProtocol子類實例件豌,進(jìn)行后續(xù)處理工作疮方。
init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?)

接下來進(jìn)入最重要的方法,我們需要在startLoading方法中實現(xiàn)所有自定義加載邏輯
func startLoading()
常見的處理邏輯:

  • 根據(jù)當(dāng)前Request及任何上下文信息茧彤,生成新的邏輯及請求并發(fā)送出去案站。
  • 解析自定義url scheme,讀取本地沙盒文件并返回棘街,實現(xiàn)前端url直接讀取沙盒文件

URLProtocolClient

在我們攔截并處理請求時,我們有時需要把當(dāng)前的處理情況反饋給URL Loading System承边,URLProtocol的client對象則代表了這個反饋信息的接受者遭殉。我們應(yīng)在處理過程的適當(dāng)位置使用這些回調(diào)。
URLProtocolClient協(xié)議包含如下方法

/// 緩存是否可用
func urlProtocol(_ protocol: URLProtocol, cachedResponseIsValid cachedResponse: CachedURLResponse)
/// 請求取消
func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge)
/// 請求失敗
func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error)
/// 成功加載數(shù)據(jù)
func urlProtocol(_ protocol: URLProtocol, didLoad data: Data)
/// 收到身份驗證請求
func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge)
/// 接收到Response
func urlProtocol(_ protocol: URLProtocol, didReceive response: URLResponse, cacheStoragePolicy policy: URLCache.StoragePolicy)
/// 請求被重定向
func urlProtocol(_ protocol: URLProtocol, wasRedirectedTo request: URLRequest, redirectResponse: URLResponse)
/// 加載過程結(jié)束博助,請求完成
func urlProtocolDidFinishLoading(_ protocol: URLProtocol)

實戰(zhàn)應(yīng)用

URLProtocol攔截常用于 hybrid應(yīng)用的前端-客戶端交互如實現(xiàn)網(wǎng)頁對沙盒文件訪問险污、瀏覽器數(shù)據(jù)攔截等,以下介紹兩種常見case:
工程代碼可見:此鏈接

Hybrid應(yīng)用

Hybrid應(yīng)用較為常見,經(jīng)常存在網(wǎng)頁需要訪問本地目錄的需求蛔糯,包括存儲clientvar拯腮、獲取客戶端cache、訪問沙盒文件等蚁飒。
若不適用URLProtocol动壤,上述過程可以通過前端通知客戶端提供某資源->客戶端通過接口傳輸資源這一過程實現(xiàn)。但存在適配復(fù)雜淮逻,兩過程分離等問題琼懊。而通過URLProtocol攔截請求,可使這一過程對前端透明爬早,其無須關(guān)心數(shù)據(jù)請求邏輯哼丈。

示例代碼見LocalFile目錄

override func startLoading() {
    if let urlStr = request.url?.absoluteString,
        let scheme = request.url?.scheme {
        let startIndex = urlStr.index(urlStr.startIndex, offsetBy: scheme.count + 3)
        let endIndex = urlStr.endIndex
        let imagePath: String = String(urlStr[startIndex..<endIndex])
        
        if let image = UIImage(contentsOfFile: imagePath),
            let data = UIImagePNGRepresentation(image) {
            
            // Logic of Success
            let response = HTTPURLResponse(url: self.request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)
            self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: URLCache.StoragePolicy.notAllowed)
            self.client?.urlProtocol(self, didLoad: data)
            self.client?.urlProtocolDidFinishLoading(self)
            return
        }
    }
    
    // Logic of Failed
    let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil) as Error
    self.client?.urlProtocol(self, didFailWithError: error)
    return
}

上述代碼攔截了前端對于mcimg://的網(wǎng)絡(luò)請求,同時從Bundle中查找該文件并返回請求筛严。該邏輯同樣適用于從本地Cache醉旦、持久化存儲中獲取,實現(xiàn)了native資源獲取與前端資源獲取過程的解耦桨啃。

攔截請求數(shù)據(jù)

對于應(yīng)用內(nèi)置瀏覽器等場景车胡,經(jīng)常需要記錄用戶訪問了那些網(wǎng)頁等信息,并進(jìn)行危險提示优幸、免責(zé)提示吨拍、數(shù)據(jù)統(tǒng)計、競品攔截等工作网杆。此過程同樣可通過URLProtocol攔截實現(xiàn)

override func startLoading() {
    RequestInfoProtocol.requestInfoProtocolDict.insert(request.hashValue)
    NotificationCenter.default.post(name: NSNotification.Name.RequestInfoURL, object: request.url?.absoluteString)
    
    if let newRequest = (request as NSURLRequest).copy() as? URLRequest {
        let newTask = session.dataTask(with: newRequest)
        newTask.resume()
        self.copiedTask = newTask
    }
}

上述代碼實現(xiàn)了收到請求時做出處理邏輯(如通知)羹饰。但由于該請求被攔截將無法繼續(xù)發(fā)至目的地,故復(fù)制該請求并發(fā)起碳却,同時實現(xiàn)下述URLSession方法正確返回response队秩。

extension RequestInfoProtocol: URLSessionDataDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            self.client?.urlProtocol(self, didFailWithError: error)
            return
        }
        self.client?.urlProtocolDidFinishLoading(self)
    }
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        self.client?.urlProtocol(self, didLoad: data)
    }
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        completionHandler(.allow)
    }
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
        completionHandler(proposedResponse)
    }
    func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
        self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
        
        RequestInfoProtocol.requestInfoProtocolDict.remove(request.hashValue)
        let redirectError = NSError(domain: NSURLErrorDomain, code: NSUserCancelledError, userInfo: nil)
        task.cancel()
        self.client?.urlProtocol(self, didFailWithError: redirectError)
    }
}

上述代碼實現(xiàn)了URLSessionDataDelegate,主要作用是將已發(fā)送請求所收到的響應(yīng)昼浦,正確返回給請求者馍资。
通過攔截請求,并按序返回二次確認(rèn)頁面关噪、危險提示頁面等鸟蟹,實現(xiàn)了內(nèi)置瀏覽器攔截需求,并保證了瀏覽器的正常運(yùn)行使兔。

Tips: 上述過程需要使用WebKit私有API建钥,WKWebView在iOS 11開放了WKURLSchemeHandler,流程類似URLProtocol虐沥。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熊经,一起剝皮案震驚了整個濱河市泽艘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镐依,老刑警劉巖匹涮,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異槐壳,居然都是意外死亡然低,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門宏粤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脚翘,“玉大人,你說我怎么就攤上這事绍哎±磁” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵崇堰,是天一觀的道長沃于。 經(jīng)常有香客問我,道長海诲,這世上最難降的妖魔是什么繁莹? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮特幔,結(jié)果婚禮上咨演,老公的妹妹穿的比我還像新娘。我一直安慰自己蚯斯,他們只是感情好薄风,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拍嵌,像睡著了一般遭赂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上横辆,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天撇他,我揣著相機(jī)與錄音,去河邊找鬼狈蚤。 笑死困肩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脆侮。 我是一名探鬼主播僻弹,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼他嚷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤筋蓖,失蹤者是張志新(化名)和其女友劉穎卸耘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粘咖,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚣抗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓮下。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翰铡。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖讽坏,靈堂內(nèi)的尸體忽然破棺而出锭魔,到底是詐尸還是另有隱情,我是刑警寧澤路呜,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布迷捧,位于F島的核電站,受9級特大地震影響胀葱,放射性物質(zhì)發(fā)生泄漏漠秋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一抵屿、第九天 我趴在偏房一處隱蔽的房頂上張望庆锦。 院中可真熱鬧,春花似錦轧葛、人聲如沸搂抒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燕耿。三九已至,卻和暖如春姜胖,著一層夾襖步出監(jiān)牢的瞬間誉帅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工右莱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚜锨,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓慢蜓,卻偏偏與公主長得像亚再,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子晨抡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 前言:NSURLProtocol是NSURLConnection的handle類, 它更像一套協(xié)議,如果遵守這套協(xié)...
    天下林子閱讀 2,170評論 0 3
  • 概覽 緩存組件應(yīng)該說是每個客戶端程序必備的核心組件氛悬,試想對于每個界面的訪問都必須重新請求勢必降低用戶體驗则剃。但是如何...
    默默_David閱讀 1,910評論 1 9
  • title: NSURLProtocol 全攻略author: 全凱description: NSURLProto...
    84a6eed103c0閱讀 10,547評論 6 46
  • 前言 ??因為DNS發(fā)生域名劫持,所以需要手動將URL請求的域名重定向到指定的IP地址如捅,但是由于請求可能是通過NS...
    小盟城主閱讀 5,101評論 5 21
  • 如果每個人都那么單純相處該有多好棍现,就不會去在乎別人的看法,不會在意別人喜不喜歡镜遣。還是一如既往按照自己的方式生活己肮。讓...
    wy5閱讀 119評論 1 4