Flutter動態化-Android(一)

panmin發表於2020-04-09

Flutter動態化-Android(一)

上篇我們講了flutter engine編譯環境搭建,這篇我們正式來看下如何修改flutter engine原始碼,實現動態化。

本篇文章將分為兩部分來講,本篇講述有哪些核心程式碼需要修改的,如何編譯engine原始碼,以及在flutter應用中該如何使用;下一篇會講解如何打包編譯成aar,以方便混合開發時使用。

一、flutter engine核心程式碼修改

思路:分析libapp.so和flutter_assets的載入過程,在載入之前修改原始碼替換需要成自己的路徑

1.1、FlutterLoaderensureInitializationComplete

從前面的文章我們可以知道,這個方法主要是構建shellArgs列表,在C層程式碼中會呼叫此配置初始化引數

先看下面這段程式碼:

if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
		String snapshotAssetPath =
				PathUtils.getDataDirectory(applicationContext) + File.separator + flutterAssetsDir;
    kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB;
    shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
    shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData);
    shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData);
}
複製程式碼

在debug和JIT模式時會配置flutter_assetsvm_snapshot_dataisolate_snapshot_data的位置,我們知道在libapp.so檔案的本質是相當於vm_snapshot_dataisolate_snapshot_data的打包合集,從這裡我們可以找到想到將libapp.soflutter_assets檔案放到debug或者JIT模式時的檔案路徑下,也就是PathUtils.getDataDirectory(applicationContext)目錄下。

我們再來看如何修改這個程式碼對應的else部分的程式碼:

// 檢視/user/0/package/app_flutter目錄是否存在libapp.so檔案,如果存在就傳遞這個新的路徑,否則就還使用預設路徑(也就是lib/arm/或者lib/arm64/)的libapp.so檔案
File appFile = new File(PathUtils.getDataDirectory(applicationContext) + File.separator + aotSharedLibraryName);
String aotSharedLibraryPath = applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName;
if(appFile.exists()){
  aotSharedLibraryPath = appFile.getPath();
}
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryPath);
複製程式碼

如果不需要動態替換flutter_assets檔案,其實上面就修改就足夠動態替換libapp.so了。還有一種不用修改engine原始碼的方法就是在繼承FlutterActivity時重寫getFlutterShellArgs方法,把AOT_SHARED_LIBRARY_NAME傳遞成自定義的路徑,親測有效,小夥伴們自行嘗試,有問題可以在留言區交流。

附上AOT_SHARED_LIBRARY_NAME在C層使用的程式碼

// 程式碼位置shell-->common-->switches.cc
if (aot_shared_library_name.size() > 0) {
  // 迴圈會導致最後一個shellArgs中AOT_SHARED_LIBRARY_NAME生效,不用擔心設定了多個AOT_SHARED_LIBRARY_NAME
  for (std::string_view name : aot_shared_library_name) {
    settings.application_library_path.emplace_back(name);
  }
}
複製程式碼

1.2、FlutterJNInativeAttach

這是一個呼叫libflutter.so的jni方法,通過這個方法,我們可以傳遞一個路徑到C層,在C層的初始化AndroidShellHolder之前將自定義的路徑配置進去

修改後的nativeAttach方法體以及相關程式碼,如下

private native long nativeAttach(@NonNull FlutterJNI flutterJNI, String dynamicPath, boolean isBackgroundView);

 public void attachToNative(String dynamicPath, boolean isBackgroundView) {
    ensureRunningOnMainThread();
    ensureNotAttachedToNative();
		// 因為當前類中無法獲得Contenxt,所以需要FlutterNativeView和FlutterEngine呼叫attachToNative方法時傳入路徑   
    nativePlatformViewId = nativeAttach(this, dynamicPath, isBackgroundView);
 }
複製程式碼

1.3、platform_view_android_jni.cc修改

// 1. jni對映方法新增引數型別
bool RegisterApi(JNIEnv* env) {
  static const JNINativeMethod flutter_jni_methods[] = {
      // Start of methods from FlutterJNI
      {
          .name = "nativeAttach",
        	// 新增一個String引數型別
          .signature = "(Lio/flutter/embedding/engine/FlutterJNI;Ljava/lang/String;Z)J",
          .fnPtr = reinterpret_cast<void*>(&AttachJNI),
      },
    。。。

// 2. AttachJNI方法修改
static jlong AttachJNI(JNIEnv* env,
                       jclass clazz,
                       jobject flutterJNI,
                       jstring dynamicPath,
                       jboolean is_background_view) {
  fml::jni::JavaObjectWeakGlobalRef java_object(env, flutterJNI);
  const auto dynamic_path = fml::jni::JavaStringToString(env, dynamicPath);
  // 獲取配置
  Settings settings = FlutterMain::Get().GetSettings();
  if(dynamic_path.size() > 0) {
      settings.application_library_path.clear();
    	// 在AndroidShellHolder初始化前設定新路徑
      settings.application_library_path.emplace_back(dynamic_path + "/libapp.so");
      settings.assets_path = dynamic_path + "/flutter_assets";
  }

  FML_LOG(INFO) << "settings.assets_path:" << settings.assets_path;
	
  // 將修改後的settings傳遞進去
  auto shell_holder = std::make_unique<AndroidShellHolder>(
      settings, java_object, is_background_view);
  if (shell_holder->IsValid()) {
    return reinterpret_cast<jlong>(shell_holder.release());
  } else {
    return 0;
  }
}
複製程式碼

1.4、FlutterNativeView中相關修改的程式碼

private void attach(FlutterNativeView view, boolean isBackgroundView) {
    mFlutterJNI.attachToNative(PathUtils.getDynamicPath(mContext), isBackgroundView);
    dartExecutor.onAttachedToJNI();
}
複製程式碼

1.5、FlutterEngine中相關修改的程式碼

// 1. 建構函式中
attachToJni(context);

// 2. attachToJni方法修改
private void attachToJni(Context context) {
		Log.v(TAG, "Attaching to JNI.");
  	// TODO(mattcarroll): update native call to not take in "isBackgroundView"
    flutterJNI.attachToNative(PathUtils.getDynamicPath(context), false);

    if (!isAttachedToJni()) {
      throw new RuntimeException("FlutterEngine failed to attach to its native Object reference.");
    }
}
複製程式碼

1.6、PathUtils新增getDynamicPath方法

// 獲取動態化資原始檔路徑
public static String getDynamicPath(Context applicationContext){
    String packagePath = getDataDirectory(applicationContext);
    String aotLibFile = packagePath + File.separator + FlutterLoader.DEFAULT_AOT_SHARED_LIBRARY_NAME;
    String flutterAssetsPath = packagePath + File.separator + FlutterLoader.DEFAULT_FLUTTER_ASSETS_DIR;
    File aotFile = new File(aotLibFile);
    File flutterAssetsFile = new File(flutterAssetsPath);
    if (!aotFile.exists() && !flutterAssetsFile.exists()) {
      packagePath = "";
    }
    return packagePath;
}
複製程式碼

到此,動態化所需要的方法基本都修改完了,具體程式碼請看github.com/panmin/engi…,歡迎starwatch,程式碼會不定期優化更新。

二、編譯本地engine

修改完engine的程式碼,這一小節我們就來看看如何將修改後的engine編譯成flutter.jarlibflutter.so

2.1、編譯相關基礎知識

  • CPU架構

    編譯結果包括armarm64x86這幾種架構,arm對應Android的armeabi-v7a,arm64對應Android的arm64-v8a,x86還是x86一般是模擬器上用的。

  • 是否優化

    未優化的engine包是可以新增列印出C層的程式碼的,engine的C++裡用FML_LOG(INFO)列印log;優化後的包體積也更小。

  • 執行模式

    根據flutter的模式是分為debugprofilerelease這三種模式的。

2.2、常用的編譯引數

  • --android-cpu: CPU架構,對應armarm64x86,如:gn --android-cpu arm
  • --runtime-mode: 執行模式,對應debugprofilerelease,如:gn --runtime-mode debug
  • --unoptiimized: 是否優化,帶上這個引數就說明是不優化的情況

2.3、編譯開始

# 1、定位到`engine/src`目錄
cd engine/src
# 2、編譯Android對應平臺已優化的release程式碼,這裡大家根據自己的實際使用情況,合理的使用2.2中提到的編譯引數
./flutter/tools/gn --android --runtime-mode release --android-cpu arm
# 通過2中的命令會在src目錄下生成一個out/android_release的目錄
# 3、編譯2中生成的程式碼成為flutter.jar和libflutter.so,這一步就最耗時的,有快有慢,看電腦效能了
ninja -C out/android_release
# 如果2中使用的CPU架構是arm64時,3中的這一步就要用android_release_arm64資料夾了
# 4、編譯Android打包時需要的程式碼
./flutter/tools/gn --runtime-mode release --android-cpu arm
# 5、同樣編譯一下
ninja -C out/host_android
# 如果4中使用的是arm64,這裡就需要用host_android_arm64資料夾了
複製程式碼

通過上的編譯,我們可以看到out/android_release資料夾中已經生成了flutter.jar和裡面已經包含了libflutter.so

三、使用本地engine

這一小節只講解純flutter專案時如何使用;至於混合開發的專案中如何使用,因為牽扯到一些gradle指令碼的修改,我會單獨抽出一篇文章來講。

3.1、使用本地engine打包apk

# 打包arm平臺的apk
flutter build apk --target-plarform android-arm --split-per-abi --local-engine-src engine/src --local-engine=android-release
# 打包arm64平臺的apk
flutter build apk --target-plarform android-arm --split-per-abi --local-engine-src engine/src --local-engine=android-release_arm64
複製程式碼

3.2、修改程式碼後如何檢視新的libapp.soflutter_assets檔案

對於已經安裝完使用本地engine打包的apk的手機來說,想動態更新新的程式碼,需要找到修改後程式碼打包生成的libapp.soflutter_assets檔案,這個檔案怎麼生成和找到的呢?

  1. 修改dart程式碼
  2. 使用3.1中的命令打包apk
  3. 在跟lib同級別的目錄build/app/intermediates/flutter/release下找到對應CPU架構的app.so檔案,將其改名成libapp.so,然後在app啟動時複製到PathUtils.getDataDirectory(applicationContext)對應的目錄下,也就是user/0/package/app_flutter目錄下;把build/app/intermediates/flutter/release目錄下的flutter_assets也複製到這個目錄下。
  4. 待下次重啟app時即可生效

四、總結

本篇文章講解了flutter engine程式碼實現動態化的程式碼修改,以及編譯和使用本地的engine,下一篇我會詳細講解在混合開發時使用本地engine,以及如何修改bulid aar時的指令碼,打包成aar供業務方使用,也是伸手黨們的福利,歡迎大家關注和點贊。

相關文章