> For the complete documentation index, see [llms.txt](https://yongpenglovemimi123.gitbook.io/henry/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yongpenglovemimi123.gitbook.io/henry/ios/nsurlprotocol-lan-jie-qi.md).

# 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` 搞进去，开始任务，发起获取数据的动作，这个时候就触发到拦截器啦。

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