iOS 網(wǎng)絡框架的封裝

最近因為項目需要,又給我寫的網(wǎng)絡框架添加了不少功能。到目前看來秕狰,這個網(wǎng)絡框架已經(jīng)具備了最基本的項目需求,實現(xiàn)了以后大部分項目都能使用的現(xiàn)實需要躁染,但還有幾點令我不太滿意鸣哀。這篇文章就是記錄我設計這個框架的整體思路,以及記錄一下不滿意的地方吞彤,方便以后回顧修改我衬。

構建一個請求

首先叹放,我的設計思路是偏向 POP 的,也就是面向協(xié)議編程挠羔【觯框架并不直接提供 URLRequest 的構建接口,而是由繼承了 Requestable 協(xié)議的類與結構體自身來實現(xiàn)破加。

Requestable 協(xié)議是長這個樣子的:

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

可以看到俱恶,這其中還有兩個關聯(lián)類型,它們分別是負責描述請求的 Request 協(xié)議范舀。負責描述回調(diào)的 Response 協(xié)議合是。

它們的結構是這樣的:

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 又要求實現(xiàn) JSON 協(xié)議,JSON 協(xié)議其實比較特殊锭环,它規(guī)范了你的數(shù)據(jù)模型必須是這樣的:

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

也就是聪全,你需要提供模型轉字典、字典轉模型這一基本能力辅辩,而這項操作是相對復雜且多元化的难礼,有非常多的第三方框架能幫助實現(xiàn)這一點,甚至自己寫也不麻煩玫锋,所以我索性將 JSON 協(xié)議設計為“可變實現(xiàn)”的協(xié)議蛾茉,這充分利用了 Swift 的協(xié)議特性。什么意思呢撩鹿,原理其實很簡單谦炬,首先我定義一個類型別名,也就是 typealias三痰,它的內(nèi)容是“任意一個能實現(xiàn)字典模型互轉的類型(簡稱類型 M) & JSON”吧寺,假定這個別名取為 Model,那么散劫,只要我全局實現(xiàn) Model稚机,那么就等于實現(xiàn)了 JSON,且依賴于類型 M获搏,我能讓所有 Model 都實現(xiàn)字典與模型互轉赖条。
具體做起來也很簡單,首先我創(chuàng)建一個類型別名:

public typealias Model = JSON & HandyJSON

這里我使用 HandyJSON 來作為類型 M 的示例常熙,接下來纬乍,我需要將 HandyJSON 提供的字典模型互轉方法作為 JSON 的默認實現(xiàn),方法如下:

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 一個默認實現(xiàn):

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

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

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

這一點主要是方便于其它沒有實現(xiàn) HandyJSON 但又想實現(xiàn) JSON 以使用網(wǎng)絡框架的結構和類茧泪。
而我為什么要大張旗鼓地做到這一點呢蜓氨,主要是考慮到了 HandyJSON 的不穩(wěn)定性,HandyJSON 在最近的幾次 Swift 語言更新中队伟,都體現(xiàn)出了不穩(wěn)定性穴吹,很容易就導致 App 崩潰,如果遇到了這種情況嗜侮,甚至 Swift 或 iOS 發(fā)生了一些 HandyJSON 無法解決的更新港令,HandyJSON 用就無法使用,那么我只需要修改 Model 的定義锈颗,并提供新的默認實現(xiàn)顷霹,就能拯救我的 App,而無需去修改每一個文件宜猜,使得去除 HandyJSON泼返。

然后硝逢,我給 Requestable 提供一個默認方法 resume姨拥,這個方法用于返回一個可訂閱的信號源,只要訂閱了這個信號渠鸽,就能獲得這個信號的狀態(tài)叫乌,Rx 對這些狀態(tài)做了很好的區(qū)分,你可以選擇性的訂閱想要的信號類型徽缚,例如成功憨奸、失敗、完成狀態(tài)凿试。
resume 的實現(xiàn)是這樣的:

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 來統(tǒng)一創(chuàng)建這個信號排宰,那么你可能已經(jīng)發(fā)現(xiàn)了 Network 這個類,這個類就是框架的核心部分那婉,實際的對請求完成構建板甘、發(fā)送、回調(diào)處理详炬、緩存等操作盐类,同時也是對 Alamofire 的一個封裝。

除此之外呛谜,Requestable 還有兩個額外屬性在跳,它們是:

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

這兩個屬性的 get 方法是有默認實現(xiàn)的,也就是 Requestable 不強制你實現(xiàn)它們隐岛,但依然是構建一個請求較為重要的部分猫妙。
extendErrorHandler 是一個閉包,你可以在這里拿到所有請求回調(diào)的數(shù)據(jù)聚凹,但我不建議你在這里對數(shù)據(jù)進行操作割坠,這里指應該關心回調(diào)中的錯誤信息逻悠,也就是說,此時請求是成功的韭脊,但服務器可能返回的不是一個標準的數(shù)據(jù)童谒,或直接包裝了 Error 數(shù)據(jù)模型返回沪羔。如果在這里返回了一個 Error饥伊,那么請求將不會被標記為成功,最終會走到錯誤回調(diào)中蔫饰,也就是 onError 事件琅豆。
logRequestTime 默認是 false,它的作用是用來記錄每次請求的時間篓吁,并且會把日志輸出到一個磁盤文件中茫因,便于接口優(yōu)化。

那么現(xiàn)在杖剪,我們就可以動手構建一個請求了冻押,大概是這樣的:

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?
}

這是一個用于綁定用戶信息的后臺接口,在實現(xiàn)了這個接口的定義后盛嘿,我就可以在需要用到的地方進行調(diào)用:

BindUser().resume()

最后洛巢,通過對 resume 返回的信號源進行訂閱,我就可以對數(shù)據(jù)進行處理了次兆。

訂閱一個請求

前文提到稿茉,該框架實際上就是一個對 RxSwift + Alamofire 的封裝,所以當請求發(fā)出后芥炭,你可以立刻對請求進行訂閱漓库,框架會對數(shù)據(jù)進行二次封裝,然后通過 RxSwift 這一層對數(shù)據(jù)進行篩選园蝠。

數(shù)據(jù)處理的結果渺蒿,符合 RxSwift 對數(shù)據(jù)的定義,也就是說:

onNext 信號:請求成功且無任何錯誤回調(diào)砰琢。
onError 信號:請求發(fā)生了任何錯誤蘸嘶,包括下層應用層錯誤和上層返回了自定義錯誤。
onComplete 信號:請求完成了陪汽,包括請求成功和請求失敗训唱。

現(xiàn)在就可以根據(jù)需要來對數(shù)據(jù)進行篩選,例如挚冤,我只關心請求是否結束况增,我不關心成功與失敗,那么只需要訂閱 onComplete 信號就足夠了训挡。

// 在界面上顯示一個“正在請求”的圖標澳骤。
Alert.show()

_ = BindUser().resume().subscribe(onComplete: {
    // 請求完成了歧强,不論成功或失敗,都應該隱藏“正在請求”的圖標为肮。
    Alert.hide()
})

緩存

框架的緩存策略是基于請求域名+相對路徑的摊册,也就是不論 GET 參數(shù)是什么、協(xié)議頭是什么颊艳,都不會影響到緩存機制茅特。
在實現(xiàn) Request 協(xié)議時,或者手動創(chuàng)建 Network 對象時棋枕,你就可以指定一個緩存策略白修,目前的緩存策略有如下幾種:

/// 始終使用緩存,如果沒有緩存重斑,則在第一次請求成功時建立緩存兵睛。
case alwaysUseCache

/// 首先使用緩存,當請求成功后窥浪,會發(fā)出請求的數(shù)據(jù)祖很,并覆蓋原緩存。
case useCacheAndRequest

/// 使用緩存寒矿,當請求成功后突琳,會發(fā)出請求的數(shù)據(jù)若债,但不覆蓋原有緩存符相。
case useCacheAndRequestWithoutRecache

/// 默認。始終不使用緩存蠢琳,請求的數(shù)據(jù)也不會緩存到本地啊终。
case withoutCache

到目前,框架還不會對非 JSON 串的二進制數(shù)據(jù)進行緩存傲须,例如圖片蓝牲、視頻等,所以需要依賴其他方式泰讽。

緩存的底層實現(xiàn)是利用了 SQLite 數(shù)據(jù)庫例衍,在需要的時候直接將數(shù)據(jù)寫入數(shù)據(jù)庫,或從數(shù)據(jù)庫讀取緩存的數(shù)據(jù)已卸。

目前的緩存策略還非常的原始佛玄,以后我需要為它添加更多能力,例如最大緩存大小累澡、清除緩存梦抢、緩存過期時間,同時還需要能夠緩存二進制文件愧哟,能識別這個二進制文件在服務器上是否發(fā)生了修改奥吩,如果修改了哼蛆,則要建立新的緩存,如果沒有修改霞赫,那么直接使用緩存即可腮介。

最后

對框架不滿意的部分,其實已經(jīng)體現(xiàn)在上文中了端衰。包括:
框架是對 Alamofire 的二次封裝萤厅,依賴項太龐大了,需要一個輕量化的靴迫,基于原生 URLRequest 的封裝惕味。
框架的緩存機制還非常原始。
框架的性能還未在考慮之中玉锌。

源碼可能會在我實現(xiàn)了全面的緩存機制之后傳到 GitHub 上名挥。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市主守,隨后出現(xiàn)的幾起案子禀倔,更是在濱河造成了極大的恐慌,老刑警劉巖参淫,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件救湖,死亡現(xiàn)場離奇詭異,居然都是意外死亡涎才,警方通過查閱死者的電腦和手機鞋既,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耍铜,“玉大人邑闺,你說我怎么就攤上這事∽丶妫” “怎么了陡舅?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伴挚。 經(jīng)常有香客問我靶衍,道長,這世上最難降的妖魔是什么茎芋? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任颅眶,我火速辦了婚禮,結果婚禮上败徊,老公的妹妹穿的比我還像新娘帚呼。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布煤杀。 她就那樣靜靜地躺著眷蜈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沈自。 梳的紋絲不亂的頭發(fā)上酌儒,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音枯途,去河邊找鬼忌怎。 笑死,一個胖子當著我的面吹牛酪夷,可吹牛的內(nèi)容都是我干的榴啸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼晚岭,長吁一口氣:“原來是場噩夢啊……” “哼鸥印!你這毒婦竟也來了?” 一聲冷哼從身側響起坦报,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤库说,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后片择,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體潜的,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年字管,在試婚紗的時候發(fā)現(xiàn)自己被綠了啰挪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡纤掸,死狀恐怖脐供,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情借跪,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布酌壕,位于F島的核電站掏愁,受9級特大地震影響,放射性物質發(fā)生泄漏卵牍。R本人自食惡果不足惜果港,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望糊昙。 院中可真熱鬧辛掠,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至猩谊,卻和暖如春千劈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背牌捷。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工墙牌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人暗甥。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓喜滨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撤防。 傳聞我的和親對象是個殘疾皇子鸿市,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容

  • iOS網(wǎng)絡架構討論梳理整理中。即碗。焰情。 其實如果沒有APIManager這一層是沒法使用delegate的,畢竟多個單...
    yhtang閱讀 5,172評論 1 23
  • iOS開發(fā)系列--網(wǎng)絡開發(fā) 概覽 大部分應用程序都或多或少會牽扯到網(wǎng)絡開發(fā)剥懒,例如說新浪微博内舟、微信等,這些應用本身可...
    lichengjin閱讀 3,644評論 2 7
  • AFHTTPRequestOperationManager 網(wǎng)絡傳輸協(xié)議UDP初橘、TCP验游、Http、Socket保檐、X...
    Carden閱讀 4,326評論 0 12
  • 前幾天偶然聽見一個朋友的母親身體不好 而不再身邊的他一直在自責 覺得他自己沒有盡孝心 在我看來他已經(jīng)很不錯了 盡...
    黃邢室內(nèi)設計師閱讀 304評論 0 1
  • 眼看看已經(jīng)到了暮春了耕蝉,天氣漸漸熱起來,今天光一發(fā)現(xiàn)小鎮(zhèn)上的人是越來越多夜只,爸爸的房子是蓋的越來越多了垒在,很多的周圍的農(nóng)...
    季中閱讀 164評論 0 0