2014年,蘋果公司在WWDC上發佈Swift這一新的編程語言。通過幾年的發展,Swift已經成爲iOS開發語言的「中流砥柱」,Swift提供了很是靈活的高級別特性,例如協議、閉包、泛型等,而且Swift還進一步開發了強大的SIL(Swift Intermediate Language)用於對編譯器進行優化,使得Swift相比Objective-C運行更快性能更優,Swift內部如何實現性能的優化,咱們本文就進行一下解讀,但願能對你們有所啓發和幫助。html
針對Swift性能提高這一問題,咱們能夠從概念上拆分爲兩個部分:git
下面咱們將從這兩個角度切入,對Swift性能優化進行分析。經過了解編譯器對不一樣數據結構處理的內部實現,來選擇最合適的算法機制,並利用編譯器的優化特性,編寫高性能的程序。github
理解Swift的性能,首先要清楚Swift的數據結構,組件關係和編譯運行方式。算法
數據結構編程
Swift的數據結構能夠大致拆分爲:Class
,Struct
,Enum
。swift
組件關係後端
組件關係能夠分爲:inheritance
,protocols
,generics
。數組
方法分派方式安全
方法分派方式能夠分爲Static dispatch
和Dynamic dispatch
。性能優化
要在開發中提升Swift性能,須要開發者去了解這幾種數據結構和組件關係以及它們的內部實現,從而經過選擇最合適的抽象機制來提高性能。
首先咱們對於性能標準進行一個概念陳述,性能標準涵蓋三個標準:
接下來,咱們會分別對這幾個指標進行說明。
內存分配能夠分爲堆區棧區,在棧的內存分配速度要高於堆,結構體和類在堆棧分配是不一樣的。
Stack
基本數據類型和結構體默認在棧區,棧區內存是連續的,經過出棧入棧進行分配和銷燬,速度很快,高於堆區。
咱們經過一些例子進行說明:
//示例 1 // Allocation // Struct struct Point { var x, y:Double func draw() { … } } let point1 = Point(x:0, y:0) //進行point1初始化,開闢棧內存 var point2 = point1 //初始化point2,拷貝point1內容,開闢新內存 point2.x = 5 //對point2的操做不會影響point1 // use `point1` // use `point2`
以上結構體的內存是在棧區分配的,內部的變量也是內聯在棧區。將point1
賦值給point2
實際操做是在棧區進行了一份拷貝,產生了新的內存消耗point2
,這使得point1
和point2
是徹底獨立的兩個實例,它們之間的操做互不影響。在使用point1
和point2
以後,會進行銷燬。
Heap
高級的數據結構,好比類,分配在堆區。初始化時查找沒有使用的內存塊,銷燬時再從內存塊中清除。由於堆區可能存在多線程的操做問題,爲了保證線程安全,須要進行加鎖操做,所以也是一種性能消耗。
// Allocation // Class class Point { var x, y:Double func draw() { … } } let point1 = Point(x:0, y:0) //在堆區分配內存,棧區只是存儲地址指針 let point2 = point1 //不產生新的實例,而是對point2增長對堆區內存引用的指針 point2.x = 5 //由於point1和point2是一個實例,因此point1的值也會被修改 // use `point1` // use `point2`
以上咱們初始化了一個Class
類型,在棧區分配一塊內存,可是和結構體直接在棧內存儲數值不一樣,咱們只在棧區存儲了對象的指針,指針指向的對象的內存是分配在堆區的。須要注意的是,爲了管理對象內存,在堆區初始化時,除了分配屬性內存(這裏是Double類型的x,y),還會有額外的兩個字段,分別是type
和refCount
,這個包含了type
,refCount
和實際屬性的結構被稱爲blue box
。
內存分配總結
從初始化角度,Class
相比Struct
須要在堆區分配內存,進行內存管理,使用了指針,有更強大的特性,可是性能較低。
優化方式:
對於頻繁操做(好比通訊軟件的內容氣泡展現),儘可能使用Struct
替代Class
,由於棧內存分配更快,更安全,操做更快。
Swift經過引用計數管理堆對象內存,當引用計數爲0時,Swift確認沒有對象再引用該內存,因此將內存釋放。對於引用計數的管理是一個很是高頻的間接操做,而且須要考慮線程安全,使得引用計數的操做須要較高的性能消耗。
對於基本數據類型的Struct
來講,沒有堆內存分配和引用計數的管理,性能更高更安全,可是對於複雜的結構體,如:
// Reference Counting // Struct containing references struct Label { var text:String var font:UIFont func draw() { … } } let label1 = Label(text:"Hi", font:font) //棧區包含了存儲在堆區的指針 let label2 = label1 //label2產生新的指針,和label1同樣指向一樣的string和font地址 // use `label1` // use `label2`
這裏看到,包含了引用的結構體相比Class
,須要管理雙倍的引用計數。每次將結構體做爲參數傳遞給方法或者進行直接拷貝時,都會出現多份引用計數。下圖能夠比較直觀的理解:
備註:包含引用類型的結構體出現Copy的處理方式
Class在拷貝時的處理方式:
引用計數總結
Class
在堆區分配內存,須要使用引用計數器進行內存管理。Struct
在棧區分配內存,無引用計數管理。Struct
經過指針管理在堆區的屬性,對結構體的拷貝會建立新的棧內存,建立多份引用的指針,Class
只會有一份。優化方式
在使用結構體時:
咱們以前在Static dispatch VS Dynamic dispatch中提到過,可以在編譯期肯定執行方法的方式叫作靜態分派Static dispatch,沒法在編譯期肯定,只能在運行時去肯定執行方法的分派方式叫作動態分派Dynamic dispatch。
Static dispatch
更快,並且靜態分派能夠進行內聯等進一步的優化,使得執行更快速,性能更高。
可是對於多態的狀況,咱們不能在編譯期肯定最終的類型,這裏就用到了Dynamic dispatch
動態分派。動態分派的實現是,每種類型都會建立一張表,表內是一個包含了方法指針的數組。動態分派更靈活,可是由於有查表和跳轉的操做,而且由於不少特色對於編譯器來講並不明確,因此至關於block了編譯器的一些後期優化。因此速度慢於Static dispatch
。
下面看一段多態代碼,以及分析實現方式:
//引用語義實現的多態 class Drawable { func draw() {} } class Point :Drawable { var x, y:Double override func draw() { … } } class Line :Drawable { var x1, y1, x2, y2:Double override func draw() { … } } var drawables:[Drawable] for d in drawables { d.draw() }
Method Dispatch總結
Class
默認使用Dynamic dispatch
,由於在編譯期幾乎每一個環節的信息都沒法肯定,因此阻礙了編譯器的優化,好比inline
和whole module inline
。
使用Static dispatch代替Dynamic dispatch提高性能
咱們知道Static dispatch
快於Dynamic dispatch
,如何在開發中去儘量使用Static dispatch
。
inheritance constraints
繼承約束 咱們可使用final
關鍵字去修飾Class
,以今生成的Final class
,使用Static dispatch
。
access control
訪問控制 private
關鍵字修飾,使得方法或屬性只對當前類可見。編譯器會對方法進行Static dispatch
。
編譯器能夠經過whole module optimization
檢查繼承關係,對某些沒有標記final
的類經過計算,若是能在編譯期肯定執行的方法,則使用Static dispatch
。 Struct
默認使用Static dispatch
。
Swift快於OC的一個關鍵是能夠消解動態分派。
總結
Swift提供了更靈活的Struct
,用以在內存、引用計數、方法分派等角度去進行性能的優化,在正確的時機選擇正確的數據結構,可使咱們的代碼性能更快更安全。
延伸
你可能會問Struct
如何實現多態呢?答案是protocol oriented programming
。
以上分析了影響性能的幾個標準,那麼不一樣的算法機制Class
,Protocol Types
和Generic code
,它們在這三方面的表現如何,Protocol Type
和Generic code
分別是怎麼實現的呢?咱們帶着這個問題看下去。
這裏咱們會討論Protocol Type如何存儲和拷貝變量,以及方法分派是如何實現的。不經過繼承或者引用語義的多態:
protocol Drawable { func draw() } struct Point :Drawable { var x, y:Double func draw() { … } } struct Line :Drawable { var x1, y1, x2, y2:Double func draw() { … } } var drawables:[Drawable] //遵照了Drawable協議的類型集合,多是point或者line for d in drawables { d.draw() }
以上經過Protocol Type
實現多態,幾個類之間沒有繼承關係,故不能按照慣例藉助V-Table
實現動態分派。
若是想了解Vtable和Witness table實現,能夠進行點擊查看,這裏不作細節說明。 由於Point和Line的尺寸不一樣,數組存儲數據實現一致性存儲,使用了Existential Container
。查找正確的執行方法則使用了 Protoloc Witness Table
。
Existential Container
是一種特殊的內存佈局方式,用於管理遵照了相同協議的數據類型Protocol Type
,這些數據類型由於不共享同一繼承關係(這是V-Table
實現的前提),而且內存空間尺寸不一樣,使用Existential Container
進行管理,使其具備存儲的一致性。
結構以下:
Protocol Type
的類型不一樣,內存空間,初始化方法等都不相同,爲了對Protocol Type
生命週期進行專項管理,用到了Value Witness Table
。Protocol Type
的方法分派。內存分佈以下:
1. payload_data_0 = 0x0000000000000004, 2. payload_data_1 = 0x0000000000000000, 3. payload_data_2 = 0x0000000000000000, 4. instance_type = 0x000000010d6dc408 ExistentialContainers`type metadata for ExistentialContainers.Car, 5. protocol_witness_0 = 0x000000010d6dc1c0 ExistentialContainers protocol witness table for ExistentialContainers.Car:ExistentialContainers.Drivable in ExistentialContainers
爲了實現Class
多態也就是引用語義多態,須要V-Table
來實現,可是V-Table
的前提是具備同一個父類即共享相同的繼承關係,可是對於Protocol Type
來講,並不具有此特徵,故爲了支持Struct
的多態,須要用到protocol oriented programming
機制,也就是藉助Protocol Witness Table
來實現(細節能夠點擊Vtable和witness table實現,每一個結構體會創造PWT
表,內部包含指針,指向方法具體實現)。
用於管理任意值的初始化、拷貝、銷燬。
Value Witness Table
的結構如上,是用於管理遵照了協議的Protocol Type
實例的初始化,拷貝,內存消減和銷燬的。
Value Witness Table
在SIL
中還能夠拆分爲%relative_vwtable
和%absolute_vwtable
,咱們這裏先不作展開。
Value Witness Table
和Protocol Witness Table
經過分工,去管理Protocol Type
實例的內存管理(初始化,拷貝,銷燬)和方法調用。
咱們來藉助具體的示例進行進一步瞭解:
// Protocol Types // The Existential Container in action func drawACopy(local :Drawable) { local.draw() } let val :Drawable = Point() drawACopy(val)
在Swift編譯器中,經過Existential Container
實現的僞代碼以下:
// Protocol Types // The Existential Container in action func drawACopy(local :Drawable) { local.draw() } let val :Drawable = Point() drawACopy(val) //existential container的僞代碼結構 struct ExistContDrawable { var valueBuffer:(Int, Int, Int) var vwt:ValueWitnessTable var pwt:DrawableProtocolWitnessTable } // drawACopy方法生成的僞代碼 func drawACopy(val:ExistContDrawable) { //將existential container傳入 var local = ExistContDrawable() //初始化container let vwt = val.vwt //獲取value witness table,用於管理生命週期 let pwt = val.pwt //獲取protocol witness table,用於進行方法分派 local.type = type local.pwt = pwt vwt.allocateBufferAndCopyValue(&local, val) //vwt進行生命週期管理,初始化或者拷貝 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,這裏說一下projectBuffer,由於不一樣類型在內存中是不一樣的(small value內聯在棧內,large value初始化在堆內,棧持有指針),因此方法的肯定也是和類型相關的,咱們知道,查找方法時是經過當前對象的地址,經過必定的位移去查找方法地址。 vwt.destructAndDeallocateBuffer(temp) //vwt進行生命週期管理,銷燬內存 }
咱們知道,Swift中Class
的實例和屬性都存儲在堆區,Struct
實例在棧區,若是包含指針屬性則存儲在堆區,Protocol Type
如何存儲屬性?Small Number經過Existential Container
內聯實現,大數存在堆區。如何處理Copy呢?
在出現Copy狀況時:
let aLine = Line(1.0, 1.0, 1.0, 3.0) let pair = Pair(aLine, aLine) let copy = pair
會將新的Exsitential Container
的valueBuffer指向同一個value即建立指針引用,可是若是要改變值怎麼辦?咱們知道Struct
值的修改和Class
不一樣,Copy是不該該影響原實例的值的。
這裏用到了一個技術叫作Indirect Storage With Copy-On-Write
,即優先使用內存指針。經過提升內存指針的使用,來下降堆區內存的初始化。下降內存消耗。在須要修改值的時候,會先檢測引用計數檢測,若是有大於1的引用計數,則開闢新內存,建立新的實例。在對內容進行變動的時候,會開啓一塊新的內存,僞代碼以下:
class LineStorage { var x1, y1, x2, y2:Double } struct Line :Drawable { var storage :LineStorage init() { storage = LineStorage(Point(), Point()) } func draw() { … } mutating func move() { if !isUniquelyReferencedNonObjc(&storage) { //如何存在多份引用,則開啓新內存,不然直接修改 storage = LineStorage(storage) } storage。start = ... } }
這樣實現的目的:經過多份指針去引用同一份地址的成本遠遠低於開闢多份堆內存。如下對比圖:
支持Protocol Type
的動態多態(Dynamic Polymorphism
)行爲。
經過使用Witness Table
和Existential Container
來實現。
對於大數的拷貝能夠經過Indirect Storage
間接存儲來進行優化。
說到動態多態Dynamic Polymorphism
,咱們就要問了,什麼是靜態多態Static Polymorphism
,看看下面示例:
// Drawing a copy protocol Drawable { func draw() } func drawACopy(local :Drawable) { local.draw() } let line = Line() drawACopy(line) // ... let point = Point() drawACopy(point)
這種狀況咱們就能夠用到泛型Generic code
來實現,進行進一步優化。
咱們接下來會討論泛型屬性的存儲方式和泛型方法是如何分派的。泛型和Protocol Type
的區別在於:
foo
和bar
方法是同一種類型。對於如下示例:
func foo<T:Drawable>(local :T) { bar(local) } func bar<T:Drawable>(local:T) { … } let point = Point() foo(point)
分析方法foo
和bar
的調用過程:
//調用過程 foo(point)-->foo<T = Point>(point) //在方法執行時,Swift將泛型T綁定爲調用方使用的具體類型,這裏爲Point bar(local) -->bar<T = Point>(local) //在調用內部bar方法時,會使用foo已經綁定的變量類型Point,能夠看到,泛型T在這裏已經被降級,經過類型Point進行取代
泛型方法調用的具體實現爲:
Existential Container
, 而是將Protocol/Value Witness Table
做爲調用方的額外參數進行傳遞。VWT
和PWT
來執行。看到這裏,咱們並不以爲泛型比Protocol Type
有什麼更快的特性,泛型如何更快呢?靜態多態前提下能夠進行進一步的優化,稱爲特定泛型優化。
specialization
由於是靜態多態。因此能夠進行很強大的優化,好比進行內聯實現,而且經過獲取上下文來進行更進一步的優化。從而下降方法數量。優化後能夠更精確和具體。例如:
func min<T:Comparable>(x:T, y:T) -> T { return y < x ? y : x }
從普通的泛型展開以下,由於要支持全部類型的min
方法,因此須要對泛型類型進行計算,包括初始化地址、內存分配、生命週期管理等。除了對value的操做,還要對方法進行操做。這是一個很是複雜龐大的工程。
func min<T:Comparable>(x:T, y:T, FTable:FunctionTable) -> T { let xCopy = FTable.copy(x) let yCopy = FTable.copy(y) let m = FTable.lessThan(yCopy, xCopy) ? y :x FTable.release(x) FTable.release(y) return m }
在肯定入參類型時,好比Int,編譯器能夠經過泛型特化,進行類型取代(Type Substitute),優化爲:
func min<Int>(x:Int, y:Int) -> Int { return y < x ? y :x }
泛型特化specilization
是什麼時候發生的?
在使用特定優化時,調用方須要進行類型推斷,這裏須要知曉類型的上下文,例如類型的定義和內部方法實現。若是調用方和類型是單獨編譯的,就沒法在調用方推斷類型的內部實行,就沒法使用特定優化,保證這些代碼一塊兒進行編譯,這裏就用到了whole module optimization
。而whole module optimization
是對於調用方和被調用方的方法在不一樣文件時,對其進行泛型特化優化的前提。
特定泛型的進一步優化:
// Pairs in our program using generic types struct Pair<T :Drawable> { init(_ f:T, _ s:T) { first = f ; second = s } var first:T var second:T } let pairOfLines = Pair(Line(), Line()) // ... let pairOfPoint = Pair(Point(), Point())
在用到多種泛型,且肯定泛型類型不會在運行時修改時,就能夠對成對泛型的使用進行進一步優化。
優化的方式是將泛型的內存分配由指針指定,變爲內存內聯,再也不有額外的堆初始化消耗。請注意,由於進行了存儲內聯,已經肯定了泛型特定類型的內存分佈,泛型的內存內聯不能存儲不一樣類型。因此再次強調此種優化只適用於在運行時不會修改泛型類型,即不能同時支持一個方法中包含line
和point
兩種類型。
###whole module optimization whole module optimization
是用於Swift編譯器的優化機制。能夠經過-whole-module-optimization
(或 -wmo
)進行打開。在XCode 8以後默認打開。 Swift Package Manager
在release模式默認使用whole module optimization
。module是多個文件集合。
編譯器在對源文件進行語法分析以後,會對其進行優化,生成機器碼並輸出目標文件,以後連接器聯合全部的目標文件生成共享庫或可執行文件。 whole module optimization
經過跨函數優化,能夠進行內聯等優化操做,對於泛型,能夠經過獲取類型的具體實現來進行推斷優化,進行類型降級方法內聯,刪除多餘方法等操做。
全模塊優化的優點
如何下降編譯時間 和全模塊優化相反的是文件優化,即對單個文件進行編譯。這樣的好處在於能夠並行執行,而且對於沒有修改的文件不會再次編譯。缺點在於編譯器沒法獲知全貌,沒法進行深度優化。下面咱們分析下全模塊優化如何避免沒修改的文件再次編譯。
編譯器內部運行過程分爲:語法分析,類型檢查,SIL
優化,LLVM
後端處理。
語法分析和類型檢查通常很快,SIL
優化執行了重要的Swift特定優化,例如泛型特化和方法內聯等,該過程大概佔用整個編譯時間的三分之一。LLVM
後端執行佔用了大部分的編譯時間,用於運行降級優化和生成代碼。
進行全模塊優化後,SIL
優化會將模塊再次拆分爲多個部分,LLVM
後端經過多線程對這些拆分模塊進行處理,對於沒有修改的部分,不會進行再處理。這樣就避免了修改一小部分,整個大模塊進行LLVM
後端的再次執行,除此外,使用多線程並行操做也會縮短處理時間。
Swift由於方法分派機制問題,因此在設計和優化後,會產生和咱們常規理解不太一致的結果,這固然不能算Bug。可是仍是要單獨進行說明,避免在開發過程當中,由於對機制的掌握不足,形成預期和執行出入致使的問題。
Message dispatch
咱們經過上面說明結合Static dispatch VS Dynamic dispatch對方法分派方式有了瞭解。這裏須要對Objective-C
的方法分派方式進行說明。
熟悉OC的人都知道,OC採用了運行時機制使用obj_msgSend
發送消息,runtime很是的靈活,咱們不只能夠對方法調用採用swizzling
,對於對象也能夠經過isa-swizzling
來擴展功能,應用場景有咱們經常使用的hook和你們熟知的KVO
。
你們在使用Swift進行開發時都會問,Swift是否可使用OC的運行時和消息轉發機制呢?答案是能夠。
Swift能夠經過關鍵字dynamic
對方法進行標記,這樣就會告訴編譯器,此方法使用的是OC的運行時機制。
注意:咱們常見的關鍵字
@ObjC
並不會改變Swift原有的方法分派機制,關鍵字@ObjC
的做用只是告訴編譯器,該段代碼對於OC可見。
總結來講,Swift經過dynamic
關鍵字的擴展後,一共包含三種方法分派方式:Static dispatch
,Table dispatch
和Message dispatch
。下表爲不一樣的數據結構在不一樣狀況下采起的分派方式:
![Swift dispatch method](img/Swift_Compile_Performance/Swift dispatch method.png)
若是在開發過程當中,錯誤的混合了這幾種分派方式,就可能出現Bug,如下咱們對這些Bug進行分析:
SR-584 此狀況是在子類的extension中重載父類方法時,出現和預期不一樣的行爲。
class Base:NSObject { var directProperty:String { return "This is Base" } var indirectProperty:String { return directProperty } } class Sub:Base { } extension Sub { override var directProperty:String { return "This is Sub" } }
執行如下代碼,直接調用沒有問題:
Base().directProperty // 「This is Base」 Sub().directProperty // 「This is Sub」
間接調用結果和預期不一樣:
Base()。indirectProperty // 「This is Base」 Sub()。indirectProperty // expected "this is Sub",but is 「This is Base」 <- Unexpected!
在Base.directProperty
前添加dynamic
關鍵字就能夠得到"this is Sub"的結果。Swift在extension 文檔中說明,不能在extension中重載已經存在的方法。
「Extensions can add new functionality to a type, but they cannot override existing functionality.」
會出現警告:Cannot override a non-dynamic class declaration from an extension
。
出現這個問題的緣由是,NSObject的extension是使用的Message dispatch
,而Initial Declaration
使用的是Table dispath
(查看上圖 Swift Dispatch Method)。extension重載的方法添加在了Message dispatch
內,沒有修改虛函數表,虛函數表內仍是父類的方法,故會執行父類方法。想在extension重載方法,須要標明dynamic
來使用Message dispatch
。
協議的擴展內實現的方法,沒法被遵照類的子類重載:
protocol Greetable { func sayHi() } extension Greetable { func sayHi() { print("Hello") } } func greetings(greeter:Greetable) { greeter.sayHi() }
如今定義一個遵照了協議的類Person
。遵照協議類的子類LoudPerson
:
class Person:Greetable { } class LoudPerson:Person { func sayHi() { print("sub") } }
執行下面代碼結果爲:
var sub:LoudPerson = LoudPerson() sub.sayHi() //sub
不符合預期的代碼:
var sub:Person = LoudPerson() sub.sayHi() //HellO <-使用了protocol的默認實現
注意,在子類LoudPerson
中沒有出現override
關鍵字。能夠理解爲LoudPerson
並無成功註冊Greetable
在Witness table
的方法。因此對於聲明爲Person
實際爲LoudPerson
的實例,會在編譯器經過Person
去查找,Person
沒有實現協議方法,則不產生Witness table
,sayHi
方法是直接調用的。解決辦法是在base類內實現協議方法,無需實現也要提供默認方法。或者將基類標記爲final
來避免繼承。
進一步經過示例去理解:
// Defined protocol。 protocol A { func a() -> Int } extension A { func a() -> Int { return 0 } } // A class doesn't have implement of the function。 class B:A {} class C:B { func a() -> Int { return 1 } } // A class has implement of the function。 class D:A { func a() -> Int { return 1 } } class E:D { override func a() -> Int { return 2 } } // Failure cases。 B().a() // 0 C().a() // 1 (C() as A).a() // 0 # We thought return 1。 // Success cases。 D().a() // 1 (D() as A).a() // 1 E().a() // 2 (E() as A).a() // 2
其餘
咱們知道Class extension使用的是Static Dispatch:
class MyClass { } extension MyClass { func extensionMethod() {} } class SubClass:MyClass { override func extensionMethod() {} }
以上代碼會出現錯誤,提示Declarations in extensions can not be overridden yet
。
影響程序的性能標準有三種:初始化方式, 引用指針和方法分派。
文中對比了兩種數據結構:Struct
和Class
的在不一樣標準下的性能表現。Swift相比OC和其它語言強化告終構體的能力,因此在瞭解以上性能表現的前提下,經過利用結構體能夠有效提高性能。
在此基礎上,咱們還介紹了功能強大的結構體的類:Protocol Type
和Generic
。而且介紹了它們如何支持多態以及經過使用有條件限制的泛型如何讓程序更快。
##做者簡介
亞男,美團點評iOS工程師。2017年加入美團點評,負責專業版餐飲管家開發,研究編譯器原理。目前正積極推進Swift組件化建設。
咱們餐飲生態技術部是一個技術氛圍活躍,大牛彙集的地方。新到店緊握真正的大規模SaaS實戰機會,多租戶、數據、安全、開放平臺等全方位的挑戰。業務領域複雜技術挑戰多,技術和業務能力迅速提高,最重要的是,加入咱們,你將實現真正經過代碼來改變行業的夢想。咱們歡迎各端人才加入,Java優先。感興趣的同窗趕忙發送簡歷至 zhaoyanan02@meituan.com,咱們期待你的到來。