原文見我的個人博客
初識NSURLProtocol 及 URL Loading System
Hybrid應用逐漸普遍,對于iOS開發,NSURLProtocol為其提供了許多重要的Hybrid能力。
說到NSURLProtocol,首先要提到URL Loading System,后者支持著整個App訪問URL指定內容。根據文檔配圖,其結構大致如下:
都有哪些網絡請求經由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。