滴滴 iOS 動態化方案 DynamicCocoa 的誕生與起航

ymbi4526 7年前發布 | 6K 次閱讀 移動開發 iOS開發

滴滴出行客戶端 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

 

 本文由用戶 ymbi4526 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!