滴滴 iOS 動態化方案 DynamicCocoa 的誕生與起航
滴滴出行客戶端 App 架構團隊在對 React Native、Weex 進行調研嘗試后發現并不適用于滴滴現有業務,由此自研了 iOS 動態化方案——DynamicCocoa,在這篇文章中,作者詳細分享了它的背景以及具體功能實現。
方案誕生
動態化一直是 App 開發夢寐以求的能力,而在 iOS 環境下,Apple 禁止了在 Main Bundle 外加載和執行的自己的動態庫,所以像 Android 一樣下發原生代碼的方案被堵死。
后來像 React Native、Weex 這樣的基于 Web 標準的跨端方案出現,各大公司都有對其進行嘗試,但對于滴滴現狀,也許并不適合:
滴滴 App 強交互、以地圖為主體、端特異性高;
客戶端人員充足,跨技術棧學習和開發有較大成本;
大量固化 Native 代碼,重寫成本高。
所以我們思考,能不能做一套保持 iOS 原生技術棧、不重寫代碼就神奇的擁有動態化能力的方案呢?
于是,我們設計和實現了一個具有里程碑意義的 iOS 專屬動態化方案:DynamicCocoa
DynamicCocoa 初識
DynamicCocoa 可以讓現有的 Objective-C 代碼轉換生成中間代碼(JS),下發后動態執行,相比其他動態化方案,優勢在于:
使用原生技術棧:使用者完全不用接觸到 JS 或任何中間代碼,保持原生的 Objective-C 開發、調試方式不變;
無需重寫已有代碼:已有 native 模塊能很方便的變成動態化插件;
語法支持完備性高:支持絕大多數日常開發中用到的語法,不用擔心這不支持那不支持;
支持 HotPatch:改完 bug 后直接從源碼打出 patch,一站式解決動態化和熱修復需求。
不論是動態化還是 HotPatch,我們都能讓開發者“Write Cocoa, Run Dynamically”。
語法支持
DynamicCocoa 能支持絕大部分日常使用的 Objective-C / C 語法,挑幾個特殊的:
完整的 Class 定義:interface、category、class extension、method、property,最重要的是支持完備的 ivar定義,保持和 native 完全一致的實例內存結構;
ARC:可以正確處理 strong、weak、unsafe_unretained 等對象的引用計數,對象的 ivar 也可以正確的釋放;
C 函數:支持 C 函數的定義與 C 函數的調用、內聯函數的調用;
可變參數:支持 C 與 OC 的可變參數方法的調用,如 NSLog;
struct:支持任意結構體的使用,無需額外處理;
block:支持創建和調用任意參數類型的 block;
其他 OC 特性:如 @selector、@protocol、@encode、for..in 等;
其他 C 特性:支持使用宏、static 變量、全局變量,取地址等。
舉個例子,你可以放心的使用下面的寫法,并能被正確的動態執行:
資源支持
一個功能模塊,除了代碼外,資源也是必不可少的,DynamicCocoa 的動態 bundle 支持:
xib 和 storyboard;
xcassets;
不放在 xcassets 里的圖片資源;
其他資源文件。
對于習慣于使用 IB 來開發 UI 的人來說,這將是一個很好的開發體驗。
工具鏈支持
我們使用 Ruby 開發了一套命令行工具( 類比為 xcodebuild ),大幅簡化了配置開發環境、OC 代碼轉換、資源處理、打包的復雜度,它可以:
解析 Xcode Project:讀取工程編譯選項,保持和 native 編譯參數一致;
增量編譯:緩存 JS 轉換結果,只重新轉換修改過的文件,大幅提高 build 速度;
鏈接:分析類依賴,將多個 JS 按依賴順序合并,提高文件讀取速度;
資源編譯:編譯用到的 xib、storyboard 和 xcassets;
打包:將 JS、資源等打包成 bundle。
對于開發者來說,就像 pod 命令一樣,所有操作都可以通過這個命令完成。
動態插件開發流程
首先 App 中需要集成 DynamicCocoa Engine SDK,用來執行下發的 bundle 開發到發布的流程如下圖所示:
當然,DynamicCocoa 只提供命令行工具和 Engine SDK,可以完成本地打包、運行和測試,而線上發布后臺、服務端、CDN 等需要自行解決。
在滴滴內部,我們構建了開發、Review、線上回歸測試、灰度、發布、回滾、統計的閉環系統,以服務的形式給內部接入。
HotPatch 過程
HotPatch 本質上是方法粒度上的動態化,所以在整個框架搭建起來后,HotPatch 也不難實現,使用 DynamicCocoa 做熱修復的最大優勢是開發者依然只對源碼負責,修改完 bug 后,打個 patch 包,修復成功后把源碼改動直接 push 到代碼倉庫就行了。
假設我們發現了下面的 bug:
然后在 Native 進行修復并自測:
自測完成后,在這個方法后面添加一個神奇的 Annotation:
使用命令行工具在 patch 模式下進行打包,就能把所有標記了的 method 提取出來,分別轉換成 JS 表示,打到一起進行發布。
除了修改一個方法外,patch 模式還支持:
調用原方法;
新增一個方法;
新增一個 property 來輔助修復 bug;
新增一個 Class。
最后,開發者可以安心的把修改后的代碼(甚至可以保留 Annotation)git push,完成熱修復工作。
打開黑箱
就像 Objective-C 是由 Clang 編譯器和 Objective-C Runtime 共同實現一樣,DynamicCocoa 也是由對應的兩部分構成:
在 Clang 的基礎上,實現了一個 OC 源碼到 JS 代碼的轉換器;
實現 OC-JS 互調引擎的 DynamicCocoa SDK。
我們知道,Clang-LLVM 的標準編譯流程是從源代碼經過預處理、詞法解析、語法解析生成語法樹,CodeGen 生成 LLVM-IR,進入編譯器后端進行優化和匯編,最終生成目標文件 (Mach-O)。
而我們既希望 Clang 幫助完成源碼處理的步驟,又希望生成結果是 JS 表示形式,于是在 Clang 生成抽象語法樹(AST)后,我們進行接管,實現了一個 OC2JS CodeGen,遍歷各個特定語法節點輸出 JS 表示:
由于轉換器和 Clang 前端標準編譯流程相同,所以只要 native 代碼能 build,轉換器就能 build,這也是 DynamicCocoa 能讓動態包和 native 保持嚴格一致的先決條件。
注:轉換器是基于 Clang 開發的獨立命令行工具,它的使用并不會對原有的 Xcode 工程產生任何影響。
另一部分是要集成進 App 的 DynamicCocoa SDK,它的職責是為 JS 中間代碼提供 Runtime 環境,實現 OC-JS 的互調引擎,能夠加載動態 bundle,提供便捷的 API,整體架構如下:
其中一些有趣的點:
底層使用 libffi 來處理各個架構下的 calling conventions,實現 caller 調用棧的構建和 callee 調用棧的解析,用于實現 OC / C 函數調用、動態 imp、block 等。
由于 JS 的弱類型,數值變量在做計算時很容易丟失類型信息,比如 int a = 1 / 2; 在 OC 中表示整除,結果為 0,但進入 JS 就都會按照 double 計算,結果為 0.5,造成了不一致。所以 DynamicCocoa 接管了 JS 中的類型信息,強轉或運算符都需要特殊處理。
為了實現 block,我們構造了和 native block 一致的內存結構,不論是 JS 創建的 block 還是 native 傳進 JS 的 block,都可以無差別的調用。
雖然 runtime 提供了動態創建 OC Class 的 API,但只能創建 MRC 的 Class,導致 ARC 下 ivar 并不會乖乖釋放,我們深入到 Class 和實例真實內存結構中,給動態創建的類增加了 ARC 能力,并按照 Non-Fragile ABI 模擬真實 ivar 內存布局和 ivar layout 編碼,如果你重寫了 dealloc 方法,DynamicCocoa 甚至能夠像 native 一樣自動調用 super。
DynamicCocoa 帶來的改變
DynamicCocoa 動態化技術給 App 開發帶來了很大的想象空間:
低成本的動態化:無需額外學習,無需重寫代碼,可以快速的將已有模塊動態化;
協作方式:對于大團隊,發布版本不必再彼此牽制;
功能快速迭代:無需經過審核和 App Store 發版,像 HTML5 一樣隨發隨上;
App 瘦身:Native 只需要留好插件入口,實現由網絡下發,減少 App 體積;
AB Test:不必局限于 Native 埋進去的 AB 功能 Test,發版后能動態下發各種 Test。
相比跨端方案,也帶來了一個新思路:iOS 和 Android 都保留 Native 開發模式,用各自的方式將 Native 代碼直接動態化,保持各平臺的差異性。
Q&A
與 JSPatch 有什么區別?
兩者思路上都是實現 JS 和 OC 的互調:DynamicCocoa 的重點是動態化能力,優勢在于完全不用寫 JS 和更多的語法特性支持;對于 HotPatch 來說 JSPatch 是更加小巧、輕量的解決方案。
這套框架在滴滴 App 有上線使用么?
有,在滴滴 App 已經上線并使用了好幾個版本,如滴滴小巴、專車接送機都有過 10k 級別的動態化模塊上線。
動態包運行的性能是否有很大下降?
動態 JS 代碼的運行要經過頻繁的 JSCore 和 OC 間的切換,性能相比 Native 必定會有損耗,但經過優化,現在已經達到了無感知的程度:在我們的實際使用中,若不在頁面上添加特定標志,開發者和 QA 都無法分辨出當前頁面運行的是 native 還是動態包… 后續會有詳細的性能分析和大家分享。
動態包大小如何?
與資源大小和 Native 源碼量有很大關系,不考慮資源的情況下,量級大概在 10000 行代碼 100KB 的動態包。
是否支持多線程?
現在簡單的支持 GCD 來處理多線程,可以使用 dispatch_async 將一個 block 放到另一個 queue 中執行。
如何定位動態包的 Crash?
動態 JS 代碼運行在 JSCore 中,并沒有直接獲取調用棧的方式,我們提供了 stack trace 功能,將最近調用棧中每個 JS 到 OC / C 的互調都記錄下來,在發生 Crash 時便可以取出來作為附加信息隨 Crash 日志上報給統計平臺,方便問題的定位。
會不會過不了蘋果審核?
市面上很多動態化、HotPatch 方案都基于 JS 的下發,運行在原生 JSCore 上,相信只要不在審核期間下發動態功能,Apple 是不太會拒絕的。
有沒有可能支持 Swift 直接動態化?
相比 OC,Swift 的動態化和 HotPatch 更加有難度,但我們已經有了可行的方案,是可以做到的,只是對于當前滴滴的現狀(絕大多數都在用 OC 開發),緊急程度并不高,后面再考慮支持。
是否有開源計劃?
有,我們正在積極的準備相關事項,于 2017 年初考慮開源。
來自:http://www.chinacloud.cn/show.aspx?id=24561&cid=12