NSURLProtocol探究及實踐

原文見我的個人博客

初識NSURLProtocol 及 URL Loading System

Hybrid應用逐漸普遍,對于iOS開發,NSURLProtocol為其提供了許多重要的Hybrid能力。
說到NSURLProtocol,首先要提到URL Loading System,后者支持著整個App訪問URL指定內容。根據文檔配圖,其結構大致如下:

URL Loading System

都有哪些網絡請求經由URL Loading System呢? 從上圖可以看出,包括NSURLConnection、NSURLSession等均是經由該加載系統。而直接使用CFNetwork的請求并不經過此系統(ASIHTTPRequest使用CFNetwork),同時,WKWebView使用了WebKit,也不經過該加載系統。

在整個URL Loading System中,NSURLProtocol并不負責主要處理邏輯,其作為一個工具獨立于URL Loading的業務邏輯。攔截所有經由URL Loading System的網絡請求并處理,是一個存在于切面的抽象類。也就是說,我們通過URLProtocol,可以攔截/處理URLConnection、URLSession、UIWebView的請求,對于WebKit(WKWebView)可以通過使用私有API實現攔截WKWebView的請求。同時,iOS11之后提供了WKURLSchemeHandler實現攔截邏輯。

使用URLProtocol

URL為抽象類,需要繼承并實現以下方法:

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選擇是否攔截請求的時候,會調用如下方法:
class func canInit(with request: URLRequest) -> Bool
我們可以根據該request上下文判斷是否要處理,如判斷當前URL scheme,從而處理我們自定義的url請求,實現前端對本地沙盒的直接讀取。后文將會演示該實現方式。

處理請求

攔截請求后,我們可以根據需要對該請求進行進一步處理。

我們可以根據請求內容,對其重新包裝,然后進行下一步處理。
class func canonicalRequest(for request: URLRequest) -> URLRequest
在此方法中,我們根據原request的上下文,生成一個新request并備用。

上面是URLProtocol的入口方法,下面則是具體處理邏輯:
當我們攔截了請求時,系統將會要求我們創建一個URLProtocol實例,并負責所有加載邏輯。
如下方法則是根據當前request生成一個URLProtocol子類實例,進行后續處理工作。
init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?)

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

  • 根據當前Request及任何上下文信息,生成新的邏輯及請求并發送出去。
  • 解析自定義url scheme,讀取本地沙盒文件并返回,實現前端url直接讀取沙盒文件

URLProtocolClient

在我們攔截并處理請求時,我們有時需要把當前的處理情況反饋給URL Loading System,URLProtocol的client對象則代表了這個反饋信息的接受者。我們應在處理過程的適當位置使用這些回調。
URLProtocolClient協議包含如下方法

/// 緩存是否可用
func urlProtocol(_ protocol: URLProtocol, cachedResponseIsValid cachedResponse: CachedURLResponse)
/// 請求取消
func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge)
/// 請求失敗
func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error)
/// 成功加載數據
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)
/// 加載過程結束,請求完成
func urlProtocolDidFinishLoading(_ protocol: URLProtocol)

實戰應用

URLProtocol攔截常用于 hybrid應用的前端-客戶端交互如實現網頁對沙盒文件訪問、瀏覽器數據攔截等,以下介紹兩種常見case:
工程代碼可見:此鏈接

Hybrid應用

Hybrid應用較為常見,經常存在網頁需要訪問本地目錄的需求,包括存儲clientvar、獲取客戶端cache、訪問沙盒文件等。
若不適用URLProtocol,上述過程可以通過前端通知客戶端提供某資源->客戶端通過接口傳輸資源這一過程實現。但存在適配復雜,兩過程分離等問題。而通過URLProtocol攔截請求,可使這一過程對前端透明,其無須關心數據請求邏輯。

示例代碼見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://的網絡請求,同時從Bundle中查找該文件并返回請求。該邏輯同樣適用于從本地Cache、持久化存儲中獲取,實現了native資源獲取與前端資源獲取過程的解耦。

攔截請求數據

對于應用內置瀏覽器等場景,經常需要記錄用戶訪問了那些網頁等信息,并進行危險提示、免責提示、數據統計、競品攔截等工作。此過程同樣可通過URLProtocol攔截實現

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
    }
}

上述代碼實現了收到請求時做出處理邏輯(如通知)。但由于該請求被攔截將無法繼續發至目的地,故復制該請求并發起,同時實現下述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)
    }
}

上述代碼實現了URLSessionDataDelegate,主要作用是將已發送請求所收到的響應,正確返回給請求者。
通過攔截請求,并按序返回二次確認頁面、危險提示頁面等,實現了內置瀏覽器攔截需求,并保證了瀏覽器的正常運行。

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

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

推薦閱讀更多精彩內容

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