iOS 网络框架的封装

最近因为项目需要,又给我写的网络框架添加了不少功能。到目前看来,这个网络框架已经具备了最基本的项目需求,实现了以后大部分项目都能使用的现实需要,但还有几点令我不太满意。这篇文章就是记录我设计这个框架的整体思路,以及记录一下不满意的地方,方便以后回顾修改。

构建一个请求

首先,我的设计思路是偏向 POP 的,也就是面向协议编程。框架并不直接提供 URLRequest 的构建接口,而是由继承了 Requestable 协议的类与结构体自身来实现。

Requestable 协议是长这个样子的:

public protocol Requestable {
    associatedtype AnyResponse: Response
    associatedtype AnyRequest:  Request
    
    var request: AnyRequest { get set }
    var response: AnyResponse.Type { get set }
    
    var extendErrorHandler: ((Data) -> Error?)? { get }
    var logRequestTime: Bool { get }
}

可以看到,这其中还有两个关联类型,它们分别是负责描述请求的 Request 协议。负责描述回调的 Response 协议。

它们的结构是这样的:

public protocol Request {
    var url: URL                    { get set }
    var httpMethod: String          { get set }
    var body: Data?                 { get set }
    var header: [String: String]?   { get set }
    var type: RequestType           { get set }
    var cachePolicy: CachePolicy    { get set }
}

public protocol Response: JSON {
    associatedtype Data
    
    var code: Int?   { get set }
    var msg: String? { get set }
    var data: Data?  { get set }
}

而 Reponse 又要求实现 JSON 协议,JSON 协议其实比较特殊,它规范了你的数据模型必须是这样的:

public protocol JSON {
    var makeJSON: [String: Any]? { get }
    
    static func makeModel(_ deserielized: String?) -> Self?
    static func makeModel(_ deserielized: [String: Any]?) -> Self?
}

也就是,你需要提供模型转字典、字典转模型这一基本能力,而这项操作是相对复杂且多元化的,有非常多的第三方框架能帮助实现这一点,甚至自己写也不麻烦,所以我索性将 JSON 协议设计为“可变实现”的协议,这充分利用了 Swift 的协议特性。什么意思呢,原理其实很简单,首先我定义一个类型别名,也就是 typealias,它的内容是“任意一个能实现字典模型互转的类型(简称类型 M) & JSON”,假定这个别名取为 Model,那么,只要我全局实现 Model,那么就等于实现了 JSON,且依赖于类型 M,我能让所有 Model 都实现字典与模型互转。
具体做起来也很简单,首先我创建一个类型别名:

public typealias Model = JSON & HandyJSON

这里我使用 HandyJSON 来作为类型 M 的示例,接下来,我需要将 HandyJSON 提供的字典模型互转方法作为 JSON 的默认实现,方法如下:

public extension JSON where Self: HandyJSON {
    var makeJSON: [String: Any]? {
        toJSON()
    }

    static func makeModel(_ deserielized: String?) -> Self? {
        deserialize(from: deserielized)
    }

    static func makeModel(_ deserielized: [String: Any]?) -> Self? {
        deserialize(from: deserielized)
    }
}

此时,只要类型是 Model 的结构和类,都会拥有字典模型互转的能力了。最后,我们再赋予 JSON 一个默认实现:

public extension JSON {
    var makeJSON: [String: Any]? {
        nil
    }

    static func makeModel(_ deserielized: String?) -> Self? {
        nil
    }

    static func makeModel(_ deserielized: [String: Any]?) -> Self? {
        nil
    }
}

这一点主要是方便于其它没有实现 HandyJSON 但又想实现 JSON 以使用网络框架的结构和类。
而我为什么要大张旗鼓地做到这一点呢,主要是考虑到了 HandyJSON 的不稳定性,HandyJSON 在最近的几次 Swift 语言更新中,都体现出了不稳定性,很容易就导致 App 崩溃,如果遇到了这种情况,甚至 Swift 或 iOS 发生了一些 HandyJSON 无法解决的更新,HandyJSON 用就无法使用,那么我只需要修改 Model 的定义,并提供新的默认实现,就能拯救我的 App,而无需去修改每一个文件,使得去除 HandyJSON。

然后,我给 Requestable 提供一个默认方法 resume,这个方法用于返回一个可订阅的信号源,只要订阅了这个信号,就能获得这个信号的状态,Rx 对这些状态做了很好的区分,你可以选择性的订阅想要的信号类型,例如成功、失败、完成状态。
resume 的实现是这样的:

func resume() -> Observable<AnyResponse?> {
    ObservableCreate(requestable: self).resume()
}

struct ObservableCreate<R: Requestable> {
    var requestable: R
    
    func resume() -> Observable<R.AnyResponse?> {
        let network = Network(requestable)
        let url = requestable.request.url
        var request = URLRequest(url: url)
        
        request.httpMethod = requestable.request.httpMethod
        request.httpBody = requestable.request.body
        request.timeoutInterval = NetworkConfiguration.timeoutInterval
        network.cachePolicy = requestable.request.cachePolicy
        
        requestable.request.header?.forEach({ (key, value) in
            request.setValue(value, forHTTPHeaderField: key)
        })
        
        network.urlRequest = request
        network.errorHandler = requestable.extendErrorHandler
        
        return equestable.request.type.resume(network)
      }
}

通过 ObservableCreate 来统一创建这个信号,那么你可能已经发现了 Network 这个类,这个类就是框架的核心部分,实际的对请求完成构建、发送、回调处理、缓存等操作,同时也是对 Alamofire 的一个封装。

除此之外,Requestable 还有两个额外属性,它们是:

var extendErrorHandler: ((Data) -> Error?)? { get }
var logRequestTime: Bool { get }

这两个属性的 get 方法是有默认实现的,也就是 Requestable 不强制你实现它们,但依然是构建一个请求较为重要的部分。
extendErrorHandler 是一个闭包,你可以在这里拿到所有请求回调的数据,但我不建议你在这里对数据进行操作,这里指应该关心回调中的错误信息,也就是说,此时请求是成功的,但服务器可能返回的不是一个标准的数据,或直接包装了 Error 数据模型返回。如果在这里返回了一个 Error,那么请求将不会被标记为成功,最终会走到错误回调中,也就是 onError 事件。
logRequestTime 默认是 false,它的作用是用来记录每次请求的时间,并且会把日志输出到一个磁盘文件中,便于接口优化。

那么现在,我们就可以动手构建一个请求了,大概是这样的:

struct BindUser: Requestable {
    var request: BindUserRequest = .init()
    
    var response: BindUserResponse.Type = BindUserResponse.self
}

struct BindUserRequest: Request {
    
    static var path: String {
        #if DEBUG
        return "http://debug.com”
        #else
        return "http://release.com”
        #endif
    }
    
    var cachePolicy: CachePolicy = .withoutCache
    
    var url: URL = URL.init(string: “\(BindUserRequest.path)/api“)!
        
    var httpMethod: String = "POST"
    
    var body: Data?
    
    var header: [String : String]? = [“Content-Type”: “application/x-www-form-urlencoded”]
    
    var type: RequestType = .request
    
    let appToken: String = "API"
    
    init() {
        
    }
}

struct BindUserResponse: Response {
    var code: Int?
    
    var msg: String?
    
    var data: String?
}

struct BindUserModel: Model {
    var status: String?
    var message: String?
    var data: String?
}

这是一个用于绑定用户信息的后台接口,在实现了这个接口的定义后,我就可以在需要用到的地方进行调用:

BindUser().resume()

最后,通过对 resume 返回的信号源进行订阅,我就可以对数据进行处理了。

订阅一个请求

前文提到,该框架实际上就是一个对 RxSwift + Alamofire 的封装,所以当请求发出后,你可以立刻对请求进行订阅,框架会对数据进行二次封装,然后通过 RxSwift 这一层对数据进行筛选。

数据处理的结果,符合 RxSwift 对数据的定义,也就是说:

onNext 信号:请求成功且无任何错误回调。
onError 信号:请求发生了任何错误,包括下层应用层错误和上层返回了自定义错误。
onComplete 信号:请求完成了,包括请求成功和请求失败。

现在就可以根据需要来对数据进行筛选,例如,我只关心请求是否结束,我不关心成功与失败,那么只需要订阅 onComplete 信号就足够了。

// 在界面上显示一个“正在请求”的图标。
Alert.show()

_ = BindUser().resume().subscribe(onComplete: {
    // 请求完成了,不论成功或失败,都应该隐藏“正在请求”的图标。
    Alert.hide()
})

缓存

框架的缓存策略是基于请求域名+相对路径的,也就是不论 GET 参数是什么、协议头是什么,都不会影响到缓存机制。
在实现 Request 协议时,或者手动创建 Network 对象时,你就可以指定一个缓存策略,目前的缓存策略有如下几种:

/// 始终使用缓存,如果没有缓存,则在第一次请求成功时建立缓存。
case alwaysUseCache

/// 首先使用缓存,当请求成功后,会发出请求的数据,并覆盖原缓存。
case useCacheAndRequest

/// 使用缓存,当请求成功后,会发出请求的数据,但不覆盖原有缓存。
case useCacheAndRequestWithoutRecache

/// 默认。始终不使用缓存,请求的数据也不会缓存到本地。
case withoutCache

到目前,框架还不会对非 JSON 串的二进制数据进行缓存,例如图片、视频等,所以需要依赖其他方式。

缓存的底层实现是利用了 SQLite 数据库,在需要的时候直接将数据写入数据库,或从数据库读取缓存的数据。

目前的缓存策略还非常的原始,以后我需要为它添加更多能力,例如最大缓存大小、清除缓存、缓存过期时间,同时还需要能够缓存二进制文件,能识别这个二进制文件在服务器上是否发生了修改,如果修改了,则要建立新的缓存,如果没有修改,那么直接使用缓存即可。

最后

对框架不满意的部分,其实已经体现在上文中了。包括:
框架是对 Alamofire 的二次封装,依赖项太庞大了,需要一个轻量化的,基于原生 URLRequest 的封装。
框架的缓存机制还非常原始。
框架的性能还未在考虑之中。

源码可能会在我实现了全面的缓存机制之后传到 GitHub 上。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容

  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,184评论 1 23
  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 3,654评论 2 7
  • AFHTTPRequestOperationManager 网络传输协议UDP、TCP、Http、Socket、X...
    Carden阅读 4,335评论 0 12
  • 前几天偶然听见一个朋友的母亲身体不好 而不再身边的他一直在自责 觉得他自己没有尽孝心 在我看来他已经很不错了 尽...
    黄邢室内设计师阅读 305评论 0 1
  • 眼看看已经到了暮春了,天气渐渐热起来,今天光一发现小镇上的人是越来越多,爸爸的房子是盖的越来越多了,很多的周围的农...
    季中阅读 165评论 0 0