iOS NSURLProtocol 拦截器

发布于 2024-09-15 09:24:35 字数 8118 浏览 11 评论 0


1iibhORPNGLz15jvM8h740Q.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/

例子代码:

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}")

例子代码:

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

2.Mocker:

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

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


WechatIMG1088.png

三,总结

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

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
24 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文