京東APP訂單業(yè)務(wù)Swift優(yōu)化總結(jié)
隨著Swift ABI穩(wěn)定,開發(fā)者對Swift的關(guān)注也持續(xù)升溫,一些開源框架甚至已經(jīng)不再提供ObjC版本了,部分蘋果新出的系統(tǒng)庫也是Swift Only。
在這樣的背景下,京東商城訂單業(yè)務(wù)在不同場景下嘗試更多的使用Swift開發(fā),比如:
京東App部分訂單業(yè)務(wù)頁面
京東App物流小組件
“京東工作站”,為公司內(nèi)部提供的集成部分工作環(huán)境與開發(fā)環(huán)境,以及部分工作流的macOS應(yīng)用
在改造過程中,Swift的高效安全與便捷和一些優(yōu)秀特性給團(tuán)隊留下了深刻的印象。有很多特性是開發(fā)者在寫ObjC時不會太多考慮的。比如,Swift的靜態(tài)派發(fā)方式、值類型的使用、靜態(tài)多態(tài)、Errors+Throws、柯里化與函數(shù)合成以及豐富高階函數(shù)等等,而且相對于OOP,Swift也能更好的支持面向協(xié)議編程、泛型編程以及更抽象函數(shù)式編程,解決了很多ObjC時代開發(fā)者面臨的痛點問題。
結(jié)合Swift和ObjC的異同點,我們從Swift優(yōu)勢出發(fā),重新審視和優(yōu)化了項目的功能代碼,優(yōu)化點包括但不限于如下幾個方面。
Swift運行速度比ObjC快的原因之一就是其派發(fā)方式:靜態(tài)派發(fā)(值類型)和函數(shù)表派發(fā)(引用類型)。使用靜態(tài)派發(fā)ARM架構(gòu)可直接用bl指令跳轉(zhuǎn)到對應(yīng)函數(shù)地址,調(diào)用效率最高并且有利于編譯器的內(nèi)聯(lián)優(yōu)化。值類型無法繼承父類,編譯時期能確定類型,滿足靜態(tài)派發(fā)的條件。對于引用類型,不同編譯器的設(shè)置也會對派發(fā)方式有影響。比如WMO全模塊編譯下,系統(tǒng)會自動填用隱式final等關(guān)鍵字來修飾沒有被子類繼承的類,從而盡可能多的使用靜態(tài)派發(fā)。
在我們的項目中,針對所有使用Class的類做了整體檢查。除非必要應(yīng)完全避免繼承NSObject,少用NSObject的子類。對于不需要考慮繼承或者多態(tài)的場景,盡可能的使用final 或者 private等關(guān)鍵字修飾。
另外需要關(guān)注的是,ObjC也引入了方法的靜態(tài)派發(fā)。在Xcode12中集成的最新LLVM已經(jīng)支持 ObjC 通過對方法指定__attribute__((objc_direct)) 的方式,來將原本的動態(tài)消息派發(fā)改為靜態(tài)派發(fā)。
Swift中的結(jié)構(gòu)體和枚舉是值類型,Class是引用類型。在Swift中使用值類型還是引用類型是開發(fā)者需要思考和評估的。
在我們開發(fā)的京東物流小組件和基于SwiftUI開發(fā)的macOS應(yīng)用中,我們目前更多的使用了結(jié)構(gòu)體和枚舉。先對比下值類型與引用類型的區(qū)別,值類型(Struct Enum等等):
在棧上創(chuàng)建,創(chuàng)建速度快
內(nèi)存占用小。整體占用的內(nèi)存就是內(nèi)部屬性內(nèi)存對齊后的大小
內(nèi)存回收快,用棧幀控制入棧出棧即可,沒有處理堆內(nèi)存的開銷
不需要引用計數(shù) (結(jié)構(gòu)體中使用引用類型作為屬性除外)
一般是靜態(tài)派發(fā),運行速度快,也方便編譯器優(yōu)化,如內(nèi)聯(lián)等
賦值時深拷貝。系統(tǒng)通過Copy-On-Write,避免不必要的copy,減少拷貝開銷
沒有隱式數(shù)據(jù)共享,具有獨立性不可變性
可通過mutating去修改結(jié)構(gòu)體中的屬性。這樣在保證值類型的獨立性的同時,也能支持對部分屬性的修改。
線程安全,一般來說沒有競態(tài)條件和死鎖(要注意確定值在各個子線程中是被copy過的)
不支持繼承,避免OOP子類過于耦合父類的問題。
可通過協(xié)議和泛型實現(xiàn)抽象。但實現(xiàn)協(xié)議的結(jié)構(gòu)體內(nèi)存大小不同,因此無法直接放入數(shù)組中,為了存儲的一致性,傳參賦值時系統(tǒng)會引入中間層Existential Container。此處如果結(jié)構(gòu)體屬性較多會復(fù)雜一點,但蘋果也會有優(yōu)化(Indirect Storage With Copy-On-Write),較少開銷。總體來說,值類型的多態(tài)是有成本的,系統(tǒng)會盡量優(yōu)化。開發(fā)者要考慮的是:減少動態(tài)多態(tài)把協(xié)議直接當(dāng)做類來使用,需要更多考慮靜態(tài)多態(tài),多結(jié)合泛型約束來使用。
引用類型(Class Function Closure等等):
引用類型在內(nèi)存使用上沒有值類型高效,在堆上創(chuàng)建并需要有棧指針指向該區(qū)域,增加了堆內(nèi)存分配和回收的開銷
賦值消耗小,一般是淺拷貝復(fù)制指針。但有引用計數(shù)成本
多個指針可指向同一內(nèi)存,獨立性差,容易誤操作
非線程安全,要考慮原子性,多線程需要線程鎖配合
需要引用計數(shù)來控制內(nèi)存釋放,使用不當(dāng)會有野指針、內(nèi)存泄漏和循環(huán)引用的風(fēng)險
允許繼承,但繼承的Side effect就是子類與父類的緊耦合。比如系統(tǒng)的 UIStackView主要目的只是用來布局使用,但卻不得不繼承UIView的所有屬性和方法。
由此可見,Swift提供了更強大的值類型試圖來解決ObjC時代OOP的子類與父類的緊耦合、對象隱式數(shù)據(jù)共享、非線程安全、引用計數(shù)等典型痛點。翻看Swift 標(biāo)準(zhǔn)庫會發(fā)現(xiàn)其主要由值類型組成,基本類型集合如 Int,Double,F(xiàn)loat,String,Array,Dictionary,Set,Tuple也都是結(jié)構(gòu)體。當(dāng)然,雖然值類型有眾多優(yōu)點,但也不是說要完全拋棄Class,還是要根據(jù)實際情況分析,實際的Swift開發(fā)中更多的是一個種結(jié)合的方式,完全不使用OOP也是不現(xiàn)實的。
和使用C語言結(jié)構(gòu)體一樣,Swift結(jié)構(gòu)體的大小就是內(nèi)部屬性內(nèi)存對齊后的大小。結(jié)構(gòu)體中屬性放置在不同的順序會影響最后的內(nèi)存大小。可使用系統(tǒng)提供的 MemoryLayout查看相應(yīng)結(jié)構(gòu)體占用內(nèi)存大小。
我們從一些細(xì)節(jié)層面做了review,比如對于Int32完全滿足的場景沒有必要使用Int,不要使用String或者Int代替應(yīng)該使用Bool的場景,內(nèi)存小的屬性盡量放在后面等等。
struct GameBoard {var p1Score: Int32var p2Score: Int32var gameOver: Bool}struct GameBoard2 {var p1Score: Int32var gameOver: Boolvar p2Score: Int32}//基于CPU尋址效率考慮,GameBoard2字節(jié)對齊后占用空間更多MemoryLayout<GameBoard>.self.size //4 + 4 + 1 = 9(bytes)MemoryLayout<GameBoard2>.self.size //4 + 4 + 4 = 12(bytes)
上面提到值類型的時候,我們有提到靜態(tài)多態(tài),靜態(tài)多態(tài)是指編譯器能在編譯時期確定類型的多態(tài)。這樣編譯器可以類型降級,在編譯時可產(chǎn)生特定類型的方法。
將泛型定義為遵守某個協(xié)議的約束可以避免直接把協(xié)議直接當(dāng)做類來傳參使用,否則編譯器會報錯,相當(dāng)于接口支持多態(tài),但調(diào)用時要用特定的類型調(diào)用,從而達(dá)到了靜態(tài)多態(tài)的目的。對于靜態(tài)多態(tài),編譯器會充分利用其靜態(tài)特性做優(yōu)化,同時在設(shè)置了WMO全模塊優(yōu)化(Whole Module Optimization)的情況下會盡量控制由此可能產(chǎn)生的代碼增長。
簡而言之,開發(fā)者要盡可能多考慮靜態(tài)多態(tài)。比如在使用協(xié)議作為函數(shù)的參數(shù)時,可以引入泛型。WWDC中有很經(jīng)典的討論:
protocol Drawable {func draw()}struct Line: Drawable {var x: Double = 0func draw() {}}func drawACopy<T: Drawable>(local: T) {//指定T必須遵守Drawablelocal.draw()}let line = Line()drawACopy(local: line)//Success (傳入具體的實現(xiàn)了Drawable的結(jié)構(gòu)體,編譯器可推斷其類型)let line2: Drawable = Line()drawACopy(local: line2)//Error,編譯器不允許直接使用Drawable協(xié)議作為入?yún)?/span>
對于類的繼承父類和遵守協(xié)議,Swift更愿意選擇后者。ObjC中OOP的形式,在Swift里基本都可以使用 Structs/Enums + Protocols + Protocol extensions + Generics 來實現(xiàn)邏輯抽象。
我們盡量減少了項目中使用OOP的場景,盡可能的只用值類型面向協(xié)議和利用泛型,這樣編譯器能做更多的靜態(tài)優(yōu)化,更能降低OOP超類帶來的緊耦合。
同時,Protocol extension能夠為protocol提供一個默認(rèn)實現(xiàn),這也是區(qū)別于ObjC協(xié)議的很重要的優(yōu)化。
使用時要注意,應(yīng)該用具體的類型去調(diào)用Protocol extension中的方法,而不是用通過類型推斷得到的Protocol來調(diào)用。使用Protocol調(diào)用時,如果該方法沒有在Protocol中定義,Protocol extension中的默認(rèn)實現(xiàn)將被調(diào)用,即使具體的類型中有實現(xiàn)對應(yīng)方法。因為此時編譯器此時只能找到默認(rèn)實現(xiàn)。
相對于ObjC, Swift 中對 Error 和 Throw 的處理更加完善,這樣顯而易見的好處是API更友好,提高可讀性,利用編輯器檢測降低出錯概率。ObjC時代大家往往不會考慮拋出異常的操作,這個也是習(xí)慣ObjC編碼的程序員在封裝底層API時需要注意的。常見的是使用繼承Error協(xié)議的Enum。
enum CustomError: Error {case error1case error2}
產(chǎn)生Error后也可以拋出讓外部處理,支持throw的方法后編譯器會做強檢測是否有處理throw。要注意 () throws -> Void 和 () -> Void 是不同的 Function Type。
//(Int)->Void可以賦值給(Int)throws->Voidlet a: (Int) throws -> Void = { n in}//反之類型不匹配 編譯報錯let b: (Int) -> Void = { n throws in}
rethrows:如果一個函數(shù)入?yún)⑹且粋€支持throw的函數(shù),那么通過rethrows可以標(biāo)識該函數(shù)同樣可以拋出Error。這樣在使用該函數(shù)時,編譯器會檢測是否需要try-catch。
這是我們在封裝基礎(chǔ)功能時需要考慮的,系統(tǒng)中友好的示例很多,比如map函數(shù)在系統(tǒng)中的定義:
public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]let a = [1, 2, 3]enum CustomError: Error {case error1case error2}do {let _ = try a.map { n -> Int inguard n >= 0 else {//如果map接受的closure內(nèi)部有拋出throw,編譯器會強制檢測外部是否有try-catchthrow CustomError.error1}return n * n}} catch CustomError.error1 {} catch {}
關(guān)鍵性檢測可以使用 Guard,其優(yōu)勢是可以增強可讀性,較少過多的if嵌套。使用Guard時,一般else里面會是 return、 throw、continue、 break等。
//if嵌套過多,難以閱讀,增加后期維護(hù)成本if (xxx){if (xxx) {if (xxx) {}}}//使用Guard,整體更清晰,便于后期維護(hù)let dict : Dictionary = ["key": "0"]guard let value1 = dict["key"], value == "0" else {return}guard let value2 = dict["key2"], value == "0" else {return}print("\(value1) \(value2)")
被defer修飾的closure會在當(dāng)前作用域退出的時候調(diào)用,主要用來避免重復(fù)添加返回前需要執(zhí)行的代碼,提高可讀性。
比如在我們macOS應(yīng)用中有對文件讀寫的操作,這時候使用defer可以確保不會忘記關(guān)閉文件。
func write() throws {//...guard let file = FileHandle(forUpdatingAtPath: filepath) else {throw WriteError.notFound}defer {try? file.close()}//...}
另外比較常用的場景是釋放鎖的時候,以及非逃逸閉包回調(diào)等。
但是defer不要過度使用,使用時要注意closure捕獲變量和作用域的問題。
比如如果在if語句中使用defer,則跳出if時,該defer就會被執(zhí)行。
對于可選值,要盡最大可能甚至完全避免強制拆包。大部分情況下如果遇到了需要使用 ! 的情況,很可能說明最初的設(shè)計是不合理的。包括downCasting時,由于類型轉(zhuǎn)換本身就有可能失敗,要避免使用 as! ,盡量使用as?,當(dāng)然try!也要避免。
對于可選值,永遠(yuǎn)要使用可選綁定檢測,確??蛇x變量具有真正的值存在,然后再進(jìn)行操作:
var optString: String?if let _ = optString {}
將項目中不需要必須創(chuàng)建的屬性,改為懶加載。Swift的懶加載相對于ObjC來說可讀性更好,也更容易實現(xiàn),使用Lazy修飾就好。
lazy var aLabel: UILabel = {let label = UILabel()return label}()
在類里面聲明過多的狀態(tài)變量是不利于后期維護(hù)的。Swift里函數(shù)可以作為函數(shù)參數(shù)、返回值以及變量,可以很好的支持函數(shù)式編程。利用函數(shù)式能有效減少全局變量或者狀態(tài)變量。
命令式編程更關(guān)注解決問題的步驟。直接反應(yīng)機器指令序列。有變量(對應(yīng)存儲單元),賦值語句(對應(yīng)獲取與存儲指令),表達(dá)式(對應(yīng) 指令算數(shù)計算),控制語句(對應(yīng)跳轉(zhuǎn)指令)。
函數(shù)式編程更關(guān)注數(shù)據(jù)的映射關(guān)系和數(shù)據(jù)的流向,即輸入和輸出。函數(shù)被當(dāng)做變量,既可以作為其它函數(shù)的參數(shù)(輸入值),也可以從函數(shù)中返回(輸出值)。將計算描述為表達(dá)式求值,自變量的映射f(x)->y,給定x,會穩(wěn)定映射為y。函數(shù)內(nèi)盡量不訪問函數(shù)作用域之外的變量,只依賴入?yún)ⅲ瑴p少狀態(tài)變量的聲明與維護(hù)。同時少用可變變量(對象),多用不可變變量(結(jié)構(gòu)體)。這樣就不會有其他side effects干擾。
利用柯里化把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)的函數(shù),將部分參數(shù)緩存到函數(shù)內(nèi)部。同時利用函數(shù)合成增加可讀性。比如做加法乘法計算,我們可以封裝加法和乘法函數(shù)然后逐一調(diào)用:
func add(_ a: Int, _ b: Int) -> Int { a + b }func multiple(_ a: Int, _ b: Int) -> Int { a * b }let n = 3multiple(add(n, 7), 6) //(n + 7) * 6 = 60
也可以使用函數(shù)式:
//柯里化add和multiple函數(shù): 由兩個入?yún)⒏臑橐粋€并返回一個(Int)->Int類型函數(shù)func add(_ a: Int) -> (Int) -> Int { { $0 + a} }func multiple(_ a: Int) -> (Int) -> Int { { $0 * a} }//函數(shù)合成 自定義中置運算符 > 增加可讀性infix operator > : AdditionPrecedencefunc >(_ f1: @escaping (Int)->Int,_ f2: @escaping (Int)->Int) -> (Int) -> Int {{f2(f1($0))}}//生成新的函數(shù) newFnlet n = 3let newFn = add(7) > multiple(6) // (Int)->Intprint( newFn(n) ) //(n + 7) * 6 = 60
可以看到,從使用multiple(add(n, 7), 6) 到 let newFn = add(7) > multiple(6), newFn(n),整體更清晰了,尤其是在更復(fù)雜的場景下,其優(yōu)勢會更明顯。
Swift提供了豐富簡便的語法糖以及強大的類型推斷,這些都讓Swift變得很容易上手入門。但是要從性能考慮或者是設(shè)計出更完美API的角度出發(fā),還是需要投入更多的實踐才行。訂單團(tuán)隊正在iOS小組件、AppClips、京東工作站(macOS桌面應(yīng)用)等場景下嘗試盡可能多的使用Swift與SwiftUI開發(fā),開發(fā)效率與項目穩(wěn)定性都有不錯的表現(xiàn)。目前京東集團(tuán)內(nèi)部對Swift的基礎(chǔ)設(shè)施正在逐步完善中,我們相信也希望未來集團(tuán)內(nèi)有更多的同學(xué)參與到Swift的開發(fā)中進(jìn)來。
https://developer.apple.com/videos/play/wwdc2016/419/
https://developer.apple.com/videos/play/wwdc2016/416/
https://swift.org/blog/whole-module-optimizations/
https://docs.swift.org/swift-book/GuidedTour/GuidedTour.html#//apple_ref/doc/uid/TP40014097-CH2
