滴滴出行客戶端 App 架構團隊在對 React Native、Weex 進行調研嘗試后發(fā)現(xiàn)并不適用于滴滴現(xiàn)有業(yè)務,由此自研了 iOS 動態(tài)化方案——DynamicCocoa,在這篇文章中,作者詳細分享了它的背景以及具體功能實現(xiàn)。
動態(tài)化一直是 App 開發(fā)夢寐以求的能力,而在 iOS 環(huán)境下,Apple 禁止了在 Main Bundle 外加載和執(zhí)行的自己的動態(tài)庫,所以像 Android 一樣下發(fā)原生代碼的方案被堵死。
后來像 React Native、Weex 這樣的基于 Web 標準的跨端方案出現(xiàn),各大公司都有對其進行嘗試,但對于滴滴現(xiàn)狀,也許并不適合:
所以我們思考,能不能做一套保持 iOS 原生技術棧、不重寫代碼就神奇的擁有動態(tài)化能力的方案呢?
于是,我們設計和實現(xiàn)了一個具有里程碑意義的 iOS 專屬動態(tài)化方案:DynamicCocoa
DynamicCocoa 可以讓現(xiàn)有的 Objective-C 代碼轉換生成中間代碼(JS),下發(fā)后動態(tài)執(zhí)行,相比其他動態(tài)化方案,優(yōu)勢在于:
不論是動態(tài)化還是 HotPatch,我們都能讓開發(fā)者“Write Cocoa, Run Dynamically”。
DynamicCocoa 能支持絕大部分日常使用的 Objective-C / C 語法,挑幾個特殊的:
interface、category、class extension、method、property,重要的是支持完備的 ivar定義,保持和 native 完全一致的實例內存結構;
strong、weak、unsafe_unretained 等對象的引用計數(shù),對象的 ivar 也可以正確的釋放;
@selector、@protocol、@encode、for..in 等;
舉個例子,你可以放心的使用下面的寫法,并能被正確的動態(tài)執(zhí)行:
一個功能模塊,除了代碼外,資源也是必不可少的,DynamicCocoa 的動態(tài) bundle 支持:
對于習慣于使用 IB 來開發(fā) UI 的人來說,這將是一個很好的開發(fā)體驗。
我們使用 Ruby 開發(fā)了一套命令行工具( 類比為 xcodebuild ),大幅簡化了配置開發(fā)環(huán)境、OC 代碼轉換、資源處理、打包的復雜度,它可以:
對于開發(fā)者來說,就像 pod 命令一樣,所有操作都可以通過這個命令完成。
首先 App 中需要集成 DynamicCocoa Engine SDK,用來執(zhí)行下發(fā)的 bundle 開發(fā)到發(fā)布的流程如下圖所示:
當然,DynamicCocoa 只提供命令行工具和 Engine SDK,可以完成本地打包、運行和測試,而線上發(fā)布后臺、服務端、CDN 等需要自行解決。
在滴滴內部,我們構建了開發(fā)、Review、線上回歸測試、灰度、發(fā)布、回滾、統(tǒng)計的閉環(huán)系統(tǒng),以服務的形式給內部接入。
HotPatch 本質上是方法粒度上的動態(tài)化,所以在整個框架搭建起來后,HotPatch 也不難實現(xiàn),使用 DynamicCocoa 做熱修復的大優(yōu)勢是開發(fā)者依然只對源碼負責,修改完 bug 后,打個 patch 包,修復成功后把源碼改動直接 push 到代碼倉庫就行了。
假設我們發(fā)現(xiàn)了下面的 bug:
然后在 Native 進行修復并自測:
自測完成后,在這個方法后面添加一個神奇的 Annotation:
使用命令行工具在 patch 模式下進行打包,就能把所有標記了的 method 提取出來,分別轉換成 JS 表示,打到一起進行發(fā)布。
除了修改一個方法外,patch 模式還支持:
后,開發(fā)者可以安心的把修改后的代碼(甚至可以保留 Annotation)git push,完成熱修復工作。
就像 Objective-C 是由 Clang 編譯器和 Objective-C Runtime 共同實現(xiàn)一樣,DynamicCocoa 也是由對應的兩部分構成:
我們知道,Clang-LLVM 的標準編譯流程是從源代碼經過預處理、詞法解析、語法解析生成語法樹,CodeGen 生成 LLVM-IR,進入編譯器后端進行優(yōu)化和匯編,終生成目標文件 (Mach-O)。
而我們既希望 Clang 幫助完成源碼處理的步驟,又希望生成結果是 JS 表示形式,于是在 Clang 生成抽象語法樹(AST)后,我們進行接管,實現(xiàn)了一個 OC2JS CodeGen,遍歷各個特定語法節(jié)點輸出 JS 表示:
由于轉換器和 Clang 前端標準編譯流程相同,所以只要 native 代碼能 build,轉換器就能 build,這也是 DynamicCocoa 能讓動態(tài)包和 native 保持嚴格一致的先決條件。
注:轉換器是基于 Clang 開發(fā)的獨立命令行工具,它的使用并不會對原有的 Xcode 工程產生任何影響。
另一部分是要集成進 App 的 DynamicCocoa SDK,它的職責是為 JS 中間代碼提供 Runtime 環(huán)境,實現(xiàn) OC-JS 的互調引擎,能夠加載動態(tài) bundle,提供便捷的 API,整體架構如下:
其中一些有趣的點:
int a = 1 / 2; 在 OC 中表示整除,結果為 0,但進入 JS 就都會按照 double 計算,結果為 0.5,造成了不一致。所以 DynamicCocoa 接管了 JS 中的類型信息,強轉或運算符都需要特殊處理。
DynamicCocoa 動態(tài)化技術給 App 開發(fā)帶來了很大的想象空間:
相比跨端方案,也帶來了一個新思路:iOS 和 Android 都保留 Native 開發(fā)模式,用各自的方式將 Native 代碼直接動態(tài)化,保持各平臺的差異性。
兩者思路上都是實現(xiàn) JS 和 OC 的互調:DynamicCocoa 的重點是動態(tài)化能力,優(yōu)勢在于完全不用寫 JS 和更多的語法特性支持;對于 HotPatch 來說 JSPatch 是更加小巧、輕量的解決方案。
有,在滴滴 App 已經上線并使用了好幾個版本,如滴滴小巴、專車接送機都有過 10k 級別的動態(tài)化模塊上線。
動態(tài) JS 代碼的運行要經過頻繁的 JSCore 和 OC 間的切換,性能相比 Native 必定會有損耗,但經過優(yōu)化,現(xiàn)在已經達到了無感知的程度:在我們的實際使用中,若不在頁面上添加特定標志,開發(fā)者和 QA 都無法分辨出當前頁面運行的是 native 還是動態(tài)包… 后續(xù)會有詳細的性能分析和大家分享。
與資源大小和 Native 源碼量有很大關系,不考慮資源的情況下,量級大概在 10000 行代碼 100KB 的動態(tài)包。
現(xiàn)在簡單的支持 GCD 來處理多線程,可以使用
dispatch_async將一個block放到另一個queue中執(zhí)行。
動態(tài) JS 代碼運行在 JSCore 中,并沒有直接獲取調用棧的方式,我們提供了 stack trace 功能,將近調用棧中每個 JS 到 OC / C 的互調都記錄下來,在發(fā)生 Crash 時便可以取出來作為附加信息隨 Crash 日志上報給統(tǒng)計平臺,方便問題的定位。
市面上很多動態(tài)化、HotPatch 方案都基于 JS 的下發(fā),運行在原生 JSCore 上,相信只要不在審核期間下發(fā)動態(tài)功能,Apple 是不太會拒絕的。
相比 OC,Swift 的動態(tài)化和 HotPatch 更加有難度,但我們已經有了可行的方案,是可以做到的,只是對于當前滴滴的現(xiàn)狀(絕大多數(shù)都在用 OC 開發(fā)),緊急程度并不高,后面再考慮支持。
有,我們正在積極的準備相關事項,于 2017 年初考慮開源。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網(wǎng)站上部分文章為轉載,并不用于任何商業(yè)目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯(lián)系我們,我們將根據(jù)著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。