1. 程式人生 > >滴滴 iOS 動態化方案 DynamicCocoa 的誕生與起航

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

作者簡介: 孫源(@我就叫Sunny怎麼了),滴滴出行 iOS 技術專家,多年專注於 iOS 開發,現就職於滴滴 App 架構組,在技術上做探索和深挖;善於刨根問底,對未知的東西興趣強烈,程式碼風格強迫症;同時喜歡寫部落格(http://blog.sunnyxx.com/),線上線下分享,貢獻開源(forkingdog)累計 star 破萬。
歡迎技術投稿、約稿、給文章糾錯,請傳送郵件至[email protected]

【CSDN 編者按】滴滴出行客戶端 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 定義interfacecategoryclass extensionmethodproperty,最重要的是支援完備的 ivar 定義,保持和 native 完全一致的例項記憶體結構;
  • ARC:可以正確處理 strongweakunsafe_unretained 等物件的引用計數,物件的 ivar 也可以正確的釋放;
  • C 函式:支援 C 函式的定義與 C 函式的呼叫、行內函數的呼叫;
  • 可變引數:支援 C 與 OC 的可變引數方法的呼叫,如 NSLog;
  • struct:支援任意結構體的使用,無需額外處理;
  • block:支援建立和呼叫任意引數型別的 block;
  • 其他 OC 特性:如 @selector@protocol@encodefor..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 年初考慮開源。

該從哪裡關注後續進展?

滴滴 App 架構組正式建立了微信公眾號 DDApp(直接搜尋),這也是其中的第一篇文章,我們會在上面釋出 DynamicCocoa 的最新的進展,還會把滴滴 iOS 和 Android 開發的乾貨技術文章分享給大家,歡迎關注。

瞭解最新移動開發相關資訊和技術,請關注 mobilehub 公眾微訊號(ID: mobilehub)。

mobilehub