當前位置:
首頁 > 最新 > Swift 項目中涉及到 JSONDecoder,網路請求,泛型協議式編程的一些記錄和想法

Swift 項目中涉及到 JSONDecoder,網路請求,泛型協議式編程的一些記錄和想法

前言

最近項目開發一直在使用 swift,因為 HTN 項目最近會有另外一位同事加入,所以打算對最近涉及到的一些技術和自己的一些想法做個記錄,同時也能夠方便同事熟悉代碼。


JSON 數據的處理

做項目只要是涉及到伺服器端介面都沒法避免和 JSON 數據打交道。對於來自網路的 JSON 結構化數據的處理,可以使用 JSONDecoder 這個蘋果自己提供的字元串轉模型類,這個類是在 Swift 4 的 Fundation 模塊里提供的,可以在Swift 源碼目錄 swift/stdlib/public/SDK/Fundation/JSONEncoder.swift 看到蘋果對這個類實現。

其它對 JSON 處理的庫還有 SwiftyJSON GitHub - SwiftyJSON/SwiftyJSON: The better way to deal with JSON data in Swift


下面蘋果使用 JSONDecoder 的一個例子來看看如何使用 JSONDecoder

這裡要注意 GroceryProduct 結構體需要遵循 Codable,因為 JSONDecoder 的實例對象的 decode 方法需要遵循 Decodable 協議的結構體。Codable 是 Encodable 和 Decodable 兩個協議的組合,寫法如下:

當然 JSON 數據的結構不會都是這麼簡單,如果遇到嵌套情況如下:

這時可以通過在 struct 里再套一個 struct 來做,修改過的 struct 如下:

這裡可以觀察到 ability 里數學物理化學的評價都是那幾個,無非是優良差,所以很適合用枚舉表示,swift 的枚舉對於字元串關聯類型枚舉也有很好的支持,只要聲明關聯值類型是 String 就行了,改後的代碼如下:

API 返回的結果會有一個不可控的因素,是什麼呢?那就是有的鍵值有時會返回有時不會返回,那麼這個 struct 怎麼兼容呢?

好在swift 原生就支持了 optional,只需要在屬性後加個問號就行了。比如 points 有時會返回有時不會,那麼就可以這麼寫:


介面還會有一些其它不可控因素,比如會產生出 snake case 的命名風格,要求風格統一固然是很好,但是現實環境總會有些不可抗拒的因素,比如不同團隊,不同公司或者不同風格潔癖的 coder 之間。還好 JSONDecoder 已經做好了。下面我們看看如何用:

這裡 nick_name 我們希望處理成 swift 的風格,那麼我們可以使用一個遵循 CodingKey 協議的枚舉來做映射。

當然這個方法是個通用方法,不光能夠處理 snake case 還能夠起自己喜歡的命名,比如你喜歡簡寫,nick name 寫成 nName,那麼也可以用這個方法。Codable 協議默認的實現實際上已經能夠 cover 掉現實環境的大部分問題了,如果有些自定義的東西要處理的話可以通過覆蓋默認 Codable 的方式來做。關鍵點就是 encoder 的 container,通過獲取 container 對象進行自定義操作。


JSONDecoder 里還有專門的一個屬性 keyDecodingStrategy,這個值是個布爾值,有個 case 是 convertFromSnakeCase,這樣就會按照這個 strategy 來轉換 snake case,這個是核心功能內置的,就不需要我們額外寫代碼處理了。上面加上的枚舉 CodingKeys 也可以去掉了,只需要在 JSONDecoder 這個實例設置這個屬性就行。

keyDecodingStrategy 這個屬性是在 swift 4.1 加上的,所以這個版本之前的還是用 CodingKey 這個協議來處理吧。

那麼蘋果是如何通過這個 keyDecodingStrategy 屬性的設置來做到的呢?

感謝蘋果使用 Swift 寫了 Swift 的核心功能,以後想要了解更多功能背後原理可以不用啃 C++ 了,一邊學習原理還能一邊學習蘋果內部是如何使用 Swift 的,所謂一舉兩得。

實現這個功能代碼就在上文提到的 Swift 源碼目錄 swift/stdlib/public/SDK/Fundation/ 下的 JSONEncoder.swift 文件,如果不想把源碼下下來也可以在 GitHub 上在線看,地址:https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/JSONEncoder.swift

先看看這個屬性的定義:

這個屬性是一個 keyDecodingStrategy 枚舉,默認是 .userDefaultKeys。這個枚舉是這樣定義的:

case convertFromSnakeCase 就是我們使用的,注釋部分描述了整個過程,首先會把 『』 符號後面的字母轉成大寫的,然後移除掉所有的 『』 符號,保留最前面和最後的 『_』 符號。比如nickname就會轉換成nickName_ 而這些都是在枚舉里定義的靜態方法 _convertFromSnakeCase 里完成的。

這段代碼處理的邏輯不算複雜,功能也不多,但是還是有很多值得學習的地方,首先可以看看是如何處理邊界條件的。可以看到兩個邊界條件都是用 guard 語法來處理的。

第一個是判斷空,第二個是通過 String 的 public func index(where predicate: (Character) throws -> Bool) rethrows -> String.Index? 這個函數來看字元串里是否包含了 『_』 符號,如果沒有包含就直接返回原 String 值。這個函數的參數就是一個自定義返回布爾值的 block,返回 true 即刻返回不再繼續遍歷了,可見蘋果對於性能一點也不浪費。

然後這個返回的 index 值還有個作用就是可以得到 『』 符號在最前面後第一個非 『』 符號的字元。因為需求如此,不需要把最前面和最後面的 『』 轉駝峰,但是前面和後面的 『』 符號個數又不一定,所以需要得到前面 『_』 符號和後面的範圍。

那麼得到前面的範圍後,後面的蘋果是怎麼做的呢?

這裡正好可以看到對 String 的 public func formIndex(before i: inout String.Index) 函數的應用,這裡的參數定義為 inout 的作用是能夠在函數里對這個參數不用通過返回的方式直接修改生效。這個函數的作用就是移動字元的 index,before 是往前移動,after 是往後移動。

上面的代碼就是先找到整個字元串的最後的 index 然後開始從後往前找,找到不是 『_』 符號時跳出這個 while,同時還要滿足不超過 lastNonUnderscore 的範圍。

在接下內容之前可以考慮這樣一個問題,為什麼在做前面的判斷時為什麼不用 public func formIndex(after i: inout String.Index) 這個方法,after 不是代表從開始往後移動遍歷么,也可以達到找到第一個不是 『_』 的字元就停止的效果。

蘋果真是雙槍老太婆,一擊兩發,既解決了邊界問題又能解決一個需求,代碼有了優化,代碼量還減少了。其實面試過程中通常都會有些演算法題的環節,很多人都以為只要有了解決思路或者寫出簡單的處理代碼就可以了,我碰到了一些的面試人甚至用中文一條條寫出思路以為就完事了。其實演算法題的考察是分為兩種的,一種是考智商的,就是解決辦法很多或者解決辦法很難,能夠想到解法或者最優解是比較困難的,這樣的題適合那些在面談過程中能覺得實力和深度不錯的人,通過這些題同時還能更多為判斷面試人是否更具創造力,屬於拔尖的考法。還有種是考嚴謹和實際項目能力的,這種更多是考察邊界條件的處理,邏輯的嚴謹還有對代碼優化的處理,這種題的解法和邏輯會比較簡單。

這裡遇到一個 Dictionary 的初始化函數

這個函數就是專門用來處理上面的重複 key 的問題。如果要選擇最後一個 key 的值用這個函數也會很容易。


KeyEncodingStrategy 還可以自定義 codingKey

在 container 初始化時會調用這個 block 來進行 key 的轉換,同樣如果轉換後出現重複 key 也會和 convertFromSnakeCase 一樣選擇第一個。這裡可以看到 Swift 里的枚舉還能夠定義一個 block 方便自定義處理自己特定規則,這樣就可以完全拋棄以前的那種覆蓋 Codable 協議默認實現的方式了。


上面提到了 public func formIndex(before i: inout Index) 這個函數,那麼跟著這個函數在源碼里看看它的實現,這個函數是在這個文件里實現的 swift/IndexSet.swift at master · apple/swift · GitHub

找到這個方法時發現沒有 inout 定義的同名函數也還在那裡

這兩個函數的實現最直觀的感受就是 inout 的少了三個 return。還有一個好處就是值類型參數 i 可以以引用方式傳遞,不需要 var 和 let 來修飾

當然 inout 還有一個好處在上面的函數里沒有體現出來,那就是可以方便對多個值類型數據進行修改而不需要一一指明返回。


網路請求

說到網路請求,在 Objective-C 世界裡基本都是用的 AFNetworking GitHub - AFNetworking/AFNetworking: A delightful networking framework for iOS, macOS, watchOS, and tvOS. 在 Swift 里就是 Alamofire GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift 。我在 Swift 1.0 之前 beta 版本時就注意到 Alamofire 庫里,那時還是 Mattt Thompson 一個人在寫,文件也只有一個。如今功能已經多了很多,但代碼量依然不算太大。我在做 HTN 項目時對於網路請求的需求不是那麼大,但是也有,於是開始的時候就是簡單的使用 URLSession 來實現了一下網路請求,就是想直接拉下介面下發的 JSON 數據。

開始結合著前面解析 JSON 的方法,我這麼寫了個網路請求:

這麼寫是 ok 的,能夠成功請求得到 JSON 數據然後轉換成對應的結構數據。不過如果還有另外幾處也要進行網路請求,拿這一坨代碼不是要到處寫了。那麼先看看 Alamofire 干這個活是什麼樣子的?

Alamofire 有 responseJSON 的方法,不過解完是個字典,用的時候需要做很多容錯判斷很不方便,所以還是要使用 JSONDecoder 或者其它第三方庫。不過 Alamofire 的寫法已經做了一些簡化,當然裡面還實現了更多的功能,我待會再說,現在我的主要任務是簡化調用。於是動手改改先前的實現,學習 Alamofire 的做法,首先創建一個類,然後簡化掉 request 寫法,再建個 block 方便請求完成後的數據返回處理,最後使用泛型支持不同 struct 的數據統一返回。寫完後,我給這個網路類起個名字叫 SMNetWorking 這個類實現如下:

這樣調用起來就簡單得多了,看起來如下:

當然這樣寫起來是簡單多了,特別是請求不同的介面返回不同結構時,本地定義了很多的 model 結構體,那麼請求時只需要指明不同的 model 類型,block 里就能夠直接返回對應的值。

默認都按照 GET 方法請求,在實際項目中會用到其它比如 POST 等方法,Alamofire 的做法是這樣的:

會先定義一個枚舉,依據的標準也列在了注釋里。使用起來是這樣的:

可以看出在 request 方法里有個可選參數,設置完會給 NSURLRequest 的 httpMethod 的這個可選屬性附上設置的值。

那麼接下來我在 SMNetWorking 類里也加上這個功能,先定義一個枚舉:

利用枚舉的字元串協議特性,可以將枚舉名直接轉值的字元串,可以通過這種方式簡化枚舉定義。

翻下 NSURLRequest 提供的那些可選設置項還不少,如果把這些設置都做成一個個可配參數那麼後期維護會非常麻煩。所以我打算使用鏈式來弄。先 fix HTTPMethod 這個。

這裡的 op 是個結構體,專門用來存放這些可選項的值的。完整的代碼可以在這裡看到 HTN/SMNetWorking.swift at master · ming1016/HTN · GitHub

使用起來也很方便:

有了這樣一個結構的設計後面擴展起來會非常方便,不過目前的功能是能夠滿足基本需求的,所以需要完善的比如對於 POST 請求需要的 HTTTP Body,還有 HTTP Headers 的自定義設置,Authentication 里的 HTTP Basic Authentication,Authentication with URLCredential 等,這些也可以先提供一個介面到外部去設置。所以可以先建個 block 把 URLRequest 提供出去由外圍設置。

弄完後的使用效果如下:

就剛才提到的請求參數來說,Alamofire 是定義了一個 ParameterEncoding 協議,協議里規定一個統一處理的方法 func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest 這樣就可以對多種情況做一樣的返回處理了。從遵循這個協議的結構體可以看到 URL,JSON 和 PropertyList 都遵循了,那麼從實現這個協議的 encode 函數的實現里可以看到他們都是殊途同歸到 request 的 httpBody 里。可以拿 URLEncoding 看看具體實現:


泛型協議式編程

對於目前 HTN 項目來說,請求到了數據,將 JSON 解析生成了對應的 Struct,那麼下一步就是要把這個結構化的數據生成不同平台的代碼,比如首先是 Objective-C 代碼,然後是 Swift 代碼,再然後會有 Java 代碼。為了能夠更好的合併多語言里重複的東西,我打算將處理生成不同語言的實現遵循相同的協議,這樣就可以更規範更減少重複的實現這樣的功能了。最終的效果如下:

如果是轉成 Swift 的話就把 H5EditorObjc 改成 H5EditorSwift 就好了,他們遵循的都是 HTNMultilingualismSpecification 協議,其它語言依此類推。如果遇到統一的實現,可以建個協議的擴展,然後用統一函數去實現就好了。

這種設計很類似類簇,比如我們熟悉的 NSString 就是這麼設計的,根據初始化的不同,比如 initWith 什麼的實例出來的對象是不同的,不過他們都遵循了相同的協議,所以我們在使用的時候沒有感覺到差別。

HTNMultilingualismSpecification 這個協議里具體的定義在這裡:https://github.com/ming1016/HTN/blob/master/HTNSwift/HTNSwift/Core/HTNFundation/HTNMultilingualism.swift

回頭看看 JSONDecoder 也是使用協議泛型式編程的一個典範。先看看 decode 函數的定義

入參 type 是遵循了統一的 Decodable 協議的,那麼就可以按照統一的方法去做處理,在內部實現時實際上 JSONDecoder 會代理給 _JSONDecoder 來實現具體邏輯的。所以在 decode 里的具體實現值類型轉換的 unbox 函數都是在 _JSONDecoder 的擴展里實現的。unbox 會處理數字,字元串,布爾值這些基礎數據類型,如果有其它層級的結構體也會一層層解下去, _JSONDecoder 的 _JSONDecodingStorage 通過保存最終得到完整的結構體。可以通過下面的代碼看出支持這個過程的結構是怎麼設計的。首先是 _JSONDecoder 的屬性

下面是初始化

這裡可以看到 storage 在初始化時只 push 了頂層,push 的實現是:

containers 在定義的時候是個 [Any] 數組,這樣就允許 container 包含 container 也就是 struct 包含 struct 這樣的結構。


函數式思想編程

在處理映射成表達式是設置布局屬性最複雜的地方,需要考慮兼顧到各種表達式情況的處理,這樣救需要設計一個類似 SnapKit 那樣可鏈式調用設置值的結構,我先設計了一個結構體用來存一些可變的信息

對於這些結構的設置會在 PtEqualC 這個類里去處理,把每個結構體屬性的設置做成各個函數返回類本身即可實現。效果如下:

不過每次設置完後需要累加到最後返回的字元串里,這樣一個過程其實也可以封裝一個簡單函數,比如 add()。這個怎麼做能夠更通用呢?比如希望支持不同的累加方法等。

那麼可以先設計一個累加的 block 屬性

添加累加字元串和換行標示

寫個函數去設置這個 block 返回是類自己用於鏈式

最後添加一個函數專門用來使用的

我們看看用起來是什麼效果:

細心的同學會注意到這裡多了兩個東西,一個是 filter, 一個是 once,這兩個函數里的 block 會把一些通用邏輯進行封裝。filter 的設置會根據返回決定是否處理後面的 block 或者結構體屬性的設置,實現方式如下

這裡的 filterBl 是類的一個屬性,後面會根據這個屬性來決定動作是否繼續執行。比如屬性的設置會去判斷

once 這個函數也會判斷

同時 once 這個函數還會重置 filterBl 和重置設置的結構體,一箭三雕,相當於一個完整的設置周期。

有了這樣一套函數,再複雜的設置過程以及邏輯處理都可以很清晰統一的表達出來,下面可以看一個複雜布局比如映射成原生表達式的代碼效果:

完整代碼在這裡:https://github.com/ming1016/HTN/blob/master/HTNSwift/HTNSwift/H5Editor/H5EditorObjc.swift

PS:最近在一個公司分享時有人希望推薦下 iOS 相關的博客,當時我推薦了孫源的博客,其實孫源也推薦過一個博客,當時由於地址沒記住沒有說出來,現在推薦給大家:https://www.mikeash.com/ 他的twitter:https://twitter.com/mikeash

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 starming 的精彩文章:

TAG:starming |