Flutter啟動流程分析之外掛化升級探索

得物技術發表於2022-01-26

Flutter是Google推出的一款跨平臺框架。與Weex等其他跨端框架不同的是,Flutter的介面佈局繪製是由自己完成的,而不是轉換成對應平臺的原生元件。那麼各個平臺是如何啟動它的呢?從Flutter官方提供的架構圖上看,Flutter Embedder層提供了底層作業系統到Flutter的程式入口,平臺採用適合當前系統特性的方式去各自實現。本文基於flutter 2.0.6版本原始碼,來探索Android平臺上flutter Embedder層對應的啟動流程,看看這個過程中做了些什麼事情,有什麼問題是需要我們在專案中注意的。

這部分原始碼位於engine原始碼中的/engine/shell/platform/android/ 目錄下。

1.主流程

先來看看整體的流程:

Android以FlutterActivity/FlutterFragment/FlutterView的形式承載flutter介面。當我們使用AndroidStudio建立一個新的flutter工程時,生成的MainActivity是直接繼承了FlutterActivity,那麼很明顯,主要的邏輯都在這個FlutterActivity裡面了。從流程圖看到,flutter的啟動流程也是從FlutterActivity的onCreate方法開始的:

1.FlutterActivity將onCreate主要的操作委託給delegate物件去實現。

2.delegate中呼叫setupFlutterEngine建立FlutterEngine。

3.FlutterEngine初始化各種channel之後,再建立FlutterLoader去載入資原始檔和apk裡的打包產物,之後初始化JNI的幾個執行緒和DartVM。

4.delegate之後再通過FlutterEngine註冊各個外掛。

5.FlutterActivity呼叫delegate的onCreateView建立FlutterView。

6.最後,onStart生命週期中通過delegate的onStart方法執行DartExecutor.executeDartEntrypoint,這個方法會在jni層執行Dart程式碼的入口函式。至此啟動完成。

1.1.FlutterActivity

FlutterActivity也是繼承的Activity,但是它把主要的功能都委託給了FlutterActivityAndFragmentDelegate類去實現,實現的Host介面主要是支援在delegate中獲取FlutterActivity的一些引數,比如configureFlutterEngine,這些方法可以由子類去重寫,實現自定義配置。

接下來,我們看看FlutterActivity的onCreate(),主要的兩個步驟是:

1.delegate.onAttach(this): 初始化FlutterEngine、註冊各個外掛。(注意,這裡傳的this即是delegate中的host物件)

2.setContentView(createFlutterView() ): 建立FlutterView並繫結到FlutterEngine。

這兩個步驟都是委託給 FlutterActivityAndFragmentDelegate 去實現的。

1.2.FlutterActivityAndFragmentDelegate

1.2.1.onAttach

總結一下,onAttach中主要做了一下幾件事情:

1.設定flutterEngine:

1.1.判斷是否從快取中獲取;

1.2.判斷是否有自定義flutterEngine;

1.3.new 一個新的flutterEngine物件;

  1. 將外掛attach到host activity,最終會呼叫各個外掛的onAttachedToActivity方法。

3.建立PlatformPlugin

4.註冊外掛。

1.2.2.configureFlutterEngine

這裡說一下configureFlutterEngine(flutterEngine)主要是幹什麼的,這個方法是在FlutterActivity中實現的,程式碼如下:

它通過反射找到了GeneratedPluginRegistrant類,並呼叫了其registerWith方法。這個類我們可以在工程中的 /android/java/目錄下找到,是flutter tool自動生成的,當我們在pubspec.yaml中新增一個外掛,並執行pub get命令後即會生成。

系統預設使用反射實現,我們也可以在MainActivity中重寫這個方法,直接呼叫registerWith方法。

1.3.FlutterEngine

再來看看FlutterEngine的建構函式。FlutterEngine是一個獨立的flutter執行環境,通過它能使用DartExecutor執行Dart程式碼。

DartExecutor可以跟FlutterRenderer配合渲染UI,也可以在只在後臺執行Dart程式碼,不渲染UI。

當初始化第一個FlutterEngine時,DartVM會被建立,之後可以繼續建立多個FlutterEngine, 每個FlutterEngine對應的DartExecutor執行在不同的DartIsolate中,但同一個Native程式只有一個DartVM。

可以看到,這裡面做的事情還是很多的:

1.初始化AssetsManager。

2.建立DartExecutor並設定對應PlatformMessageHandler

3.初始化一系列的系統channel。

4.初始化FlutterLoader,載入Resource資源和libflutter.so、libapp.so等apk產物。

5.建立FlutterRenderer、FlutterEngineConnectionRegistry。

6.如果需要,自動註冊pubspec.yaml中宣告的外掛。

接下來看一下FlutterLoader相關的內容。

1.4.FlutterLoader

FlutterLoader以單例的形式存在,一個程式只用初始化一次。用來載入apk安裝包中的資原始檔和程式碼產物,必須在主執行緒中進行。

startInitialization()方法中主要做了以下幾件事情:

1.載入傳給activity的meta配置資訊;

2.提取apk安裝包中的assets資源,主要是在DEBUG和JIT_RELEASE模式下的產物 ,比如vmSnapshotData、isolateSnapshotData等;

3.載入flutter engine C++部分原始碼,即在flutterJNI執行System.loadLibrary("flutter")

public void ensureInitializationComplete(
    @NonNull Context applicationContext, @Nullable String[] args) {
  //多次呼叫無效
  if (initialized) {
    return;
  }
  ...
  try {
    //startInitializatioz中得到的幾個資原始檔目錄
    InitResult result = initResultFuture.get();
    //這個列表中動態配置了flutter啟動需要載入的一些資源的路徑
    List<String> shellArgs = new ArrayList<>();
    shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");
    //libflutter.so的路徑
    shellArgs.add(
        "--icu-native-lib-path="
            + flutterApplicationInfo.nativeLibraryDir
            + File.separator
            + DEFAULT_LIBRARY);
    if (args != null) {
      //方法引數中傳來的,可以在重寫FltterActivity::getFlutterShellArgs()來自定義引數
      Collections.addAll(shellArgs, args);
    }
    String kernelPath = null;
    //DEBUG和JIT_RELEASE模式下只載入snapshot資料
    if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
      ...
      shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
      shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData);
      shellArgs.add(
          "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData);
    } else {
    //RELEASE模式下載入libapp.so檔案,這是Dart程式碼編譯後的產物
    //預設是相對路徑
      shellArgs.add(
          "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);
      //同一個key可以存多個值,當根據前面的相對路徑找不到檔案時,再嘗試用絕對路徑載入
      shellArgs.add(
          "--"
              + AOT_SHARED_LIBRARY_NAME
              + "="
              + flutterApplicationInfo.nativeLibraryDir
              + File.separator
              + flutterApplicationInfo.aotSharedLibraryName);
    }
    ...
    //到jni層去初始化Dart VM和Flutter engine,該方法只可以被呼叫一次
    flutterJNI.init(
        applicationContext,
        shellArgs.toArray(new String[0]),
        kernelPath,
        result.appStoragePath,
        result.engineCachesPath,
        initTimeMillis);

    initialized = true;
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

這個方法的作用是動態配置flutter引擎啟動前的各種資源路徑和其他配置,以 --key=value 的方式統一新增到shellArgs中,然後呼叫flutterJNI.init到C++層去處理,C++層會將傳入的配置儲存到一個setting物件中,之後根據setting建立FlutterMain物件,儲存為一個全域性靜態變數g_flutter_main。之後初始化DartVM等步驟就可以用到這裡儲存的配置資訊了。

1.5.onStart

根據Android中Activity的生命週期,onCreate執行完之後就是onStart了。同樣的,FlutterView還是將onStart中的操作委託給了delegate物件去完成。

可以看到,onStart生命週期就做了一件事情:執行Dart程式碼的入口函式。這裡有一些需要注意的地方:

  1. DartExecutor只會執行一次,這意味著一個FlutterEngine對應的DartExecutor不支援重啟或者過載

2.Dart Navigator的初始路由預設是"/"。我們可以重寫getInitialRoute來自定義。

3.Dart 入口函式 預設是main(),重寫 getDartEntrypointFunctionName 方法可以自定義。

  1. executeDartEntrypoint最終會通過FlutterJNI的方法來呼叫JNI方法來執行。在UI Thread中執行DartIsolate.Run(config),根據entrypoint_name找到Dart入口的控制程式碼後執行_startIsolate執行入口函式,之後執行main函式的runApp()。

至此,Flutter專案成功在Android平臺上啟動完成。

2.應用-熱更新

其實我這次探索Flutter啟動流程的一個主要目的是尋找Flutter在Android側的熱更新方案。那麼看完了整個流程之後,我們要如何做到熱更新呢?

flutter app的apk安裝包的幾個主要產物是,flutter_assets、libflutter.so和libapp.so:

flutter_assets:包含flutter應用專案中的資原始檔,font、images、audio等;

libflutter.so:flutter embedder層相關的C++程式碼。

libapp.so:我們寫的Dart程式碼編譯後的產物

只要可以在載入之前動態替換掉libapp.so這個檔案,即可實現flutter程式碼的熱更新。

2.1.方法一:反射修改FlutterLoader

那麼libapp.so是在哪裡載入的呢?其實上面 1.4.FlutterLoader 已經提到了,在ensureInitializationComplete()方法中,有一個shellArgs列表儲存了資源路徑配置資訊。libapp.so對應的key是 "aot-shared-library-name"

那麼,只要替換掉這一塊程式碼,將路徑設定成自定義的路徑即可讓框架去載入新的libapp_fix.so檔案。具體步驟是:

1.繼承FlutterLoader,重寫ensureInitializationComplete(),將 "aot-shared-library-name" 對應的路徑設定成自定義的路徑。

2.我們看看flutterEngine中是怎麼建立的FlutterLoader例項的:

flutterLoader = FlutterInjector.instance().flutterLoader();

那麼,我們只要例項化自定義的FlutterLoader類,並通過反射的方式將FlutterInjector中的flutterLoader例項替換成新的例項即可。

2.2.方法二:重寫getFlutterShellArgs()

我們注意到ensureInitializationComplete()方法中往AOT_SHARED_LIBRARY_NAME這個key裡面新增了2個值,只有當相對路徑下找不到檔案的情況下才回去尋找絕對路徑下的檔案。那麼我們只要將自定義的so檔案路徑設定成 "aot-shared-library-name" 第一條value就可以讓框架只載入最新的安裝包了。

由於ensureInitializationComplete()方法會將引數String[] args中的內容全部加入shellArgs列表,那麼我們只要在args中加上 "aot-shared-library-name=自定義路徑" 這一條配置就行了,我們看看這個args引數怎麼來的:

host.getFlutterShellArgs().toArray()即使args引數的來源了。從之前的分析,我們已經知道了,delegate中的host物件是FlutterActivity的引用,我們再來看看FlutterActivity是怎麼實現的:

這是一個public方法,那麼我們只要在MainActivity中重寫這個方法,並在獲取到FlutterShellArgs之後將需要的配置新增進去即可:

很明顯,這個方法更加簡單有效。需要注意的是,這個配置只會在RELEASE模式下載入,所以DEBUG和JIT_RELEASE模式模式下除錯是不起作用的。

3.總結

最後,大致進行一下總結:

1.純flutter專案中,Android預設以FlutterActivity的形式承載flutter介面。Native-Flutter混合工程中還可以使用FlutterFragment/FlutterView2種方式,具體看使用場景。

2.FlutterActivity將絕大部分工作委託給FlutterActivityAndFragmentDelegate實現。

3.啟動過程主要是FlutterActivity的onCreate()和onStart()方法。

onCreate() 會初始化FlutterEngine、註冊各個外掛,之後建立FlutterView並繫結到FlutterEngine。

onStart() 主要是通過DartExecutor去執行Dart程式碼的入口函式。

4.初始化第一個FlutterEngine時會建立和初始化DartVM。可以建立多個FlutterEngine,一個FlutterEngine對應一個DartExecutor,每個DartExecutor在自己的DartIsolate中執行。

5.DartExecutor可以和FlutterRender配合渲染UI,也可以只執行Dart程式碼不渲染UI。

6.FlutterView有兩種模式:FlutterSurfaceView和FlutterTextureView。顧名思義,即分別使用surfaceView和textureView來承載flutter檢視。FlutterSurfaceView渲染效能更好,但是檢視在Native-Flutter混合工程中不支援靈活的z-index設定。

文/KECHANGZHAO

關注得物技術,做最潮技術人!

相關文章