美文网首页
NSURLProtocol探究及实践

NSURLProtocol探究及实践

作者: Minecode | 来源:发表于2019-07-21 16:13 被阅读0次

    原文见我的个人博客

    初识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。

    相关文章

      网友评论

          本文标题:NSURLProtocol探究及实践

          本文链接:https://www.haomeiwen.com/subject/mixvlctx.html