Flutter dynamic -Android (1)

In this article we will take a formal look at how to modify the Flutter Engine source code to make it dynamic.

This article will be divided into two parts. This article will explain what core code needs to be modified, how to compile engine source code, and how to use FLUTTER application. The next article will show you how to package and compile an AAR for mixed development.

1. Change the core code of Flutter Engine

Before loading libapp.so and flutter_assets, modify the source code and replace it with its own path

1.1,FlutterLoadertheensureInitializationComplete

As you can see from the previous article, this method basically builds a list of shellArgs that are called in C layer code to initialize the configuration

Take a look at the following code:

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);
}
Copy the code

The locations of Flutter_assets, VM_SNAPshot_data and isolate_SNAPshot_data are configured in DEBUG and JIT mode. We know that the libapp.so file is essentially a packaged collection of vm_snapshot_data, isolate_snapshot_data, From here we can find the thought of will libapp. So and flutter_assets files in the debug file path or JIT mode, namely PathUtils. GetDataDirectory (applicationContext) directory.

Let’s look at how to modify the else part of this code:

// Check whether /user/0/package/app_flutter contains the libapp.so file. If so, pass the new path, otherwise use the libapp.so file of the default path (i.e. Lib /arm/ or lib/arm64/)
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);
Copy the code

If you don’t need to replace the flutter_assets file dynamically, this is enough to replace libapp.so dynamically. Another way to avoid modifying engine source code is to override the getFlutterShellArgs method and pass AOT_SHARED_LIBRARY_NAME to a custom path. If you have any questions, please contact us in the comments section.

The enclosedAOT_SHARED_LIBRARY_NAMECode used at the C layer

// Code location shell-->common--> switchs.cc
if (aot_shared_library_name.size() > 0) {
  // The loop causes AOT_SHARED_LIBRARY_NAME in the last shellArgs to take effect. Don't worry about setting multiple AOT_SHARED_LIBRARY_NAME
  for (std::string_view name : aot_shared_library_name) { settings.application_library_path.emplace_back(name); }}Copy the code

1.2,FlutterJNIthenativeAttach

This is a Jni method that calls libflutter. So. With this method we can pass a path to layer C and configure the custom path before layer C initializes AndroidShellHolder

The modified nativeAttach method body and related codes are as follows

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

 public void attachToNative(String dynamicPath, boolean isBackgroundView) {
    ensureRunningOnMainThread();
    ensureNotAttachedToNative();
		// Because Contenxt is not available in the current class, you need to pass in the path when FlutterNativeView and FlutterEngine call the attachToNative method
    nativePlatformViewId = nativeAttach(this, dynamicPath, isBackgroundView);
 }
Copy the code

1.3,platform_view_android_jni.ccModify the

// 1. New parameter type for jNI mapping method
bool RegisterApi(JNIEnv* env) {
  static const JNINativeMethod flutter_jni_methods[] = {
      // Start of methods from FlutterJNI
      {
          .name = "nativeAttach".// Add a String argument type
          .signature = "(Lio/flutter/embedding/engine/FlutterJNI; Ljava/lang/String; Z)J",
          .fnPtr = reinterpret_cast<void*>(&AttachJNI),},...// 2. AttachJNI method modification
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);
  // Get the configuration
  Settings settings = FlutterMain::Get().GetSettings();
  if(dynamic_path.size() > 0) {
      settings.application_library_path.clear();
    	// Set the new path before AndroidShellHolder is initialized
      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;
	
  // Pass in the modified 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; }}Copy the code

1.4,FlutterNativeViewCode for relevant modifications in

private void attach(FlutterNativeView view, boolean isBackgroundView) {
    mFlutterJNI.attachToNative(PathUtils.getDynamicPath(mContext), isBackgroundView);
    dartExecutor.onAttachedToJNI();
}
Copy the code

1.5,FlutterEngineCode for relevant modifications in

// 1. In the constructor
attachToJni(context);

// 2. AttachToJni method modification
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."); }}Copy the code

1.6,PathUtilsnewgetDynamicPathmethods

// Get the dynamic resource file path
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;
}
Copy the code

At this point, the methods required for dynamic have basically been modified, the specific code please see github.com/panmin/engi… Welcome star and Watch. The code will be optimized and updated from time to time.

Build the local engine

After modifying the engine code, we will see how to compile the modified engine into flutter. Jar and libflutter. So

2.1. Basic knowledge of compilation

  • The CPU architecture

    Arm64 corresponds to Armeabi-V7A for Android, arm64 corresponds to arm64-V8A for Android, x86 or x86 is generally used on emulators.

  • Whether the optimization

    The engine package is not optimized to add code that prints out of the C layer. Engine C++ uses FML_LOG(INFO) to print logs. The optimized package is also smaller.

  • The operation mode

    According to the modes of Flutter, there are debug, profile and release modes.

2.2 common compilation parameters

  • --android-cpu: INDICATES the CPU architecturearm,arm64,x86, such as:gn --android-cpu arm
  • --runtime-mode: Operating mode, corresponding todebug,profile,release, such as:gn --runtime-mode debug
  • --unoptiimized: Optimized or not. If this parameter is added, it indicates that the case is not optimized

2.3. Compilation begins

#1. Navigate to the 'engine/ SRC' directory
cd engine/src
#2. Compile the optimized Release code of the corresponding Android platform, and make reasonable use of the compilation parameters mentioned in 2.2 according to their actual use
./flutter/tools/gn --android --runtime-mode release --android-cpu arm
#Using the commands in 2 will generate an out/ Android_release directory in the SRC directory
#The code generated in compilation 2 becomes flutter. Jar and libflutter. So, which is the most time-consuming step, depending on the performance of your computer
ninja -C out/android_release
#If the CPU architecture used in 2 is ARM64, this step in 3 uses the folder android_release_arm64
#4. Compile the code needed for Android packaging
./flutter/tools/gn --runtime-mode release --android-cpu arm
#5. Do the same
ninja -C out/host_android
#If arm64 is used in 4, the host_android_arm64 folder is needed
Copy the code

The out/ Android_release folder contains libflutter. Jar and libflutter. So

Third, use local engine

This section only explains how to use the Pure FLUTTER project; As for how to use it in mixed development projects, I will give a separate article about it because it involves some gradle script modifications.

3.1. Package APK with local engine

#Package APK for ARM platform
flutter build apk --target-plarform android-arm --split-per-abi --local-engine-src engine/src --local-engine=android-release
#Package APK for ARM64 platform
flutter build apk --target-plarform android-arm --split-per-abi --local-engine-src engine/src --local-engine=android-release_arm64
Copy the code

3.2. How to view the new code after modificationlibapp.soandflutter_assetsfile

Libapp. so and flutter_assets files generated by the apK package installed with the local engine are needed to dynamically update the new code.

  1. Modifying the DART code
  2. Package apK using the commands in 3.1
  3. withlibPeer directorybuild/app/intermediates/flutter/releaseFind the corresponding CPU architectureapp.soFile, rename it tolibapp.soAnd then copy to when the app startsPathUtils.getDataDirectory(applicationContext)In the corresponding directory, which isuser/0/package/app_flutterDirectory; thebuild/app/intermediates/flutter/releaseIn the directoryflutter_assetsCopy to this directory as well.
  4. It takes effect the next time you restart the app

Four,

This article explains how to make code changes to the Flutter Engine code dynamically, and how to compile and use the local engine. In the next article, I will explain in detail how to use the local engine for hybrid development, and how to modify the script of the Bulid AAR, package it as an AAR for business use, and also for the benefit of the stakeholders. Welcome to follow and like.