iOS與Flutter混合開發的姿式

photo from pixabay by freephotoccandroid

首先要解釋一下題目, 本文關於混合開發細節本文會簡要聊一些, 由於官方文檔與網友的智慧已經至關完備, 徹底能夠面向google編程, 這裏沒必要贅述。那麼就回到了本文的核心中來, 主要講述了針對 iOS 與 Flutter 混合開發中爲了一個小優化點而進行的一系列不 ( zi ) 懈 ( zuo ) 努 ( zi ) 力 ( shou ) 。ios

Flutter混合集成模式簡述

說到Flutter混合工程, 起因仍是由於這項技術的使用漸進式。在你們的現有業務中使用, 大多數場景是在原有業務中摸索試水, 先用在一個低頻頁面中, 坐觀其變。當看到開發case覆蓋率、crash率、性能、穩定性等一些列指標微微一笑的時候, 加之開發效率提高的加持, 一聲令下要大規模使用, 集結多個團隊, 更多名同窗來用的時候, 混合開發模式的優化就迫在眉睫了。git

如何讓多名同窗協同開發Flutter, 如何對原有工程開發模式的最小侵入, 以及如何快速集成開發和更好的工程化都是Flutter混合開發模式要解決的問題。github

官方方案

官方Flutter工程集成至IOS工程, 在這裏能看到Flutter官方是多麼親切的指導這咱們來使用這個技術, 其中總共給出裏 ABC 三種方案。shell

本着不會都選 C 的原則, 選擇這個方案最靠譜。一句話形容就是將Flutter技術的編譯產物 ( Flutter.framework, App.framework等 ), 經過Cocoapods集成至iOS工程中, 它省去了方案 A 的複雜Podfile的修改, 也避免了方案 B 的手動 embed 和 link Flutter產物。編程

注意 這個方案的Pod引入方式是本地引入, 因爲是本地打包, 並不太適合多人協做json

業內方案

業內方案就比較簡單了, 基於官方的方案 C, 將Flutter產物發佈到遠端便可, 話很少說, 直接上圖xcode

嘿嘿, 圖片可能你們會以爲好熟悉啊, 這個不是重點, 直觀表達纔是。ruby

固然這個方案也不是那麼的簡單, 仍是有一些細緻的工做, 包括收集依賴, 處理plugin以及生成Podspec文件等操做, 須要在一個腳手架的工具中完成這些操做。bash

基於這個腳手架, 原有IOS工程能夠快速無成本的接入flutter的業務模塊, 就像引入一個三方庫那麼簡單, 只須要在 Podfile 文件增長一行便可

pod 'FlutterXXX'
複製代碼

是否是很開心, 很興奮, 完成了如此重大技術的引入盡在彈指之間。可是在開發 Flutter 的同窗就一臉黑了, 開發過程當中只能使用 flutter 提供的 demo 工程跑起來, 根本沒有原有 iOS 工程的上下文環境, 沒辦法開發的, 這就是下面要聊的混合工程開發模式。

一鍵集成的思路

想要進行 Flutter 與 iOS 工程混合開發咱們須要什麼能力?

  • 能在 IOS 工程中運行 Flutter 項目
  • 運行的 Flutter 項目可以進行熱更新 ( hot reload )

我理解至少有上面兩項能力就能開心的進行玩耍了, 體會着擁有 Web 開發的體驗, 看着擁有 Native 開發的高性能也是心裏歡喜, 還能夠不時的幻想着一我的完成 iOS、Android、桌面應用三種產出結果, 貌似就更有成就感了。

因而咱們進一步查看 Flutter 的 Module 類型工程, 會發現谷歌粑粑已經幫咱們作好了這一切, 在工程目錄有一個 .ios文件夾, 下面這一個 Ruby 腳本 podhelper.rb, 一個方法 install_all_flutter_pods 搞定一切。

立刻咱們就開始快馬加鞭的來包裝它, 將它集成到咱們的腳手架工具中, 爲了用戶體驗 ( KPI ) 也是拼了, 最終咱們想要的是這樣的 :

程序猿 : 腳手架, 咱們想要把 IOS 工程和 Flutter 工程集成在一塊兒
腳手架 : 好的, 給我他們的目錄, 我幫你搞定...
$> integrate iOS_PATH FLUTTER_PATH
$> done 
複製代碼

通過程序猿的一頓操做猛如虎, 將 Flutter 的混合開發模式能力也集成到了腳手架工具中。這個能力在官方的文檔中也有體現, 是須要在 iOS 工程編譯階段插入可執行腳本, 使用 flutter_tools 中的腳本生成 Flutter 相關的 framework, 進而能夠進行混合開發, 實現 hot reload 功能。

這裏暫時不針對爲何使用 flutter_tools 腳本會實現 hot reload 能力的緣由展開, 我是不會告訴你我還不知道呢

下面就是如何將 Flutter 集成至 iOS 工程中進行開發的核心邏輯圖 :

有了 Flutter 腳本在編譯過程當中的集成, 原有 iOS 工程就被賦予了針對 Flutter 代碼 hot reload 的能力, 不得不興奮一小下。

發現的小問題

非 Flutter 開發的同窗的混合集成方式以及 Flutter 開發同窗的混合開發方式都已經在腳手架中具有了能力, 如今是時候由程序猿大展身手的時候了, 他們快速下意識的打開了 Xcode 和 VS Code ( Android Studio 也可 )。 正所謂倚天屠龍在手, 誰與爭鋒, 在 Xcode 下熟練的按下了快捷鍵 Command + R , 開始編譯啓動 App, 待 App 順利喚起的時候, 在 Flutter 工程的根目錄下一鍵 flutter attach 命令, 搞定。

當完成了今天的開發任務的時候, 因爲開發效率的提高, 程序猿還有一丟丟時間來回顧整個開發過程:

  1. 打開 Xcode 進行 App 啓動
  2. 打開 Flutter 工程, 進行工程連接 , 使用flutter attach 命令
  3. 在 Flutter 工程中進行開發

對於 Flutter 開發者, 在多數場景下, 只須要在 Flutter 工程下進行開發便可, IOS工程只有在提供橋接能力的時候, 纔會經過 IOS 端上的原生能力進行支持。

那麼, 爲何不能經過在 Flutter 工程下的 flutter run 命令來啓動開發呢? 這樣的話平常開發步驟就變成了 :

  1. 打開 Flutter 工程, 使用flutter run 命令啓動 App
  2. 在 Flutter 工程中進行開發

省去了個打開 iOS 工程的過程, 有沒有更簡單些, 也對非 iOS 開發者比較友好呢 ?

好的, 程序猿收到需求, 準備開工~

分析過程 ( zi zuo )

既然需求已定, 那麼程序猿必使命必達, 完成需求。

首先來看下 Flutter 工程中 module 模式工程的工程目錄 :

├── .android
├── .ios
├── lib
├── pubspec.yaml
複製代碼

經過目錄結構能夠清晰的看到, lib 是 Flutter 相關代碼, .ios.android 是 native 工程, 那麼 flutter run 命令必定是啓動了這兩個 native 工程, 進一步看下 .ios 目錄結構 :

├── .ios
│   ├── Config
│   ├── Flutter
│   ├── Runner
│   ├── Runner.xcodeproj
│   └── Runner.xcworkspace
複製代碼

沒錯, 就是它, 熟悉又陌生的 Runner 工程, 下面也會屢次提到它。這樣能夠推斷出當執行 flutter run 命令的時候, 會使用 xcodebuild 命令找到這個 Runner工程進行編譯啓動, 因此若是能把 Runner 工程替換成咱們的項目工程, 理論上就能夠了。

通過查看 Flutter 源碼能夠發現, flutter run 命令只會查找根目錄下 .iosios 兩個目錄做爲啓動工程目錄, 而且優先查找 ios 目錄, 由於 .ios 目錄會被 flutter clean 命令清除掉, 因此認爲有 ios 目錄是開發者的工程目錄, 源碼片斷以下 :

// 源碼目錄: packages/flutter_tools/lib/src/project.dart 315:324
  Directory get ephemeralDirectory => parent.directory.childDirectory('.ios');
  Directory get _editableDirectory => parent.directory.childDirectory('ios');

  /// This parent folder of `Runner.xcodeproj`.
  Directory get hostAppRoot {
    if (!isModule || _editableDirectory.existsSync()) {
      return _editableDirectory;
    }
    return ephemeralDirectory;
  }	
複製代碼

進一步查看源碼也不難發現, flutter_tools 工具中也對 iOS 工程名稱作了限制 ( 就是hard code), 必須是 Runner 的工程名稱, 以及在後面的 xcodebuild 階段指定的 target 也是 Runner

$> xcodebuild --target Runner
複製代碼

分析到這裏, 只能說程序猿太難了, 要作這麼多適配工做, 可是, 可是不得不說只是個開始而已...

實現過程 ( zi shou )

通過了大量的分析及實驗以後, 終於有了一份可行性很高的適配指南產出了, 棒棒噠

1. 修改 Podfile 文件, 注入腳本
2. 在 Flutter工程根目錄建立 ios 目錄 
3. ios 目錄中有項目工程, 工程名必須是 Runner.xcodeproj
4. 工程中必須有名稱爲 Runner 的 target
5. 修改xcodeproj文件, 適配環境變量及 target 配置
複製代碼

能夠看出來 Flutter 官方的 flutter run 命令是基於將 iOS工程屬於 Flutter工程的一部分來設計的, 而且須要遵循許多 Flutter工程的標準才能不出錯的運行起來。

可是每每事與願違, 咱們想要的是項目工程與 Flutter 工程解耦, 貌似只能搬出軟鏈接的方式了, 例如 :

$> ln -s source target
複製代碼

這樣既能保證原有的項目工程和 Flutter 工程物理分離開來, 又能夠知足工程更名稱等操做, 因而程序猿將 iOS 工程連接到了 Flutter 工程下的 ios目錄下, 而後進行了一頓操做, 大體的過程以下圖所示 :

從圖中能夠看出來, 第2、三步驟花費的精力比較多, 也是整個方案的核心工做。其中修改 Podfile 的工做主要有兩點 :

  1. 複製原有工程 target, 命名爲 Runner, 主要是爲了能讓 flutter_tools 腳本找獲得咱們的工程
  2. 因爲是使用軟連接來與 Flutter 工程的關聯, 那麼 flutter_tools 裏面定義的一些路徑會有誤差, 咱們須要矯正這些使用到的系統環境目錄

最終修改過的 Podfile 大體是這樣的 :

...
load "Dflu.rb"	# 加載腳本
target xxx do
...
	dflu_install_flutter_pods do	# 調用腳本安裝依賴
	end
...
end
target Runner do	 # 從 xxx 複製而來, 用於 flutter run 命令調用
...
	dflu_install_flutter_pods do	# 調用腳本安裝依賴
	end
...
end
...
複製代碼

Dflu.rb這個Ruby腳本作了兩件事情 : 第一是經過解析Flutter工程, 調用 CocoaPods 庫的接口來插入 Flutter engine庫, 以及相關plugin等依賴, 第二是適配全部 Flutter 使用的系統環境變量中和工程路徑有關的變量。

修改 xcodeproj 文件的操做也是比較大的一個工做量, 這一步驟主要是爲了建立一個 Runner的 target 來適配 flutter_tools 腳本的調用, 這裏的開發主要仍是依賴 Cocoapods 的xcodeproj 源碼, 這個庫已經幫咱們作了許多針對xcodeproj的操做的封裝, 踩在巨人的肩膀上那叫一個爽, 很快就能實現複雜功能。

爲了實現程序猿當初許下的諾言 -- 一鍵集成開發的能力, 在他們在腳手架工具中就開發了這樣一條指令 :

$> dflu integrate ios NATIVE_PATH FLUTTER_PATH
複製代碼

程序猿默默的祭出了這樣一條命令, 內心是莫名的自豪, 頓時以爲個人代碼是最好的, 以爲此處應該有掌聲, 然而這只是他的幻想罷了, 仍是簡單看下這條命令的大體入口代碼吧

執行完集成命令, 看到命令行輸出 All Done 的那一刻, 程序猿狠狠的敲下了 flutter run 這條他求之不得的命令, 默默點了支菸, 看着 flutter pub get , pod install, xcodebuild, run 等命令的執行, 看似表面很平靜的表情, 其實心裏早已焦急不已, 除非沒有任何錯誤報出。

然而, 不是一切盡在程序猿掌握之中, 在項目工程中, 仍是有一些特殊狀況須要處理, 例如項目工程使用了自定義的 xcconfig 配置, Podfile 文件中經過 path 方式引入了三方庫等, 都須要一一適配, 只能說程序猿太難了...

實現難點

爲了實現這個小優化, 沒想到要作這麼多工做, 總體實現下來不能說有多大的難點, 只是須要反覆不停的實驗以及在 Flutter, Cocospods 源碼中穿梭。

爲了達成當初能夠經過 flutter run 進行開發的目標, 也須要考慮原有的直接在 iOS 工程中開發的訴求, 爲了適配兩種開發方式, 在能力適配上, 比較多的工做是適配兩種啓動方式的工程路徑問題, 保證兩種啓動方式都能找到正確的工程目錄以及系統環境路徑。

Podfile 腳本注入

爲了能方便的調用 Cocoapods 的 API, 腳手架工具也使用了 Ruby 做爲開發語言, 在 Podfile 文件的修改過程當中, 主要調用了 CocoaPodsCocoaPods Core 的 API, 大體過程包括 :

  • 修改原有工程 pod 引入方式爲 path 的, 將 path 路徑修改成絕對路徑
  • 針對 target 添加 script_phases 的自定義腳本
  • 經過 pod 方式引入 Flutter engine 及相關的庫
  • 引入 ( load ) dflu 腳本, 並複製原有工程的 target, 添加至 Podfile 尾部並更名爲 Runner

這個過程當中會不斷的對 CocoaPods 源碼有了解, 尤爲是在對 pod 的 path 修改的時候, 怎麼樣修改已經存儲過的數據, CocoaPods 對 Podfile 的數據是如何存儲的, 都須要一一搞清楚, 而後去修改它, 正以下面的源碼, 整個Podfile會被存儲到一個 hash 數據結構中 :

# 源碼目錄 : Core/lib/cocoapods-core/podfile.rb 365:371
    private

    # @!group Private helpers

    # @return [Hash] The hash which store the attributes of the Podfile.
    #
    attr_accessor :internal_hash
複製代碼

至於 Podfile中的 pod, source, target 等方法都是定義在 Core/lib/cocoapods-core/podfile/dsl.rb 這裏的, 它會將信息分發存儲到不一樣的實例中。

xcodeproj 文件修改

針對xcodeproj文件進行的修改就只有一個操做, 就是複製一份原有工程的target, 而後命名爲 Runner。

這裏要依靠 CocoaPods Xcodeproj 的源代碼, 因爲 CocoaPods 的功能也是會對 xcodeproj 文件進行多維度修改, 因此這個工具庫比較獨立, 能夠直接使用。

在複製的過程當中. 比較難肯定的一點就是具體須要複製哪些信息, 修改哪些信息, 這些都須要從 Flutter 工程中自帶的 Runner工程中獲取, 進行不斷的文件對比, xcodeproj 文件本質上是一個由 JSON 形式表達的數據結構, 它能夠被 Xcode 解析成操做界面, 也能夠被解析成內存對象, 從下面源碼能夠看出, Xcodeproj 庫就將該文件解析成內存對象 :

# 源碼目錄 : Xcodeproj/lib/xcodeproj/project.rb 106:114
    def self.open(path)
      path = Pathname.pwd + path
      unless Pathname.new(path).exist?
        raise "[Xcodeproj] Unable to open `#{path}` because it doesn't exist."
      end
      project = new(path, true)
      project.send(:initialize_from_file)
      project
    end
複製代碼

固然, 這個解析過程仍是比較複雜的, 由於這個 JSON 文件中的對象對應的 Key 都是相似 25E80FB17FE2B7862DABB507 這樣的, 由 xcode 生成的, 並且嵌套層級比較深。

通過不斷的對比和試錯, 程序猿也是最終找到了須要的信息, 修改步驟以下 :

  1. 新建一個 Runner target, 並添加至工程文件中
  2. 從原有 target 中複製 Build phases 相關信息至 Runner, 這裏還要區分 source , resource , framework, shell 等類型, 進行不一樣的複製操做
  3. 複製 product 信息
  4. 複製 build configuration list 信息
  5. 保存工程文件

程序猿終於能夠長出一口氣了, 自做之路立刻就要結束了, 就剩下一些收尾工做就能夠了。

其餘適配

在真實的項目中, 可能還會出現不少的項目配置項, 至少目前碰到了一些 :

  • 項目工程使用了自定義的 xcconfig 文件來配置系統環境變量, 這個時候須要在自定義的 xcconfig 中配置 PODS_ROOT變量的路徑
  • 項目中其餘引入了其餘的 Flutter 項目工程, 而且也依賴了 Flutter engine 等核心庫, 這樣會形成衝突, 須要協調解決

源碼調試

在總體適配過程當中, 會查看幾個庫的源碼, 也會進行調試, 在這裏程序猿使用的 VS Code 開關工具, 可謂一個套路走天下, 徹底能夠覆蓋 Flutter , Shell , Ruby 的開發調試, 真香~

調試是使用的 code 的 debugger, 配置的 launch.json 是這樣的 :

"configurations": [
    {
      "name": "Debug Local File",
      "type": "Ruby",
      "request": "launch",
      "cwd": "${workspaceRoot}",
      "program": "${workspaceRoot}/bin/dflu",
      "args": ["mode", "ios"]
    },
  ]
複製代碼

調試 flutter_tools 源代碼的配置以下 :

"configurations": [
    {
      "name": "Dart",
      "program": "這裏是flutter工程的入口文件 xxx.dart",
      "request": "launch",
      "type": "dart"
    }
  ]
複製代碼

有了源碼調試能力, 程序猿是有如神助, 不用再猜想, 幻想, 扣代碼, 爲了那個 hash 裏面到底存的是啥而發愁?

問題

當一切都風平浪靜的時候, 程序猿緩緩坐下來, 反思這樣作可能帶來的問題, 不可避免的要說下, 畢竟本身挖的坑本身仍是要填的。

  • 在 iOS 中添加或修改 Podfile 的依賴的時候, 因爲 Runner target 的存在, 須要同步修改兩部分
  • 腳手架集成是在項目的分支基礎上集成的, 若是項目要切換分支, 須要作現場的復原操做

這樣的利弊分析下來, 貌似利小於弊, 程序猿有些恍惚了, 我都作了什麼。沒事, 咱們還能夠繼續優化它, 這時心裏的另外一個聲音出現了。

小結

整篇下來出現頻次最好的當屬 Runner 了, 這個工程啓動的引路人, 爲了適配它, 程序猿已經好幾個風黑月高的夜晚不能寐, 抓耳撓腮也常常伴隨, 偶爾還會自問, 這樣作是否是太麻煩了, 本來的操做, 打開 xcode -> 啓動 App -> flutter attach , 它不香麼?

正如老羅的那本書名同樣 <<生命不息,折騰不止>>, 程序猿也是拼了。

雖然經過此次小優化的折騰, 程序猿確實丟了半條命, 可是也收穫了很多, 好比咱們經常使用的 Podfile 文件究竟是怎麼樣工做的, 在 CocoaPods 裏面咱們引入的一些依賴, 包括不一樣的引入方式是怎麼經過格式化的數據存儲的, 甚至對 CocoaPods 的源碼架構都有了一些的瞭解, 再好比針對 flutter_tools 這個庫的工做原理, 是如何高效的支持 Flutter 應用的開發協做的, 甚至對 flutter_tools生成的整個 fluttert 命令行工具的原理都有了一些瞭解, 也是小有收穫的。

目前這個腳手架仍是屬於 Flutter 混合工程集成與開發的一個工具, 小展望一下在 Flutter 開發生態上的建設道路還很長, 至少有一套 flutter 開發套件來支撐混合集成、開發、engine管理、打包和發佈等一些列工程化工具集, 才能更好的聚焦技術和業務成長。

參考

  1. Flutter 源碼
  2. CocoaPods 源碼
  3. CocoaPods Core 源碼
  4. CocoaPods Xcodeproj 源碼
相關文章
相關標籤/搜索