Flutter和iOS混編詳解

 

前言


 

      下面的內容是最近在使用Flutter和我們自己項目進行混編時候的一些總結以及自己踩的一些坑,處理完了就順便把整個過程以及一些我們可能需要注意的點全都梳理出來,希望對有需要的小夥伴有點幫助,也方便自己後續的查看。

             

一:混編具體步驟以及需要注意的問題


 

      1:創建Flutter項目  (切記:下面任何命令執行出錯基本上都是Flutter環境有問題,多執行 Flutter doctor 檢查)

      這裡需要我們留意的就一點, 創建的Flutter項目的文件層級和你想混編的原生項目要同級,就像下面這樣:

 

       終端命令行如下: flutter_module:你自己的項目名稱,自己定義。-t 和 –template 一樣,別糾結。

flutter create -t module flutter_module

      還是前面開頭說的,有問題多執行 flutter dotcor檢查,要是沒有問題,正確創建成功之後是下面的情況:(我臨時在桌面創建的,請忽略位置)

       2:通過pod將Flutter模組導入項目

      我們在我們項目的podfile文件中加入下面兩句:

flutter_application_path = '../flutter_mixed'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

      注意: flutter_application_path 後面的是你自己Flutter項目的名稱。flutter_application_path為Flutter模組相對於podfile文件的位置。

      在target種加入下面這句

install_all_flutter_pods(flutter_application_path)

      我這裡剛好有一份寫demo時候的podfile文件,程式碼全都給出來,方便也想demo嘗試的小夥伴直接複製,節省時間。

platform :ios, '14.0'
source '//cdn.cocoapods.org/'

use_frameworks!
#use_modular_headers!

# 忽略引入庫的所有警告
inhibit_all_warnings!

# [!] Could not automatically select an Xcode project. Specify one in your Podfile like so:
# project 'path/to/Project.xcodeproj'

# [!] `xcodeproj` was renamed to `project`. Please update your Podfile accordingly.

xcodeproj 'flutter_mixed_ios.xcodeproj'

flutter_application_path = '../flutter_mixed'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'flutter_mixed_ios' do
    
    install_all_flutter_pods(flutter_application_path)
    
    ####
    pod 'AFNetworking'
    pod 'WoodPeckeriOS'

end

 

      3:接下來就是 pod install 

      4:關於原生項目的配置更改以及問題解釋

       <1> Flutter混編項目是不支援Bitcode的,具體Bitcode代表的是什麼,這個大家可以翻以前我的文章: 

 

      <2> Build Phases 添加 Script 具體的操作如下所示:

 

       添加下面內容:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed 

      注意:單純這樣添加之後編譯大概率是不通過的,主要問題就是集中的 FLUTTER_ROOT 這個點上

/packages/flutter_tools/bin/xcode_backend.sh: No such file or directory

      我們就把注意力放在 No such file or directory 上,別走別的岔路。解決上面這問題的方法就是在你的項目中指定一下FLUTTER_ROOT的具體路徑,讓不再No such file or directory就OK了

      具體的做法是在 Build Settings中找到 User-Defined 添加 FLUTTER_ROOT 和 FLUTTER_APPLICATION_PATH

      FLUTTER_ROOT是我Flutter環境所在的具體位置 FLUTTER_APPLICATION_PATH 是我這個Flutter項目所在的路徑,當然我們還有更靠譜的查找這兩路徑位方法。

      終端進入自己flutter項目,按照下面路徑/.ios(隱藏文件)/Flutter/Generated.xcconfig  我們open Generated.xcconfig 文件就,在最上面就可以看到我們需要的FLUTTER_ROOT 和 FLUTTER_APPLICATION_PATH 。

 

      經過上面的處理,我們的 No such file or directory的問題就解決了,最後我們說說 xcode_backend.sh ,其實關於它我想表達的就只有一點,就這個腳本的作用到底有哪些,他能幫我們完成什麼工作呢?

      前面的疑問,這篇文文章給出了具體的分析 #Flutter之ios腳本 xcode-backend.sh文件分析#,文章逐句分析了我們xcode_backend.sh腳本的程式碼,也就間接的闡述了它的作用。有興趣的朋友可以好好了解一下。

      經過腳本的處理,有這樣一種場景,我們在開發的時候假如修改了一些涉及到混編消息傳遞的內容(任何Flutter內容都可以),我們在Flutter這邊修改了程式碼,這時候你可以直接運行Xcode查看更改的內容是否正確,裡面具體的工作我們在運行Xcode它在執行xcode_backend.sh腳本的時候已經幫我們處理了。當然正常Flutter修改的內容我們運行Flutter項目查看,在原生傳遞消息給Flutter的時候需要我們運行iOS項目,就打個上面的比方,理解知道就可以了。

      至此,你的iOS和Flutter混編的程式碼是可以正常運行起來的了。 

      5:Local Network Privacy Permissions

      這個問題我們在查看Flutter官方文檔進行學習的時候肯定是可以看到的。  官方解釋傳送門

      在你運行混編iOS項目的時候,你不處理這個問題就可以看到下面內容的日誌:

      Failed to register observatory port with mDNS with error -65555. On iOS 14+, local network broadcast in apps need to be declared in the app’s Info.plist. Debug and profile Flutter apps and modules host VM services on the local network to support debugging features such as hot reload and DevTools. To make your Flutter app or module attachable and debuggable, add a ‘_dartobservatory._tcp’ value to the ‘NSBonjourServices’ key in your Info.plist for the Debug/Profile configurations.

      官方也給我們做出了提醒以及解釋說明:

    【 在iOS 14和更高版本,在你的應用程式的調試版本中啟用Dart組播DNS服務,以添加調試功能,如熱載入和DevTools via flutter attach。注意:該服務不能在你的應用的Release版本中啟用,否則你可能會遇到應用商店拒絕。一種方法是維護應用程式資訊的一個單獨副本。每個構建配置的Plist。下面的說明假設默認的Debug和Release。根據應用程式的構建配置,根據需要調整名稱 】

      我自己還是按照官方給的的處理方法處理的

      首先還是處理我們的plist文件,把它處理成debug和release兩個模式的,我們一旦改了它們。在 build settings 中一定要改動,不然編譯肯定過不了的!具體的操作如下圖:

                         

      Build Settings Info.plist 這裡我們添加的是 Info-$(CONFIGURATION).plist Debug和Release環境的讓它們根據自己的配置內容讀取。

      接下來就是再Debug環境中的配置問題,這裡主要有兩點:

      1、Privacy – Local Network Usage Description 填寫的 Allow Flutter tools on your computer to connect and debug your application.This prompt will not appear on release builds. ,當然我是寫demo隨便寫的,具體的在自己項目中需要自己填寫,這個許可權在iOS14之後審核比較嚴格,大家需要注意,要使用到得描述清楚,避免審核被拒絕,但具體的蘋果什麼加強這塊的審核,我們大致了解下這個許可權的用處就理解了。

    【 因為在過去的 iOS 版本中,應用可以隨意掃描本地網路中的設備,因此應用就可以很輕鬆地得到本地網路里所有設備的名稱和MAC地址。MAC地址是一種確認網路設備位置的地址,每個網卡都有一個唯一的MAC地址,加上MAC地址也具有唯一性,設備廠商會按照一定的規律分配MAC,所以不同的區域網都是獨一無二可以識別的。這樣就通過MAC地址和設備的名字以生成一個特定的「指紋」,持續地、跨應用地、跨設備地跟蹤用戶的行為,並對用戶畫像持續進行調整。就大部分應用而言,它們都不需要給本地網路許可權。因為它們沒有功能會使用到本地網路,請求這個許可權的主要目的就是為了跟蹤用戶並推送廣告。】

      2、Bonjour services  填寫的  _dartobservatory._tcp

      最後還剩一點就是把Copy Bundle當中的Info-Release.plist進行一個刪除。下面圖片中的內容我是已經刪除了的: 

       經過上面的處理之後,Local Network Privacy Permissions 這個問題我們就應該是解決了!

       

二:原生與Flutter通訊


     

     首先Flutter為我們提供了以下幾種原生和Flutter之間通訊的方式:

  •  FlutterBasicMessageChannel 雙向通道,iOS和Flutter都可以主動向對方傳遞消息,最簡單的傳遞數據方式。
  •  FlutterMethodChannel 也是雙向通訊,它的使用和FlutterBasicMessageChannel基本上一致,不同的點在於FlutterMethodChannel可以自定義Channel的name。
  • FlutterEventChannel 用於事件流的發送(event streams), 屬於持續性的單向通訊, 只能是iOS端主動調用, 常用於傳遞原生設備的資訊, 狀態等, 比如電池電量, 遠程通知, 網路狀態變化, 手機方向, 重力感應, 定位位置變化等等。

     具體的它們三者的使用我們就不在很具體的說了,我們就從FlutterMethodChannel這個方法入手,簡單的看一下Flutter給iOS發送消息以及iOS給Flutter發送消息時候具體的程式碼執行是什麼樣子的,具體的過程當中我們又遇到了那些問題,我們也簡要的進行一個分析。

     1、Flutter給iOS發送消息

      iOS端的程式碼,下面程式碼大致邏輯是iOS端接收到Flutter發送的channel name為MixChannelName.backToNative,消息名稱為 MixChannelMethod.iOSBack,執行返回上個控制器。

/// MixFlutterViewController 繼承與 FlutterViewController
extension MixFlutterViewController{
    
    /// 返回事件
    func channelBack() {
        /// MixChannelName.backToNative 字元串channel name
        self.flutterMethodChannel = MixFlutterMethodChannel.init(name: MixChannelName.backToNative, binaryMessenger: self.engine!.binaryMessenger)
        self.flutterMethodChannel!.setMethodCallHandler { [weak self] (call:FlutterMethodCall,result:@escaping FlutterResult) in
            /// 返回上一個頁面
            /// MixChannelMethod.iOSBack 字元串返回方法名稱
            if call.method == MixChannelMethod.iOSBack{
                
                self?.navigationController?.popViewController(animated: true)
                self?.flutterMethodChannel = nil
            } else {
                
                result(FlutterMethodNotImplemented)
            }
        }
    }
}

class MixFlutterMethodChannel: FlutterMethodChannel {
    
    deinit {
        debugPrint("MixFlutterMethodChannel - deinit")
    }
}

      我們再看看Flutter端的發送程式碼是怎麼處理的:

///  前面定義一個MethodChannel 名稱為flutter_backToNative 和iOS端的需要保持一致
static const _messageChannel = MethodChannel("flutter_backToNative");

/// 然後在你需要發送消息的地方調用
_messageChannel.invokeMethod("backToNative");

      經過上面的處理之後,我們的iOS端是能夠正接受到Flutter發送的消息的。

      2、iOS給Flutter發送消息

      Flutter端的程式碼,還是之前的_messageChannel這個渠道,直接調用setMethodCallHandler設置接收到消息的處理函數。

/// 建立和原生通訊的渠道
_messageChannel.setMethodCallHandler((call) => handleMessage(call.arguments));

/// 處理消息的方法
Future handleMessage(String message) async {

    print(message);
}

      iOS端的程式碼如下,flutterMethodChannel還是我們剛開始創建的渠道

/// 發送普通消息
/// - Parameter stringParams: stringParams description
func sendMessageWithString(_ stringParams:String){
      /// MixChannelMethod.goodsId 調用的方法名稱
self.flutterMethodChannel!.invokeMethod(MixChannelMethod.goodsId, arguments: stringParams)
}

     

      注意點: 在使用FlutterMethodChannel進行雙向通訊的時候,尤其需要注意的是iOS端和Flutter端的渠道Channel的name一定要保持一致!

 

      疑惑點:我在MixFlutterViewController的deinit方法中加入了日誌,然後綜合上面的MixFlutterMethodChannel中deinit的日誌,得出一個有點不理解的點,主要疑問如下面所示是在flutterMethodChannel的創建方式上。

         /*
         "MixFlutterMethodChannel - deinit"
         2022-05-08 22:28:26.159278+0800 flutter_mixed_ios[70375:6110936] flutter: 10086
         "MixFlutterViewController - deinit"
         "MixFlutterMethodChannel - deinit"
         2022-05-08 22:28:35.960283+0800 flutter_mixed_ios[70375:6110936] flutter: 10086
         "MixFlutterMethodChannel - deinit"
         "MixFlutterViewController - deinit"
         "MixFlutterMethodChannel - deinit"
         */
         /// 使用該方法創建後 在Flutter發送消息返回 列印日誌如上面注釋
         self.flutterMethodChannel = MixFlutterMethodChannel.init(name: MixChannelName.backToNative, binaryMessenger: self.engine!.binaryMessenger,codec: FlutterStandardMethodCodec.sharedInstance())
        
        
        /*
         2022-05-08 22:31:21.842965+0800 flutter_mixed_ios[70389:6112382] flutter: 10086
         "MixFlutterViewController - deinit"
         */
        /// 使用該方法創建後 在Flutter發送消息返回 列印日誌如上面注釋
        self.flutterMethodChannel = MixFlutterMethodChannel.init(name: MixChannelName.backToNative, binaryMessenger: self.engine!.binaryMessenger)

 

       總結:經過上面的內容,關於iOS和Flutter的混編的一些東西就都介紹完畢了,疑問點還是存在,等後面找到具體的答案之後我會補充在文章後面。要是對上面內容有什麼疑問,可以留言或者私信我,可以換個方式具體的溝通。