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

我和本文的作者孫源早就認識,我參加過孫源組織的好幾次線下分享活動。孫源是一個對技術喜歡刨根問底的人,熱愛分享和開源,同時特別喜歡狗,除了他的頭像外,他在百度時的開源組織都叫 forkingdog。

孫源後來去了滴滴 App 架構組,近期一直在潛心研究編譯器相關的東西,如果你關注過他今年在 MDCC 和 iDev 大會上的分享的話,你就會發現他的分享內容都與編譯器有關。其實,他分享這些是有原因的,因爲他們團隊在做一個很牛逼的動態化方案,可以直接把 Objective-C 代碼經過編譯,轉換成 JS 代碼後下發給客戶端。


我一直不知道這裏面的技術細節,直到上週末他找到我,希望在這裏發表他們的 iOS 動態化方案,於是我就有幸成爲了本文的最早一批讀者。


讀完之後,我意識到,他們團隊的這篇文章將會極大促進業界內對於 iOS 動態化方案的探索,也將會對其它動態化方案,例如 React Native, Weex, LuaView, 甚至 JSPatch 造成深遠影響。


這篇文章你一定得看,感謝滴滴 App 架構組授權發表。


方案誕生

動態化一直是 App 開發夢寐以求的能力,而在 iOS 環境下,Apple 禁止了在 Main Bundle 外加載和執行的自己的動態庫,所以像 Android 一樣下發原生代碼的方案被堵死。

後來像 ReactNative、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 發版,像 h5 一樣隨發隨上

  • 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 年初考慮開源。

該從哪裏關注後續進展?

請關注滴滴 App 開發技術微信公衆號 DDApp,我們會在上面發佈 DynamicCocoa 的最新的進展,也將會把滴滴 iOS 和 Android 開發的乾貨技術文章分享給大家:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章