原文見我的個人博客
初識NSURLProtocol 及 URL Loading System
Hybrid應(yīng)用逐漸普遍,對于iOS開發(fā)虱肄,NSURLProtocol為其提供了許多重要的Hybrid能力咏窿。
說到NSURLProtocol平斩,首先要提到URL Loading System绘面,后者支持著整個App訪問URL指定內(nèi)容晚凿。根據(jù)文檔配圖,其結(jié)構(gòu)大致如下:
都有哪些網(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虐沥。