A站 的 Swift 實踐

文章已發(fā)在快手大前端公眾號,歡迎關注,文章地址如下:

新文章寫得賊累豁跑。完美錯過了一覽祖國大好人海的機會殿怜,TIANROAST??咖啡也沒喝成典蝌,新買Switch怪物獵人崛起也僅限于炎火村。如果喜歡的話头谜,求轉(zhuǎn)發(fā)骏掀、“在看”和一個大大的贊。

下面是正文內(nèi)容柱告,我轉(zhuǎn)到博客里來截驮。

背景介紹

AcFun俗稱為“A站”,作為一款二次元內(nèi)容社區(qū)產(chǎn)品末荐,以“認真你就輸了”為文化導向侧纯,倡導輕松歡快的亞文化。AcFun涵蓋了中長視頻甲脏,小視頻眶熬,番劇妹笆,文章等眾多內(nèi)容,支撐這些內(nèi)容的大部分功能都選擇了用Swift開發(fā)娜氏,早在2019年拳缠,AcFun的iOS技術團隊就已將Swift作為AcFun app和水母app的開發(fā)首選語言。Swift的出現(xiàn)為用戶提供了更穩(wěn)定的使用體驗和更快的產(chǎn)品更新節(jié)奏贸弥,同時也為研發(fā)工程師創(chuàng)造了更高效舒適的開發(fā)體驗窟坐。Objective-C已成過去時,AcFun正全面擁抱Swift绵疲,駛入iOS開發(fā)快車道哲鸳。

蘋果推出Swift的原因

谷歌作為蘋果最大的競爭對手,除了Android上的Kotlin盔憨,還推出了Flutter和Fuchsia里在用的Dart語言徙菠,這些語言的口碑和易用性遠高于蘋果的Objective-C(后面簡稱OC)。OC歷史久遠郁岩,是C語言的超集婿奔,因此其發(fā)展受C語言限制。在這樣的背景下问慎,大家都以為蘋果會忽視其它新語言萍摊,但其實蘋果對于那些新語言特性垂涎已久,將想法施于行動的是當時還在蘋果的_ Chris Lattner_如叼。Chris是狂熱的編譯器和編程語言愛好者冰木,C、C++薇正、OC語言編譯器LLVM的創(chuàng)造者片酝,在LLVM開發(fā)過程中,Chris對類C語言有著很多不滿意的地方挖腰,比如預處理器雕沿、Trigraphs還有多年積累的奇怪東西。

為了能夠兼顧類似React一樣的編程范式和具備Java等正流行的語言的優(yōu)秀特性猴仑,Swift經(jīng)歷了長期的ABI穩(wěn)定和語言特性迭代增加的過程审轮,最終推出了能和JSX聲明式UI匹敵的Result Builders,并且通過SwiftUI和Combine這種能極大提升開發(fā)效率的框架讓開發(fā)者收獲了驚喜辽俗。

可能是Swift的ABI穩(wěn)定得太晚疾渣,不止各大APP里已經(jīng)積累了大量的OC庫和業(yè)務代碼,蘋果系統(tǒng)里的OC占比也依然很高崖飘,_博客《Evolution of the programming languages from iPhone OS 1.0 to iOS 14》 _統(tǒng)計了 iOS 歷史版本 OC 占比榴捡,從文章中可以看到最近的iOS 14版本里OC占比高達88%,C和C++主要用于音視頻朱浴、電話吊圾、網(wǎng)絡等比較基礎的模塊达椰,其占比相對穩(wěn)定,特別是C并沒有明顯增加项乒。不過在最近幾個版本中啰劲,Swift占比持續(xù)增高,iOS 14達到了8%檀何,可以看出蘋果正在使用Swift重構以前的庫蝇裤。

蘋果實際采取的行動

為了讓廣大開發(fā)者能夠用上更方便安全的Swift,蘋果采取了一系列實際行動频鉴。比如不再給OC新加接口栓辜,而是用Swift替換SDK,WWDC17之后就已經(jīng)看不到OC的例子了砚殿,蘋果主推的一些前沿技術啃憎,比如AR、AI似炎、Health等,在新版里也都只有Swift版本悯姊。所以羡藐,在未來的發(fā)展中,企業(yè)不考慮Swift或是缺少Swift人才悯许,都將會影響到新技術的引入仆嗦。

另外,蘋果的RealityKit先壕、CareKit瘩扼、Create ML、System垃僚、WidgetKit集绰、CryptoKit、Combine谆棺、SwiftUI等框架在與OC混編時都非常困難栽燕,從這些方面可以看出,蘋果所有新開發(fā)的框架都在避免和OC產(chǎn)生關系改淑,甚至自WWDC2020起新增加的App Widget只能用SwiftUI開發(fā)碍岔。

對于蘋果一系列的行動,社區(qū)與之對應的反應是沒有熱情去回答OC的Bug了朵夏,因為有了更好的追求蔼啦。OC三方庫作者也沒有維護的意愿,更新周期比Swift長很多仰猖,比如大家都知道的OC網(wǎng)絡庫AFNetworking捏肢,最新版本更新用了2年多時間奈籽,而該作者用Swift開發(fā)的對應的網(wǎng)絡庫Alamofire,更新頻率接近半個月猛计,作者對Swift的熱情可見一斑唠摹。

iOS開發(fā)首選語言也是Swift,以后可能會面臨OC工程師后繼無人的局面奉瘤,物以稀為貴勾拉,OC開發(fā)者的成本也會大增。使用Swift相關技術棧的團隊在吸引人才方面也存在一定優(yōu)勢盗温,AcFun的工程師田賽同學此前選擇了快手藕赞,而拒絕了另外一家公司的offer,一個重要因素就是AcFun可以使用Swift開發(fā)卖局。AcFun的iOS開發(fā)工程師關旭航說:“起初我們團隊在業(yè)務開發(fā)中探索Swift時斧蜕,對Swift不夠熟悉,并不敢主動嘗試使用砚偶,通過組內(nèi)的培訓以及業(yè)余時間的學習批销,我對這門語言越來越感興趣,看到其他同學寫的Swift代碼既簡潔又易懂染坯,我也慢慢開始嘗試使用均芽,現(xiàn)在我已經(jīng)不想寫Objective-C了”。

按照目前這個趨勢单鹿,使用Swift勢在必行掀宋。

Swift在AcFun的演進

2019年AcFun完成了Swift的調(diào)研和初期基礎設施建設,團隊Swift培訓以及業(yè)務的試點仲锄。在Swift調(diào)研探索過程中劲妙,AcFun開發(fā)同學體驗到了Swift的優(yōu)雅澄惊、精簡以及安全,也經(jīng)歷混編構建時間長和代碼補全慢等問題。其中構建問題只要遵循官方Module的最佳實踐就可以規(guī)避啤贩,代碼補全問題在Xcode12中得到了很好的改善枉氮。2020年上半年AcFun開始了混編工程優(yōu)化楼肪、組件化以及二進制化建設,借LLVM Module抹平模塊API在語言上的差異暂殖,基礎庫進行了Module化問題修復,并基于主站二進制化方案,完善了對Swift混編的支持。目前二進制化率為80%鳞骤,約50%的組件完成了LLVM Module化篙梢,構建速度提升了60%以上美旧。

AcFun 當前的 Swift & OC 混編架構如下圖所示贬墩。Infra層包含自研基礎庫妄呕、快手系中臺SDK以及第三方庫。Business Support層為各業(yè)務Feature提供通用業(yè)務支撐。Business Modules層包含當前已完成解耦和Module化的業(yè)務模塊颁井,模塊之間通過依賴注入容器和路由進行通信葵硕。當前Main Target中仍然存在尚未解耦的混編代碼懈凹,OC 和 Swift 之間通過橋接進行交互蜀变,另外有一些尚未Module化的OC基礎庫仍然需要通過Bridging Header橋接給Swift使用。這些橋接是影響編譯時間以及代碼補全速度的主要因素介评。

隨著架構的演進和組件化的推進库北,未來理想目標架構愿景如下圖所示。Infra 和 Business Support 層為業(yè)務提供更完整的基礎和通用業(yè)務支撐们陆,業(yè)務模塊全面解耦寒瓦、Module化和二進制化,組件均以Module的形式組織和聚合坪仇,Main Target 實現(xiàn)殼工程化杂腰。

目前AcFun的Swift文件數(shù)占工程總數(shù)40%之多,崩潰率減少了52%椅文。AcFun采用混編后欠气,性能方面钥弯,比如啟動時間缸逃、頁面流暢度耕突、內(nèi)存、CPU/GPU負載等方面差別不大芹橡。AcFun的QA負責人邵國強不禁感嘆:“AcFun的移動端研發(fā)同學開始探索Swift時毒坛,QA團隊起初沒有明顯感知,但隨著研發(fā)團隊Swift建設的推進,發(fā)版頻率也提速到單周以后煎殷,研發(fā)同學能持續(xù)高質(zhì)量交付屯伞,真是太棒了『乐保”

Swift的內(nèi)存管理是通過嚴格的劣摇、確定性的引用計數(shù)來自動管理的,可以將內(nèi)存的使用量降到最低弓乙,還可以避免垃圾收集在錯誤線程使用Finalizer末融,執(zhí)行多次不能管理數(shù)據(jù)庫句柄之類資源的問題。ARC的Retain和Release開銷在垃圾收集里也會有暇韧,比如在存儲一個對象屬性時用Write Barrier勾习。ARC的算法類似Go的Tricolor算法。垃圾回收還會移動和壓縮對象懈玻,如果調(diào)用C代碼巧婶,可能還會得到一個Dangling Pointer,比如JNI涂乌,就明確需要引入和維護對象艺栈,無形中增加了復雜度,還很容易出問題湾盒。

在atp播客205期節(jié)目中 Episode 205: Chris Lattner Interview Transcript湿右,Chris Lattner 指出OC之所以不安全的原因是因為OC是基于C語言,有指針罚勾,有不完全初始化的變量毅人,會數(shù)組越界,即使對工具鏈和編譯器有完全的控制權尖殃,也無法很好地解決以上的問題堰塌,解決Dangling Pointer就需要解決生命周期問題,而C沒有一個框架能解決分衫,改成兼容方式進入系統(tǒng)也是行不通的。因此蘋果團隊經(jīng)過思考般此,決定創(chuàng)建一門“安全”的編程語言蚪战,這種安全不止是指沒有Bug,而是在保持安全的同時還能夠保證高性能铐懊,進而推動整個編程模型前進邀桑。

Swift消除了整個類別的不安全代碼。變量在使用前總是被初始化科乎,數(shù)組和整數(shù)會被檢查是否有溢出壁畸,內(nèi)存會被自動管理,對內(nèi)存的獨占訪問可以防止許多編程錯誤。

Swift有靜態(tài)調(diào)度安全的特性捏萍,比C語言更安全太抓,很多問題能在編譯時提前發(fā)現(xiàn)。代碼中發(fā)生內(nèi)存溢出令杈,編譯器會發(fā)出診斷信息走敌,比如常量中的內(nèi)存溢出很難查。數(shù)組越界檢查逗噩,還有函數(shù)返回可達性檢測掉丽,確保返回值和函數(shù)定義的類型一致。

編譯器中的類型安全性可以讓問題更早暴露异雁。例如Swift Optional的設計在編譯期阻斷了空值訪問捶障,又如利用范型類型推導在編譯期提供約束,從而避免Unsafe Type Casting纲刀。水母的研發(fā)工程師趙赫在使用Swift過程中项炼,發(fā)現(xiàn)代碼中的很多問題和隱患都可以在編譯期暴露出來,在大部分情況下代碼只要能編譯通過柑蛇,運行效果就不會離預期有很大的偏差芥挣,這讓他對其代碼交付質(zhì)量更加充滿信心。

Swift語言的演進

Swift的演進比較穩(wěn)定耻台,并沒有在初期版本一股腦把特性都加上空免,而是每個版本迭代增加特性。演進之路如下圖所示:

Swift第一個版本推出了基本語法盆耽,Swift2.0主要是將泛型和協(xié)議能力做了提升蹋砚,并對Linux進行了支持,后端框架Vapor和Perfect也是在Swift2.0時出現(xiàn)的摄杂,Swift也是在這個版本開源的坝咐。Swift3.0出了Swift Package Manager,對標準庫API進行了重新的設計析恢。4.0 推出Codable協(xié)議和Key path墨坚。5.0終于ABI穩(wěn)定,Swift運行時內(nèi)置到了iOS12系統(tǒng)里映挂。5.1版本推出了讓大家感到蘋果活力的SwiftUI和Combine泽篮,新增了一大堆圍繞提升開發(fā)舒適度的Property Wrapper、Opaque Type等語言特性柑船,隨之帽撑,社區(qū)開始異常活躍起來鞍时,與之對應的技術文章大量輸出亏拉。AcFun就使用了5.1版本的Property Wrapper包裝了UserDefaults扣蜻,Codable,RxSwift Relay等及塘,業(yè)務開發(fā)過程中避免雷同代碼的編寫莽使。

Swift 6的 Roadmap_ _表明了Swift下一步發(fā)展方向是優(yōu)化Swift部署安裝,比如LSP和包管理等磷蛹;豐富開源生態(tài)吮旅,包括完善標準庫,開發(fā)類似科學計算這樣的新庫味咳;圍繞開發(fā)體驗的構建和代碼補全提速庇勃、豐富診斷信息、穩(wěn)定調(diào)試體驗等槽驶;DSL能力提升责嚷;完善低級別系統(tǒng)編程和機器學習等重要領域的拓展;提供內(nèi)存所有權和并發(fā)等主要語言特性的方案掂铐,要做到出色為止罕拂。

目前Swift這個項目的負責人叫 Ted Kremenek,斯坦福博士全陨,他之前還是Rust的主力開發(fā)爆班。在蘋果工作的十年,一個人做了Clang的靜態(tài)分析器辱姨,后面一直管理著Clang和Swift項目柿菩,向Chris匯報。Swift項目團隊核心成員還有Dave Abrahams(已退出)雨涛、John McCall枢舶、Doug Gregor、Joe Groff替久、Saleem Abdulrasool(移植Swift到windows)凉泄、Tom Doron(創(chuàng)建SwiftNIO)等,他們的身影活躍在Github的Swift各個提案中蚯根。

回到我們身邊后众,國內(nèi)Swift用的情況怎么樣?

一些耳熟能詳?shù)腁pp颅拦,比如微信吼具、淘寶、百度矩距、支付寶、拼多多怖竭、京東锥债、嗶哩嗶哩、優(yōu)酷、小紅書等都已經(jīng)開始嘗試使用Swift哮肚,這些App無一例外都采用了Swift和OC混編開發(fā)登夫。由于國內(nèi)業(yè)務競爭壓力大,很難像國外公司Uber那樣花大半年時間全部用Swift重構允趟,因此如果要在現(xiàn)有工程基礎上引入Swift開發(fā)恼策,不可避開采用混編開發(fā)。很多App使用Swift混編潮剪,也是因為蘋果對Widget功能開發(fā)語言設置了限制涣楷,即只能使用Swift,看來蘋果公司這個策略是相當有效的抗碰。

框架選擇

而正式進入混編開發(fā)前狮斗,需要先做開發(fā)框架的選型,我們先從架構演進開始說起弧蝇。

架構演進

一般App經(jīng)過多年發(fā)展碳褒,架構都會經(jīng)過如下四個階段:

如圖所示,App架構從單Module看疗,MVC架構到幾百個Module沙峻,無依賴,動態(tài)跳轉(zhuǎn)两芳。團隊從小變大摔寨,如今App的架構更偏重高質(zhì)量、穩(wěn)定性和高可維護性盗扇。蘋果公司也是順應發(fā)展趨勢祷肯,先后推出提高穩(wěn)定性的Swift語言,而后推出提高可維護性的SwiftUI和Combine疗隶。

SwiftUI

對于一個基于UIKit的項目是沒有必要全部用SwiftUI重寫的佑笋,在UIKit里使用SwiftUI的視圖非常容易,UIHostingController是UIViewController的子類斑鼻,可以直接用在UIKit里蒋纬,因此直接將SwiftUI視圖加到UIHostingController中,就可以在UIKit里使用SwiftUI視圖了坚弱。

SwiftUI的布局核心是 GeometryReader蜀备、View Preferences和Anchor Preferences。如下圖所示:

SwiftUI的數(shù)據(jù)流更適合Redux結構荒叶,如下圖所示:

如上圖碾阁,Redux結構是真正的單向單數(shù)據(jù)源結構,易于分割些楣,能充分利用SwiftUI內(nèi)置的數(shù)據(jù)流Property Wrapper脂凶。UI組件干凈宪睹、體量小、可復用并且無業(yè)務邏輯蚕钦,因此開發(fā)時可以聚焦于UI代碼亭病。業(yè)務邏輯放在一起,所有業(yè)務邏輯和數(shù)據(jù)Model都在Reducer里嘶居。ACHNBrowserUIMovieSwiftUI 開源項目都是使用的Redux架構罪帖。最近比較矚目的TCA(The Composable Architecture)也是類Redux/Elm的架構的框架,項目地址見邮屁。

提到數(shù)據(jù)流就不得不說下蘋果公司新出的Combine整袁,對標的是RxSwift,由于是蘋果公司官方的庫樱报,所以應該優(yōu)先選擇葬项。不過和SwiftUI一樣,這兩個新庫對APP支持最低的系統(tǒng)版本都要求是iOS13及以上迹蛤。那么怎么能夠提前用上SwiftUI和Combine呢民珍?或者說現(xiàn)在使用什么庫可以以相同接口方式暫時替換它們,又能在以后改為SwiftUI和Combine時成本最小化呢盗飒?

對于SwiftUI嚷量,AcFun自研了聲明式UI Ysera,類似SwiftUI的接口逆趣,并且重構了AcFun里收藏模塊列表視圖和交互邏輯蝶溶,如下圖所示:

通過上圖可以看到,swift代碼量相比較OC減少了65%以上宣渗,原先使用Objective-C實現(xiàn)的相同功能代碼超過了1000行抖所,而Swift重寫只需要350行,對于AcFun的業(yè)務研發(fā)工程師而言痕囱,同樣的需求實現(xiàn)代碼比之前少了至少30%田轧,面對單周迭代這樣的節(jié)奏,團隊也變得更從容鞍恢。代碼可讀性增加了傻粘,后期功能迭代和維護更容易了,Swift讓AcFun駛入了iOS開發(fā)生態(tài)的“快車道”帮掉。

SwiftUI全部都是基于Swift的各大可提高開發(fā)效率特性完成的弦悉,比如前面提到的,能夠訪問只給語言特性級別行為的Property Wrapper蟆炊,通過Property Wrapper包裝代碼邏輯稽莉,來降低代碼復雜度,除了SwiftUI和Combine里@開頭的Property Wrapper外涩搓,Swift還自帶類似@dynamicMemberLookup@dynamicCallable 這樣重量級的Property Wrapper污秆。還有ResultBuilder_ _這種能夠簡化語法的特性后室,有些如GraphQL、REST和Networking實際使用ResultBuilder的范例可以參考混狠。這些Swift的特性如果也能得到充分利用,即使不用SwiftUI也能使開發(fā)效率得到大幅提升疾层。

網(wǎng)飛(Netflix)App已使用SwiftUI重構了登錄界面将饺,網(wǎng)飛增長團隊移動負責人故胤道長記錄了SwiftUI在網(wǎng)飛的落地過程,詳細描述了SwiftUI的收益痛黎。網(wǎng)飛能夠直接使用SwiftUI得益于他們最低支持iOS 13系統(tǒng)予弧。

不過如最低支持系統(tǒng)低于iOS 13,還有開源項目AltSwiftUI_ _也實現(xiàn)了SwiftUI的語法和特性湖饱,能夠向前兼容到iOS 11掖蛤。

對于Combine,也有開源實現(xiàn)OpenCombine井厌,目前都未完全實現(xiàn)所有特性蚓庭。因此,具體在工程中使用還是需要了解Combine的核心原理仅仆。

RxSwift

Combine的靈感來源于RxSwift蜘矢。RxSwift的核心这弧,這里有份實現(xiàn)了RxSwift核心邏輯的簡版樣例代碼,可以窺視其核心邏輯。整體流程如下圖:

如上圖所示蔑穴,RxSwift整體流程非常簡單,主要就是訂閱者和發(fā)布者之間進行訂閱酥诽、發(fā)布匙隔、取消操作,訂閱者會監(jiān)聽和處理這些事件涌韩。具體RxSwift數(shù)據(jù)傳遞關系如下圖:

上圖中的Observable是發(fā)布者畔柔,Observer是訂閱者。取消訂閱是通過CompositeDisposable來進行管理贸辈,管理方式就是加個中間訂閱者來決定是否發(fā)送事件給原訂閱者释树。SinkDisposable是一個中間層用來把中間訂閱者和原訂閱者還有事件轉(zhuǎn)發(fā)的邏輯放到一起。新增一個操作符就會新增一個SinkDisposable擎淤,比如新增filter操作符就會新增FilterObserver和FilterObservable奢啥,如果沒有操作符就是AnoymousObserver和AnoymousObservable。訂閱是通過Disposer類來管理的嘴拢,會判斷是否完成或者出錯桩盲,執(zhí)行Dispose方法。

Combine

Combine的思路基本和RxSwift一樣席吴,只是接口命名不同赌结,這里有份表格捞蛋,列出了Combine和RxSwift功能的對應關系,可以看出目前Combine相較于RxSwift還缺少很多能力柬姚,Combine畢竟新生兒拟杉,還需要時間成長。但是Combine有個特性是RxSwift沒有的量承,那就是Backpressure搬设,Backpressure可自定義策略控制Subscribe能夠接收的數(shù)量。

除了SwiftUI和Combine撕捍,在Swift開發(fā)中還有哪些庫是可以直接拿來使用的呢拿穴?這里有份 Swift開源庫的awesome,在這里可以查缺補漏忧风。AcFun主要使用了Swift開源庫有Protobuf,_ _RxSwift, Cache, Observable默色。

以上,為《A站 的 Swift 實踐》的上篇內(nèi)容狮腿,下篇我們會繼續(xù)詳細介紹OC和Swift是怎么混編的腿宰,以及Swift的動態(tài)性。

如何混編

昨天剛剛結束的Google I/O讓人想起了Kotlin在三年前曾經(jīng)上過一次熱搜蚤霞,Google I/O官宣Kotlin替代Java酗失,正式成為Android開發(fā)的首選語言。正所謂演進的力量昧绣,這一切都要歸功于蘋果公司在2014年推出的Swift替代了Objective-C规肴,成為iOS乃至蘋果全平臺首選的開發(fā)語言,從而提高了iOS開發(fā)者的熱情夜畴。上篇介紹了Swift的技術背景以及如何選擇開發(fā)框架拖刃。下篇的內(nèi)容會介紹大多數(shù)以OC為主體的工程如何與Swift共舞,以及如何利用Swift動態(tài)性解決工程難題贪绘。

如果你的工程是OC開發(fā)的兑牡,要用上Swift就需要進行OC和Swift的混編開發(fā)。

然而税灌,混編開發(fā)應該怎么開始呢均函?有沒有什么前置條件?

前置條件

混編本質(zhì)上就是把OC語法的聲明通過編譯工具生成Swift語法的聲明菱涤,這樣Swift就可以通過生成的聲明直接調(diào)用OC接口苞也。反之,OC調(diào)用Swift接口也可以通過相同的方法粘秆,把Swift語法的聲明生成OC語法的頭文件如迟。這些轉(zhuǎn)換生成的編譯工具都集成在開發(fā)工具Xcode里。

Xcode其實就是執(zhí)行多命令行的工具,比如Clang殷勘、ld等等此再。Xcode、Project文件里包含了這些命令的參數(shù)和它們執(zhí)行的順序玲销,也有所有待編譯文件和它們的依賴關系输拇。llbuild是低等級構建系統(tǒng),根據(jù)Xcode Project里的配置按順序執(zhí)行命令贤斜。命令行工具的參數(shù)配置是在Xcode的Build Settings里進行設置的淳附。如果是在同一個Project里混編,首先需要將Build Settings里Always Embed Swift Standard Libraries設置為YES蠢古,然后在橋接文件,也就是ProductName-Bridging-Header.h里導入需要暴露給Swift的OC類别凹。如果Swift要調(diào)用的OC在不同Project里草讶,則需要將OC的Project設置為Module,將Defines Module設為YES炉菲,再把Module里的頭文件導入到OC Modulemap文件里的Umbrella Header里堕战。

如何設置CocoaPods

Swift Pod的Podspec需要寫明對OC Pod的依賴。在工程Podfile中拍霜,OC Pod后面要寫 :modular_headers => true嘱丢。開啟Modular Header就是把Pod轉(zhuǎn)換為Module。那CocoaPods究竟做了什么祠饺?執(zhí)行 Pod Install -- Verbose就可以看到越驻,在生成Pod Targets時,CocoaPods會生成Module Map File和Umbrella Header道偷。

每個工程設置的情況千奇百怪缀旁,而CocoaPods主要是通過自己的dsl配置來完成這些編譯參數(shù)的設置,所以就需要先了解些混編設置的編譯參數(shù)和概念:

  • 前面提到的Defines Module勺鸦,需要設置為YES并巍。
  • Module Map File表示 Module Map的路徑。
  • Header Search Paths代表Module Map定義的OC頭文件路徑换途。
  • Product Module Name的默認設置和Target Name一樣懊渡。
  • Framework Search Paths是設置依賴Framework的搜索路徑。
  • Other C Flags可以用來配置依賴其它Module文件路徑军拟。
  • Other Swift Flags可以配置其Module Map文件路徑剃执。

CocoaPods的主要組件有解析命令的CLAide用來解析Pod描述文件吻谋,比如Podfile忠蝗、Podfile.lock和PodSpec文件的Cocoapods-core拉倉庫代碼和資源的Cocoapods-downloader漓拾、分析依賴的Molinillo阁最、以及創(chuàng)建和編輯Xcode的.xcodeproj和.xcworkspace文件的Xcodeproj戒祠。在執(zhí)行了Pod Install以后,組件調(diào)用流程以及配置Module所處流程位置速种,如下圖所示:

按照上圖的邏輯姜盈,Integrates這一步主要是用來配置Module的。先檢查Targets配阵,主要是對于包括Swift版本和Module依賴等問題的檢查馏颂,然后再使用Xcodeproj組件做Module的工程配置。

完成以上工作后棋傍,如果我們想要在Swift里使用OC開發(fā)的庫FMDB救拉,就可以直接使用Import來導入,代碼如下:

import UIKit
import FMDB

class SwiftTestClass: NSObject {
    var db:FMDB.FMDatabase?

    override init() {
        super.init()
        self.db = FMDB.FMDatabase(path: "dbname")
        print("init ok")

    }
}

可以看到瘫拣,Import FMDB將FMDB的Module倒入進來后亿絮,接口依然能夠直接使用Swift語法調(diào)用。

這里需要注意的是麸拄,Module依賴的Pod也需要是Module派昧。因此改造時需要從底向上地改造成Module。另外拢切,開啟Module后蒂萎,如果某個頭文件在Umbrella Header里,那么其它包含這個頭文件的Pod也需要打開Module淮椰。

為什么要用Module五慈?

在Module被使用之前,開發(fā)者們需要對要導入的C語言編譯器處理方式類頭文件進行預處理主穗,查找頭文件里還導入了哪些頭文件豺撑,遞歸直到找到全部頭文件。但是黔牵,預處理的方式會遇到許多問題聪轿。其一,編譯的復雜度高且耗時長猾浦,這是因為每個可編譯的文件都會單獨編譯進行預處理陆错,所以在預處理過程中遞歸查找導入頭文件的工作會重復很多次,尤其是當包含關系很深的頭文件被很多.m所導入的時候金赦;其二音瓷,會出現(xiàn)宏定義沖突時需要重新排序以及和解依賴的問題等。

Module相對來說更加簡易夹抗,它的頭文件只需要解析一次绳慎,所以編譯的復雜度會指數(shù)級降低,且編譯器對Module的處理方式和C語言的預處理方式是完全不同的。編譯器會將要編譯的文件導入的頭文件生成二進制格式杏愤,存儲在Module Cache中靡砌,編譯時如果碰到需要導入模塊時,會先檢查Module Cache珊楼,有對應的二進制文件就直接加載通殃,沒有才會解析,以此來保證Module解析只有一次厕宗。重新解析編譯Module只會發(fā)生在頭文件包含的任何頭文件有變動画舌,或者依賴另外一個模塊有更新的時候。比如下面的代碼:

#import <FMDB/FMDatabase.h>

Clang會先從FMDB.framework的Headers目錄里查找FMDatabase.h已慢,再去FMDB.framework的Modules目錄里查找module.modulemap文件曲聂,分析module.modulemap來判斷FMDatabase.h是否是模塊的一部分。Module Map用來定義Module和頭文件之間的關系佑惠。FMDB.framework的module.modulemap的內(nèi)容如下:

framework module FMDB {
  umbrella header "FMDB-umbrella.h"

  export *
  module * { export * }
}

想要確定FMDatabase.h是否是Module的一部分就要看module.modulemap里的Umbrella Header文件句葵,即FMDB-umbrella.h目錄里是否包含了FMDatabase.h。在Headers目錄里查看FMDB-umbrella.h文件兢仰,內(nèi)容如下:

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif

#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#import "FMDatabasePool.h"
#import "FMDatabaseQueue.h"
#import "FMDB.h"
#import "FMResultSet.h"

FOUNDATION_EXPORT double FMDBVersionNumber;
FOUNDATION_EXPORT const unsigned char FMDBVersionString[];

上面代碼中可以看到FMDatabase.h已經(jīng)包含在文件中,因此Clang會將FMDB作為Module導入剂碴。Umbrella框架是對框架的一個封裝把将,目的是隱藏各個框架之間的復雜依賴關系。構建完的Module會被存放到 ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/ 這個目錄下面忆矛。

Clang編譯單個OC文件是通過導入頭文件方式進行的察蹲,而Swift沒有頭文件,所以Swift編譯器Swiftc就需要先查找聲明催训,再來生成接口洽议。除此之外,Swiftc還會在Module Map文件和Umbrella Header文件中暴露的聲明里查找OC聲明漫拭。

如果工程要構建二進制庫亚兄,需要支持Swift 5.1加的Module Stability和Library Evolution。

Name Mangling

找到OC聲明后采驻,Swiftc就需要進行Name Mangling审胚。Name Mangling的作用在一方面是會像C++那樣防止命名沖突,另外一方面是會對OC接口命名進行Swift風格化重命名礼旅。如果對Name Mangling命名的效果不滿意膳叨,還可以回到OC源碼中用NS_SWIFT_NAME重新定義想要在Swift使用的名字。

Swiftc的Name Mangling相比較于C和C++的Name Mangling會生成更多信息痘系,比如下面的代碼:

public func int2string(number: Int) -> String {
    return "\(number)"
}

Swiftc編譯后菲嘴,使用nm -g查看生成如下的信息:

0000000000003de0 T _$s8demotest10int2string6numberSSSi_tF

如上所示,信息中的$s表示全局,8demotest的demotest是Module名龄坪,8是Module名的長度昭雌。int2string是函數(shù)名,前面的10是類名長度悉默,6number是參數(shù)名城豁。SS表示參數(shù)類型是Int。Si表示的是String類型抄课,_tF表示前面的Si是返回類型唱星。

接下來對比一下Clang和Swiftc的編譯過程,首先是Clang的編譯過程跟磨,如下圖:

其次是Swift的編譯過程间聊,如下圖:

從兩者的對比中可以看出,Swift編譯過程缺少了頭文件抵拘,因為它通過分組編譯模糊了文件的概念哎榴,減少了很多重復查找聲明的工作,這樣不僅僅可以簡化代碼的編寫僵蛛,還可以給編譯器更多的發(fā)揮空間尚蝌。

至于OC怎樣調(diào)用Swift接口,Swiftc會生成一個頭文件充尉,代碼中有Public的聲明會先按文件生成Swiftmodule飘言,文件鏈接完會合并Swiftmodule,最后整體生成到一個頭文件里驼侠。過程如下圖所示:

為什么可以調(diào)OC接口姿鸿?

Swift代碼之所以可以調(diào)OC接口,是因為OC的接口會被編譯器自動生成為Swift語法接口文件倒源。在Xcode中苛预,在OC頭文件中點擊左上角的 Related Items,選擇Generated Interface笋熬,就可以選擇查看生成的Swift版本接口文件热某。自動轉(zhuǎn)換成的Swift接口文件可以直接供Swift調(diào)用,在轉(zhuǎn)換過程中胳螟,編譯器會將NSString這種OC的基礎庫轉(zhuǎn)換成Swift里對應的String苫拍、Date等Swift庫。OC的初始化方法也會被轉(zhuǎn)換成Swift的構造器方法旺隙。錯誤處理也會被轉(zhuǎn)換成Swift風格绒极。下面是OC和Swift轉(zhuǎn)換對應的類型:

但是,僅僅只依賴于編譯器的轉(zhuǎn)換肯定是不夠的蔬捷,為了能讓Swift調(diào)用得更加舒服垄提,還需要對OC接口做些修改適配榔袋,比如將函數(shù)改成使用OC泛型,NSArray paths轉(zhuǎn)成Swift是open var paths:[Any]铡俐;如果使用了泛型凰兑,將其改成 NSArray paths,那對應的Swift就是open var paths:[KSPath]审丘,這種接口Swift使用起來會更方便有效吏够。

蘋果公司也提供了一些宏來幫助生成好用的Swift接口。

眾所周知滩报,OC之前一直缺少是非空的類型信息锅知,可以通過 NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END包起來,這樣就不用逐個去指定是非空了脓钾。NS_DESIGNATED_INITIALIZER宏可以將初始化設置為Designated售睹,不加這個宏為Convenience。NS_SWIFT_NAME用來重命名Swift中使用的名稱可训,NS_REFINED_FOR_SWIFT可以解決數(shù)據(jù)不一致的問題昌妹。

在iOS開發(fā)的過程中不可避免地需要訪問 Core Foundation 類型,Core Fundation框架一旦導入到Swift混編環(huán)境中握截,它的類型就會被自動轉(zhuǎn)為Swift類飞崖,Swift也會自動管理Annotated Core Foundation對象的內(nèi)存,而不用像在OC中那樣手動調(diào)用CFRetain谨胞、CFRelease或者CFAutorelease函數(shù)固歪。Unannotated的對象會被包裝在一個Unmanaged結構里,比如下面的代碼:

CFStringRef showTwoString(CFStringRef s1, CFStringRef s2)

轉(zhuǎn)成Swift就是:

func showTwoString(_: CFString!, _: CFString!) -> Unmanaged<CFString>! {
    // ...
}

如上面代碼所示畜眨,Core Fundation 類型的名字轉(zhuǎn)換后會去掉后綴Ref叶组,這是因為在Swift中所有類都是引用類型圈暗,Ref后綴比較多余。上面的Unmanaged結構有兩個方法北滥,一個是takeUnretainedValue()胞四,另一個是takeRetainedValue()恬汁,這兩個方法都是用來返回對象的原始未封裝類型。如果對象之前沒有Retain就用takeUnretainedValue()辜伟,已經(jīng)Retain了氓侧,就用takeRetainedValue()。

在Swift里用getVaList(:::) 或withVaList(::) 函數(shù)調(diào)用C的Variadic函數(shù)导狡,比如 vasprintf(:::)约巷。

調(diào)用指針參數(shù)的C函數(shù),和Swift映射如下圖:

Swift也有無法調(diào)用的C接口旱捧,比如復雜的宏独郎、C風格的Variadic參數(shù)踩麦,復雜的Array成員等。簡單賦值的宏會被轉(zhuǎn)換成Swift里的常量賦值氓癌,對于復雜的宏定義谓谦,編譯器無法自動轉(zhuǎn)換,如果還是想享受宏帶來的好處贪婉,比如可以避免重復輸入大量模板代碼和避免類型檢查約束反粥,可以通過函數(shù)和泛型替換獲取同樣的好處。

Swift寫出來的Module也可以給OC來調(diào)用疲迂。但是這樣的調(diào)用會有很多限制才顿,因為Swift中有很多類型是沒法給OC用的,比如在Swift里定義的枚舉鬼譬、Swift定義的結構體娜膘、頂層定義的函數(shù)、全局變量优质、Typealiases竣贪、Nested類型,但是如果繞過這些類型巩螃,Swift也變得不那么Swift了演怎。

即使是實現(xiàn)了混編,開發(fā)者們還需要面對許多難題避乏。因為在OC時代的很多問題爷耀,例如Hook,無痕埋點等可以在OC運行時很方便地實現(xiàn)拍皮,而Swift卻缺少天然的支持歹叮。下面介紹一下Swift的動態(tài)性,在官方完善前铆帽,我們應該怎么使用它咆耿。

動態(tài)性

Swift在處理純粹的Swift語言時是有自己的運行時的,但是對于“這個運行時是不提供訪問的接口”的問題爹橱,Swift核心團隊不是不做動態(tài)特性萨螺,而是因為如果想要支持動態(tài)特性就需要處理虛函數(shù)表(Virtual Method Table)的動態(tài)調(diào)用對SIL函數(shù)優(yōu)化的影響,比如類沒有被Override就會自動優(yōu)化到靜態(tài)調(diào)用愧驱,而這需要大量的時間∥考迹現(xiàn)階段還有優(yōu)先級更高的事情要做,比如并發(fā)模型组砚、系統(tǒng)編程吻商、靜態(tài)分析支持類型狀態(tài)等。因此糟红,有人選擇自己去實現(xiàn)一套Swift運行時手报,使得Swift代碼具有動態(tài)特性蚯舱。Jordan Rose實現(xiàn)了一個精簡版的Swift運行時,更加嚴謹?shù)倪\行時實現(xiàn)可以參考EchoRuntime掩蛤。

有人可能會問枉昏,SwiftUI的Preview不就是典型的在運行時替換方法的嗎?他是怎么做到的呢揍鸟?其實他使用的是@_dynamicReplacement屬性兄裂,這是一個可以直接拿著用來進行方法替換的內(nèi)部使用屬性。

@_dynamicReplacement(for: runSomething())
static func _replaceRunSomething() -> String {
    "replaced"
}

如果想要把上面的代碼放到一個庫中阳藻,并且在運行時加載這個庫進行運行時方法替換可以通過這樣的方式:

runSomething()

let file = URL(fileURLWithPath: "/path/of/replaceLib.dylib")

guard let handle = dlopen(file.path, RTLD_NOW) else {
    fatalError("oops dlopen failed")
}

runSomething()

除了這個方法以外晰奖,還有其他辦法可以進行運行時的方法替換嗎?

值類型的方法替換

通過 AnyClass和class_getSuperclass方法可以查看Swift對象的繼承鏈腥泥,沒有繼承NSObject的Swift類匾南,會有一個隱含的Super Class,這個類會帶有一個生成的帶前綴的SwiftObject蛔外,比如_TtCs12_SwiftObject蛆楞。Swift是實現(xiàn)了NSObject的一個objc運行時的類型,這個類型不能和OC交互夹厌。但是如果繼承了NSObject就可以和OC交互豹爹。

如果方法或?qū)傩月暶髁?@objc dynamic,那么就可以在運行時通過動態(tài)派發(fā)在Swift對象上去調(diào)用矛纹,方法是:使用AnyObject的Perform方法去執(zhí)行NSSelectorFromString里傳入的方法或?qū)傩悦?/p>

對于Swift里的值類型臂聋,比如Struct、Enum或南、Array等孩等,可以遵循_ObjectiveCBridgeable協(xié)議,經(jīng)過Type Casting(顯示或隱式)轉(zhuǎn)成對應的OC對象類型采够。舉個例子肄方,如果想要查看Array的類繼承關系,代碼如下:

func classes(of cls: AnyClass) -> [AnyClass] {
    var clses:[AnyClass] = []
    var cls: AnyClass? = cls
    while let _cls = cls {
        clses.append(_cls)
        cls = class_getSuperclass(_cls)
    }
    return clses
}
let arrays = ["jone", "rose", "park"]
print(classes(of: object_getClass(arrays)!))
// [Swift.__SwiftDeferredNSArray, Swift.__SwiftNativeNSArrayWithContiguousStorage, Swift.__SwiftNativeNSArray, __SwiftNativeNSArrayBase, NSArray, NSObject]

如上面代碼所示吁恍,Swift的Array最終都是繼承自NSObject扒秸,其它值類型也類似播演〖酵撸可以看出,所有Swift類型都是可兼容objc運行時的写烤。因此可以給這些值類型添加objc運行時方法翼闽,代碼如下:

// MARK: 為Swift類型提供動態(tài)派發(fā)的能力
struct structWithDynamic {
    public var str: String
    public func show(_ str: String) -> String {
        print("Say \(str)")
        return str
    }
    internal func showDynamic(_ obj: AnyObject, str: String) -> String {
        return show(str)
    }
}
let structValue = structWithDynamic(str: "Hi!")
// 為 structValue 添加Objc運行時方法
let block: @convention(block)(AnyObject, String) -> String = structValue.showDynamic
let imp = imp_implementationWithBlock(unsafeBitCast(block, to: AnyObject.self))
let dycls: AnyClass = object_getClass(structValue)!
class_addMethod(dycls, NSSelectorFromString("objcShow:"), imp, "@24@0:8@16")
// 使用Objc動態(tài)派發(fā)
_ = (structValue as AnyObject).perform(NSSelectorFromString("objcShow:"), with: String("Bye!"))!

如上面代碼所示,取出函數(shù)閉包可以通過 @convertion(block)轉(zhuǎn)換成C函數(shù)Call Convention來調(diào)用洲炊,C函數(shù)也可以直接去執(zhí)行這個指針感局。使用 Memory Dump 工具可以查看Swift函數(shù)內(nèi)存結構尼啡,以及解析出符號信息DL_Info。Memory Dump工具有Mikeash的memorydumper2询微,源碼解讀可以參考Swift Memory Dumping崖瞭。逆向查看內(nèi)存布局可以參考《初探Swift Runtime:使用Frida實現(xiàn)針對Alamofire的抓包工具》
__

類的方法替換

在運行時進行類方法的替換時,先將方法的Block以AnyObject類型傳入imp_implementationWithBlock方法撑毛,返回一個imp书聚,然后使用 class_getInstanceMethod 來獲取實例的原方法,再通過 class_replaceMethod 進行方法替換藻雌,完整代碼可以參看InterposeKit雌续,另外還有一個使用libffi的方法替換庫,參見SwiftHook胯杭。

另外驯杜,通過獲取函數(shù)地址來改變函數(shù)指向位置的方法在Swift里實現(xiàn)比較困難,這是因為NSInvocation不可用了做个,因此需要通過C的函數(shù)來Hook Swift鸽心。在Swift的AnyClass中有類似OC的布局,記錄了指向類和類成員函數(shù)的數(shù)據(jù)叁温,這樣就可以使用匯編來做函數(shù)指針替換的事情再悼。思路是:保存寄存器,調(diào)用新函數(shù)膝但,然后恢復寄存器冲九,還原函數(shù)。具體可以參考項目SwiftTrace跟束。

插樁

使用編譯插樁的方式也可以實現(xiàn)運行中的方法替換莺奸,關鍵步驟在于編譯時,需要使用DYLD_INSERT_LIBRARIES進行攔截冀宴,CommandLine.arguments可以得到Swiftc的執(zhí)行參數(shù)灭贷,以查找待編譯的Swift文件。通過蘋果公司的SwiftSyntax源代碼解析略贮、生成和轉(zhuǎn)換的工具可以查出所有方法甚疟,并插入特定的方法替換邏輯代碼。修改完通過-output-file-map來獲取mach-o的地址去覆蓋先前產(chǎn)物逃延。使用self.originalImplementation(...)調(diào)用原始的實現(xiàn)作為閉包傳入execute(arguments:originalImpl:)方法览妖。

ClassContextDescriptorBuilder

Swift運行時給每個類型保留了Metadata信息。Metadata是由編譯器靜態(tài)生成的揽祥,有了Metadata的調(diào)試才能夠發(fā)現(xiàn)類型的信息讽膏。Metadata偏移-1是Witness table 指針,Witness Table 提供分配拄丰、復制和銷毀類型的值府树,Witness Table 還記錄了類型大小俐末、對齊、Stride等其它屬性奄侠。Metadata偏移量0的地方是Kind字段卓箫,其描述了Metadata所描述的類型的種類,例如Class垄潮、Struct丽柿、Enum、Optional魂挂、Opaque甫题、Tuple、Function涂召、Protocol等類型坠非。這些類型的Metadata具體詳述可見Type Metadata 的官方文檔,代碼描述可以在include/swift/ABI/MetadataValues.h里看到果正。比如在Metadata里類的方法數(shù)量會比實際代碼里寫的方法數(shù)量要多炎码,那是因為編譯器會自動生成一些方法,這些方法的種類在MethodDescriptorFlags類中Kind里描述了秋泳,代碼如下:

enum class Kind {
    Method,
    Init,
    Getter,
    Setter,
    ModifyCoroutine,
    ReadCoroutine,
};

可以看到潦闲,Getter、Setter以及線程相關讀寫的ModifyCoroutine迫皱、ReadCoroutine類型都是自動生成的歉闰。

Class的內(nèi)存結構生成方法可以在/lib/IRGen/GenMeta.cpp里找到:

  • ClassContextDescriptorBuilder這個類是用來生成Class內(nèi)存結構的,它繼承于TypeContextDescriptorBuilderBase卓起。
  • Enum和敬、Struct等類型的內(nèi)存結構Builder基類都是繼承于ContextDescriptorBuilderBase的TypeContextDescriptorBuilderBase。
  • ContextDescriptorBuilderBase 是最基礎的基類戏阅,Module昼弟、Extension、Anonymous奕筐、Protocol舱痘、Opaque Type、Generic都是繼承于它离赫。
  • Struct的Metadata和Enum的Metadata共享內(nèi)存布局芭逝,Struct會多個指向Type Context Descriptor的指針。

內(nèi)存布局指的是使用一個Struct或者Tuple笆怠,根據(jù)每個字段的大小和對齊方式?jīng)Q定怎樣來安排內(nèi)存中的字段铝耻,在這個過程中誊爹,不僅需要描述清楚每個字段的偏移量蹬刷,還有Struct或Tuple整體的大小和對齊方式瓢捉。下面就是GenMeta里和Class類型相關的內(nèi)存方法代碼:

// 最底層基類 ContextDescriptorBuilderBase的布局方法
void layout() {
  asImpl().addFlags();
  asImpl().addParent();
}

// TypeContextDescriptorBuilderBase的布局方法
void layout() {
  asImpl().computeIdentity();

  super::layout();
  asImpl().addName();
  asImpl().addAccessFunction();
  asImpl().addReflectionFieldDescriptor();
  asImpl().addLayoutInfo();
  asImpl().addGenericSignature();
  asImpl().maybeAddResilientSuperclass();
  asImpl().maybeAddMetadataInitialization();
}

// ClassContextDescriptorBuilder的布局方法
void layout() {
  super::layout();
  addVTable();
  addOverrideTable();
  addObjCResilientClassStubInfo();
  maybeAddCanonicalMetadataPrespecializations();
}

根據(jù)GenMeta可以看到Swift的Class類型內(nèi)存布局是根據(jù)ContextDescriptorBuilderBase、TypeContextDescriptorBuilderBase再到ClassContextDescriptorBuilder繼承層層疊加的办成,因此對應Class類型的Nominal Type Descriptor就可以用如下C結構來描述:

struct SwiftClassInfo {
    uint32_t flag;
    uint32_t parent;
    int32_t name;
    int32_t accessFunction;
    int32_t reflectionFieldDescriptor;
    ...
    uint32_t vtable;
    uint32_t overrideTable;
    ...
};

代碼中可見泡态,add的前綴就是增加的偏移記錄,addFlags后面的addParent就是下一個偏移的記錄迂卢。FieldDescriptor換成ReflectionFieldDescriptor是蘋果公司在5.0版本對Metadata做的改變某弦,官方Mirror反射目前還不完善,有些信息還沒法提供而克,因此在Metadata里增加了一些反射相關信息靶壮。

OC動態(tài)調(diào)用方法會把_cmd作為第一個參數(shù),第二個參數(shù)是Self员萍,后面是可變參數(shù)列表腾降,動態(tài)調(diào)度可以在運行時添加類、變量和方法碎绎。而在Swift中動態(tài)調(diào)用方法是基于VTable的螃壤,運行時沒法對方法進行動態(tài)搜索,地址在編譯時靜態(tài)寫在了VTable里筋帖,運行時不能改奸晴,可以用靜態(tài)地址調(diào)用,或dlsym來搜索名稱日麸。

VTable的地址在TypeContextDescriptor之后寄啼,OverrideTable存儲位置在VTable之后,有三個字段來描述代箭,第一個是記錄哪個類被重寫辕录,第二個是被重寫的函數(shù),第三個是用來重寫的函數(shù)相對的地址梢卸。因此通過OverrideTable就可以找到重寫前和重寫后函數(shù)指針走诞,這樣就有機會在VTable里找到對應函數(shù)進行函數(shù)指針的替換,達到Hook的效果蛤高。要注意蚣旱,在Swift編譯器設置優(yōu)化時VTable的函數(shù)地址可能會清空或使用直接地址調(diào)用,這兩種情況發(fā)生的話就沒法通過VTable進行方法替換戴陡。

那么還有其它思路嗎塞绿?

Mach_override

使用Wolf Rentzsch寫的Mach_override也是一種方法,可以在原始函數(shù)的匯編里加個jmp恤批,跳到自定義函數(shù)异吻,然后再跳回原始函數(shù)。Mach_override_ptr的三個參數(shù)分別是,一诀浪,要覆蓋函數(shù)的指針棋返;二,去覆蓋函數(shù)的指針雷猪;三睛竣,參數(shù)可以設置為原函數(shù)的指針地址,待Mach_override_ptr返回成功求摇,就可以調(diào)原函數(shù)射沟。Mach_override會分配一個虛擬內(nèi)存頁,使其可寫可執(zhí)行与境。需要注意的是验夯,Mach_override_ptr初始函數(shù)和重入函數(shù)指針相同,調(diào)用后摔刁,重入函數(shù)將調(diào)用替換函數(shù)而不是原始函數(shù)簿姨。在Swift中如何使用Mach_override可參考SwiftOverride

總結

通過上下篇的介紹簸搞,想必你已經(jīng)了解到A站為擁抱Swift都做了哪些事情扁位。基于A站以及快手主站的一些架構師對于Swift的熱愛趁俊,以及為之付于的實踐域仇,A站的開發(fā)體驗才得以蛻變。

為了讓OC開發(fā)同學能夠掌握Swift寺擂,以更“Swift”的方式進行開發(fā)暇务,A站組織了十多次Swift組內(nèi)的培訓和分享,并規(guī)范了Swift代碼風格和靜態(tài)檢查流程怔软。針對開發(fā)體驗上的痛點垦细,A站在2020年上半年就開始了混編工程的優(yōu)化、組件化以及二進制化的建設挡逼。完成了分層設計括改,漸進式地將模塊解耦下沉到對應的分層,進而可以借助LLVM Module來抹平模塊API在語言上的差異家坎,從而代替Swift和Objective-C在主工程的橋接嘱能,為10+ A站和中臺的基礎庫進行了Module化問題修復,并基于主站的二進制化方案 (GUNDAM)完善了對Swift以及混編的支持虱疏。從Swift ABI Stability進化為Module Stability的XCFramework惹骂,WWDC的Session很好的說明XCFramework的原理,同時表示XCFramework格式對Objective-C/C/C++也有很好的支持做瞪。目前組件的二進制化率約為80%对粪,約有50%的組件已經(jīng)完成了LLVM Module化,構建時間提升了60%以上。隨著Swift優(yōu)勢的逐漸體現(xiàn)以及團隊Swift能力建設的推進著拭,A站更多的工程師開始傾向于使用Swift進行業(yè)務開發(fā)纱扭,而Swift帶來的“加速度”,也讓技術團隊切實地感受到了強烈的“推背感“茫死。

當然,A站也曾遇到一些Swift的Bug履羞,比如打包RxSwift5后遇到模塊名和類名一樣所產(chǎn)生的Bug和Issue峦萎,RxSwift6通過避免使用Typealias的類型曲線形地解決了這個問題,目前此問題已被官方標記為“解決”忆首,后面的版本可以正常使用爱榔。另外還有兩個未解決的問題,一個是在Module的接口中出現(xiàn)Ambiguous Type Name Error問題糙及,參考Issue详幽;另一個是Import后產(chǎn)生.swiftinterface出現(xiàn)的錯誤,參見網(wǎng)站Issue浸锨。

最后想說的是唇聘,Swift開發(fā)并不容易,不要被Swift簡潔的語法所迷惑柱搜,各種大小括號組合會讓開發(fā)者們感到困惑,還有一些特性會讓直觀理解變得很困難,比如下面的代碼:

let str:String! = "Hi"
let strCopy = str

根據(jù)Swift類型推導的特性初斑,按道理str類型加上感嘆符號后疯搅,strCopy就會被自動推導為非可選String類型。但實際情況是健爬,按照官方文檔的說法控乾,strCopy沒有直接指明類型,即隱式可選值時娜遵,str類型是String后加上感嘆號蜕衡,這種是屬于隱含解包可選值String無法推導出非可選String類型,因此Swift會先將strCopy作為一個普通可選值來用设拟,這樣和直觀的感覺非常不一樣衷咽。

本以為5.0的ABI在穩(wěn)定后,Swift學起來會更容易蒜绽,但是其實新的SwiftUI和Combine這樣重量級的框架需要開發(fā)者繼續(xù)鉆研镶骗,真是“Write Swift, Learn Every Year”。Swift不斷從其它語言中吸取精髓鼎姊,接下來的async/await慰于,你準備好了嗎休里?要用上拭嫁,先得看咱家APP系統(tǒng)最低版本是不是能夠支持這些新特性驮宴。

雖說不容易,但為了穩(wěn)定和效率,終究跟上了時代的步伐阳距。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子舀锨,更是在濱河造成了極大的恐慌屎暇,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異业舍,居然都是意外死亡把介,警方通過查閱死者的電腦和手機拗踢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門蓄髓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人译红,你說我怎么就攤上這事耻陕。” “怎么了刨沦?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵诗宣,是天一觀的道長。 經(jīng)常有香客問我已卷,道長梧田,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任侧蘸,我火速辦了婚禮裁眯,結果婚禮上,老公的妹妹穿的比我還像新娘讳癌。我一直安慰自己穿稳,他們只是感情好,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布晌坤。 她就那樣靜靜地躺著逢艘,像睡著了一般旦袋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上它改,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天疤孕,我揣著相機與錄音,去河邊找鬼央拖。 笑死祭阀,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的鲜戒。 我是一名探鬼主播专控,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼遏餐!你這毒婦竟也來了伦腐?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤失都,失蹤者是張志新(化名)和其女友劉穎柏蘑,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗅剖,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡辩越,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年嘁扼,在試婚紗的時候發(fā)現(xiàn)自己被綠了信粮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡趁啸,死狀恐怖强缘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情不傅,我是刑警寧澤旅掂,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站访娶,受9級特大地震影響商虐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜崖疤,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一秘车、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧劫哼,春花似錦叮趴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伤溉。三九已至,卻和暖如春妻率,著一層夾襖步出監(jiān)牢的瞬間乱顾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工宫静, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糯耍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓囊嘉,卻偏偏與公主長得像温技,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子扭粱,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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