# NSURLProtocol 拦截器

![1iibhORPNGLz15jvM8h740Q.png](https://img.hacpai.com/file/2019/12/1iibhORPNGLz15jvM8h740Q-55eab7c5.png)

为了这篇文章的内容，写了一个小项目，支持 ObjectiveC，Swift 语言。 支持 Cocoapods 集成，主要功能是，网络拦截和 Mock 数据。 有兴趣的同学可以看看。

项目地址： <https://github.com/zColdWater/HTTPInterceptor>

## 一，NSURLProtocol 的介绍

> Apple 官方文档 <https://developer.apple.com/documentation/foundation/urlprotocol>

NSURLProtocol 是一个抽象类，你不能直接创建这个实例，如果你想使用它的功能，你应该继承它创建属于自己的子类，然后重写 `NSURLProtocol` 的方法。

#### 1.关于 NSURLProtocol 类定义

我们看下这个类的定义：

```
open class URLProtocol : NSObject {
    public init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?)
    // 操作被拦截协议的对象，用于给予被拦截者回调。
    open var client: URLProtocolClient? { get }
    // 拦截到的request
    open var request: URLRequest { get }
    // 给接收者一个缓存对象
    @NSCopying open var cachedResponse: CachedURLResponse? { get }
    // 通过URLRequest参数返回是否要拦截这个Request
    open class func canInit(with request: URLRequest) -> Bool
    // 是不是要重新返回一个新的request，一般你可以在原request基础上，加一些参数等，然后再返回，返回后，request属性就是你返回的request了。
    open class func canonicalRequest(for request: URLRequest) -> URLRequest
    // 比较两个URLRequest的缓存是否相当，如果相当，返回true，否则返回false。 目前还没想到有什么场景需要重写这个方法。
    open class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool
    // 开始你需求的操作了，可以通过self.request拿到拦截的request。
    open func startLoading()
    // 取消任务后，你要的动作。 
    open func stopLoading()
    // 读取URLRequest的附加属性，通过Key
    open class func property(forKey key: String, in request: URLRequest) -> Any?
    // 设置URLRequest的附加属性，通过Key
    open class func setProperty(_ value: Any, forKey key: String, in request: NSMutableURLRequest)
    // 移除URLRequest的附加属性，通过Key
    open class func removeProperty(forKey key: String, in request: NSMutableURLRequest)
    // 将当前协议注册进拦截系统
    open class func registerClass(_ protocolClass: AnyClass) -> Bool
    // 取消注册 和 registerClass 动作相反
    open class func unregisterClass(_ protocolClass: AnyClass)
}

extension URLProtocol {
    @available(iOS 8.0, *)
    // 是否要拦截这个URLSessionTask，它和 canInit(with request: URLRequest) 只能有一个被调用，如果声明了  canInit(with task: URLSessionTask) 就不会走 canInit(with request: URLRequest) 了。
    open class func canInit(with task: URLSessionTask) -> Bool
    @available(iOS 8.0, *)
    public convenience init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?)
    @available(iOS 8.0, *)
    // 获取拦截的task
    @NSCopying open var task: URLSessionTask? { get }
}
```

#### 2.如何拦截呢，示例代码？

那么我们一般怎么用它来拦截 URL 呢？ 我写了一个示例代码，MyURLProtocol 在页面最开始先注册好。 然后你可以创建任何已经有的标准协议 URL 还是你自定义的协议 URL，都可以。 然后通过 URLSession 创建一个会话，再用这个会话 loading 你的 URL，开始这个 task。 MyURLProtocol 就能够拦截到你的 URL 了，下面的例子。

例子输出：

```
拦截的RequestURL字符串:myscheme://www.appcoda.com/working-url-schemes-ios/
拦截的RequestURL字符串:http://www.appcoda.com/working-url-schemes-ios/
拦截的RequestURL字符串:https://www.appcoda.com/working-url-schemes-ios/
拦截的RequestURL字符串:file:///a/working-url-schemes-ios/
拦截的RequestURL字符串:https://www.appcoda.com/working-url-schemes-ios/
```

例子代码：

```swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        URLProtocol.registerClass(MyURLProtocol.self)

        // 自定义协议的URL
        let customUrl = URL(string: "myscheme://www.appcoda.com/working-url-schemes-ios/")!

        // 标准HTTP协议 URL
        let httpUrl = URL(string: "http://www.appcoda.com/working-url-schemes-ios/")!

        // 标准HTTPS协议 URL
        let httpsUrl = URL(string: "https://www.appcoda.com/working-url-schemes-ios/")!

        // 标准FILE协议 URL
        let fileUrl = URL(string: "file:///a/working-url-schemes-ios/")!

        // 分别创建URL的Task
        let customUrlTask = URLSession.shared.dataTask(with: customUrl)
        let httpUrlTask = URLSession.shared.dataTask(with: httpUrl)
        let httpsUrlTask = URLSession.shared.dataTask(with: httpsUrl)
        let fileUrlTask = URLSession.shared.dataTask(with: fileUrl)

        // 分别开始每个Task的任务
        customUrlTask.resume()
        httpUrlTask.resume()
        httpsUrlTask.resume()
        fileUrlTask.resume()
    }

}


class MyURLProtocol: URLProtocol {

    /// 重写 canInit ，用于判断这个 URLRequest 是不是需要拦截。 如果需要拦截返回true，否则返回false。
    override class func canInit(with request: URLRequest) -> Bool {
        print("拦截的RequestURL字符串:\(request.url!.absoluteString)")
        return false
    }

    /// 重写 canonicalRequest 根据当前的request返回一个新的request，当 canInit 返回true，表示需要拦截，才会走这里。
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    /// 重写 startLoading 用于你的自定义操作，当 canonicalRequest 执行完，才会走这里。
    override func startLoading() {}

    /// 重写 stopLoading 用于你的自定义操作
    override func stopLoading() {}

}
```

#### 3.做一个最简易版的 Mock 数据的例子

例子输出：

```
Mock Data:Optional("{\n  \"name\" : \"henry\"\n}")
```

例子代码：

```swift
import UIKit

let customUrl = URL(string: "myscheme://www.appcoda.com/working-url-schemes-ios/")!


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        URLProtocol.registerClass(MyURLProtocol.self)

        let customUrlTask = URLSession.shared.dataTask(with: customUrl) { (data, response, error) in
            let jsonStr = String(data: data!, encoding: .utf8)
            print("Mock Data:\(String(describing: jsonStr))")
        }
        customUrlTask.resume()
    }

}



class MyURLProtocol: URLProtocol {

    /// 重写 canInit ，用于判断这个 URLRequest 是不是需要拦截。 如果需要拦截返回true，否则返回false。
    override class func canInit(with request: URLRequest) -> Bool {
        guard let url = request.url else { return false }
        // 如果是我们想要拦截的URL，我们就返回true。
        if url == customUrl {
            return true
        }
        else {
            return false
        }
    }

    /// 重写 canonicalRequest 根据当前的request返回一个新的request，当 canInit 返回true，表示需要拦截，才会走这里。
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    /// 重写 startLoading 用于你的自定义操作，当 canonicalRequest 执行完，才会走这里。
    override func startLoading() {

        // 创建 HTTPURLResponse 对象，返回给被拦截的请求任务。
        let query: [String:String] = ["name":"henry"]
        let data = try! JSONSerialization.data(withJSONObject: query, options: [.prettyPrinted])
        let response: HTTPURLResponse = HTTPURLResponse(url: self.request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!

        // self.client 你可以理解成被你拦截的对象，然后把数据塞给它，Response 和 Data。
        self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        self.client?.urlProtocol(self, didLoad: data)

        // 告诉被拦截对象，已经完成了。 这个时候，外面被拦截的请求 URLSession.shared.dataTask 也会得到完成的回调。
        self.client?.urlProtocolDidFinishLoading(self)
    }

    /// 重写 stopLoading 用于你的自定义操作
    override func stopLoading() {}

}
```

## 二，NSURLProtocol 的应用

项目地址： <https://github.com/zColdWater/HTTPInterceptor>

#### 1.拦截器：

比如拦截一些网络请求，集中为某些规则的 `URLRequest` 添加一些参数或者再更改 `URLResponse` 等等操作。

![WechatIMG1089.png](https://img.hacpai.com/file/2019/12/WechatIMG1089-2596db6c.png)

#### 2.Mocker:

比如服务器 API 还没有开发完成，自己 Mock 假数据为了展示 UI 使用。

关于 `拦截器` 和 `Mocker` 的应用，我写了一个开源项目，API 清晰，使用简单，支持 Cocoapods 集成，支持 Swift 和 ObjectiveC。

![WechatIMG1088.png](https://img.hacpai.com/file/2019/12/WechatIMG1088-ef9a798b.png)

## 三，总结

我再总结一下下哈。 首先是我写的项目真的挺好用，哈哈。

**第一步：** 注册自己的 `URLProtocl` 子类，在触发之前。 **第二步：** 在自己的子类里面完成拦截后的逻辑，比如是给个假数据，然后告诉拦截者获取数据完成了，还是不拦截，这个根据自己的需求。 **第三步：** 触发拦截行为，首先定义一个属于你自己的 `URL`，协议是选择 `http` 还是其他，还是自己的自定义协议都可以。然后创建 `URLSession` 会话，把这个 `URL` 搞进去，开始任务，发起获取数据的动作，这个时候就触发到拦截器啦。

希望大家这个时候都能理解该如何使用这个东东了。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yongpenglovemimi123.gitbook.io/henry/ios/nsurlprotocol-lan-jie-qi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
