Zhou Jianhua: Wedoctor mobile terminal diagnosis and treatment team, like reading and exercise Android program ape

preface

In the previous article, Flutter 2 Router From Start to Stop – Basic Use, Differences & Advantages, we focused on the basic use of multi-engine hybrid development and the difference between multi-engine and single-engine hybrid development. This article will look at the source code to see how multi-engine reuse is implemented.

1. Compile and debug the source code of Flutter 2

If you want to do a good job, you must first use the tool. Here we first explain the source code compilation and debugging steps:

The source code to compile

Install depot_tools and configure environment variables

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
Copy the code

Create an empty engine directory and create a.gclient configuration file in the directory. In.gclient, configure the github project address from the main project fork of the Flutter /engine

solutions =[{"managed": False.
    "name": "src/flutter".
    "url": "https://github.com/Alex0605/engine.git".
    "custom_deps": {}.
    "deps_file": "DEPS".
    "safesync_url": "".
  }.
]
Copy the code

Run gclient sync in the engine directory

Switch source code. An important step before compilation is to switch the source code to the commit point corresponding to the engine version of the native Flutter SDK

Check the local version of the Flutter SDK engine. This file contains the commit ID
vim src/flutter/bin/internal/engine.version

# Adjust code
cd engine/src/flutter
git reset --hard <commit id>
gclient sync -D --with_branch_heads --with_tags

Prepare the build file
cd engine/src

#Android
# Generate host_debug_unopt compile configuration with the following command
./flutter/tools/gn --unoptimized
# Android ARM (Armeabi-v7a) compile configuration
./flutter/tools/gn --android --unoptimized
# Android ARM64 (Armeabi-v8a) compile configuration
./flutter/tools/gn --android --unoptimized --runtime-mode=debug --android-cpu=arm64
# compiler
ninja -C out/host_debug_unopt -j 16
ninja -C out/android_debug_unopt -j 16
ninja -C out/android_debug_unopt_arm64 -j 16

#iOS
# unopt-debug
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm64

./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm64

ninja -C out/ios_debug_unopt_arm
ninja -C out/ios_debug_unopt
ninja -C out/host_debug_unopt_arm
ninja -C out/host_debug_unopt
Copy the code

The compiled directory is as follows:

Source code run debugging

Create a flutter project by command

flutter create --org com.wedotor.flutter source_code

Open the Created Android project with Android Studio

Add the localEngineOut property to gradle.properties file and configure it as follows:

org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF- 8 -
android.useAndroidX=true
android.enableJitfier=true
localEngineOut=/Users/zhoujh/myproj/3-proj/flutter/engine/src/out/android_debug_unopt_arm64
Copy the code

The engine/SRC/flutter/shell/platform/android project (called * * flutter engine engineering) imported into the android Studio

Run the Flutter App (called the Flutter App Project) using a custom Flutter engine, as described in steps 1-3

Set a breakpoint in the source code and start the Debugger to connect to the started Flutter App process

PS: I’m using Clion to read the C++ code here. The configuration is simple. Copy the generated compile_commands.json file to SRC /flutter and open the project with Clion

2. Read the source code of Flutter 2

Before proceeding to the source code analysis, take a look at the core architecture diagram provided in the official documentation, which also represents the overall Flutter architecture.

FlutterThe architecture is divided into three layers:The Framework, the EngineEmbedder.

1), the Framework: Dart implements the Framework, including Material Design style Widgets, Cupertino style Widgets for iOS, basic text/image/button Widgets, rendering, animations, gestures, and more. The core code of this part is: The Flutter Package under the Flutter repository, and the PACKAGE of IO, async, UI under the Sky_engine repository (DART: THE UI library provides the interface between the Flutter framework and the engine). The dart: UI library is a binding to the C++ interface of the Skia library in Engine. The Dart: UI library allows you to use the Dart code to operate the Skia drawing engine. So we can actually draw the interface by instantiating the classes in the Dart: UI package (such as Canvas, Paint, etc.). However, in addition to drawing, coordinating layouts and responding to touches are all cumbersome to implement, which is exactly what the Framework does for us. Rendering Layer Rendering is the first abstraction layer on top of the :: Dart: UI library, and it does all the heavy math for you. To do this, it uses the RenderObject object, which is the rendering object that is actually drawn to the screen. The tree of these renderObjects handles the actual layout and drawing.

2) Engine: the Engine is implemented in C++, including Skia, Dart and Text. Skia is an open source two-dimensional graphics library that provides a common API for a variety of hardware and software platforms. On Android, Skia is shipped with the system. On iOS, the Skia library needs to be packaged as an APP, which results in a larger iOS application package developed by Flutter. The Dart runtime can run the Dart code in JIT, JIT Snapshot, or AOT mode.

3) Embedder: The Embedder is an Embedder layer where the Flutter is embedded on various platforms. The main work here is to render Surface setup, thread setup, plugin, etc. As can be seen here, the platform correlation layer of Flutter is very low, the platform (such as iOS) just provides a canvas, the rest of the rendering related logic is inside the Flutter, which gives it a good cross-end consistency.

The FlutterEngineGroup object is created in the Application onCreate method when the app is started

public void onCreate(a) {
    super.onCreate();
    // Create the FlutterEngineGroup object
    engineGroup = new FlutterEngineGroup(this);
}
Copy the code

3. When creating FlutterEngineGroup, make the sub-engines created by this engine group share resources faster and occupy less memory than those created by FlutterEngine constructor alone. When creating or recreating the first engine, The behavior is the same as that created through the FlutterEngine constructor. Resources in the existing engine are reused when subsequent engines are created. Shared resources are retained until the last engine is destroyed. Removing FlutterEngineGroup does not invalidate its existing created engines, but it cannot create more flutterEngines.

//src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java
public FlutterEngineGroup(@NonNullThe Context Context,@Nullable String[] dartVmArgs) {
  FlutterLoader loader = FlutterInjector.instance().flutterLoader();
  if(! loader.initialized()) { loader.startInitialization(context.getApplicationContext()); Loader. EnsureInitializationComplete (context, dartVmArgs); }}Copy the code

FlutterLoader’s startInitialization will load the Flutter engine’s native hangar.so to enable subsequent JNI calls. The lookup is also unpacked into dart resources in APK, and the method is called only once. This method calls the specific steps:

1) The Settings property is assigned to determine whether the method has been executed;

2), the method must be executed in the main thread, otherwise throw exception exit;

3) Get app context;

4) VsyncWaiter is an operation related to frame rate synchronization;

5) Record the initialization time;

6) Starting from Flutter2, initial configuration, resource initialization and loading the flutter. So dynamic library are all run in the backstage sub-thread, which accelerates the initialization speed.

//src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java
public void startInitialization(@NonNullThe Context applicationContext,@NonNull Settings settings) {
  // The initialization method can only be run once
  if (this.settings ! =null) {
    return;
  }
	// startInitialization must be called on the main thread
  if(Looper.myLooper() ! = Looper.getMainLooper()) {throw new IllegalStateException("startInitialization must be called on the main thread");
  }

  // Get the context of the app
  final Context appContext = applicationContext.getApplicationContext();

  this.settings = settings;

  initStartTimestampMillis = SystemClock.uptimeMillis();
	// Get the app information
  flutterApplicationInfo = ApplicationInfoLoader.load(appContext);
  VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE))
      .init();

  // Use background threads for initialization tasks that require disk access
  Callable<InitResult> initTask =
      new Callable<InitResult>() {
        @Override
        public InitResult call(a) {
					// Get the configuration resource
          ResourceExtractor resourceExtractor = initResources(appContext);
					// Load the Fluter local SO library
          flutterJNI.loadLibrary();
			
          Executors.newSingleThreadExecutor()
              .execute(
                  new Runnable() {
                    @Override
                    public void run(a) {
											// Preload the Skia font libraryflutterJNI.prefetchDefaultFontManager(); }});if(resourceExtractor ! =null) {
						// Wait until the initialization of the resource is complete before the downward execution, otherwise the block will continue
            resourceExtractor.waitForCompletion();
          }

          return newInitResult (PathUtils getFilesDir (appContext), PathUtils. GetCacheDirectory (appContext),  PathUtils.getDataDirectory(appContext)); }}; initResultFuture = Executors.newSingleThreadExecutor().submit(initTask); }Copy the code

5, initResources: Copy the resource files in apK to the application local file. Install the Flutter resource in DEBUG or JIT_RELEASE mode. The resource file decompression is performed asynchronously by ResourceExtractor. Finally, Dart resources vm_snapshot_data, isolate_snapshot_data, and kernel_blob.bin in ASSETS in APK will be installed in the application directory app_flutter.

private ResourceExtractor initResources(@NonNull Context applicationContext) {
  ResourceExtractor resourceExtractor = null;
  if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
		// Get the flutter data storage path
    final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
		// Get the package name
    final String packageName = applicationContext.getPackageName();
    final PackageManager packageManager = applicationContext.getPackageManager();
    final AssetManager assetManager = applicationContext.getResources().getAssets();
    resourceExtractor =
        new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);
    resourceExtractor
        .addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData))
        .addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData))
        .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));
    resourceExtractor.start();
  }
  return resourceExtractor;
}
Copy the code

6, when initializing startInitialization, also called ensureInitializationComplete initialization method confirmed to be completed, and so the file inside address to afferent FlutterJNI shellArgs, Therefore, we can modify the code generated by the flutter or replace the add method of List shellArgs by hook, so as to change the path of SO and carry out hot repair.

public void ensureInitializationComplete(
    @NonNullThe Context applicationContext,@Nullable String[] args) {
  if (initialized) {
    return;
  }
  if(Looper.myLooper() ! = Looper.getMainLooper()) {throw new IllegalStateException(
        "ensureInitializationComplete must be called on the main thread");
  }
  if (settings == null) {
    throw new IllegalStateException(
        "ensureInitializationComplete must be called after startInitialization");
  }
  try {
    InitResult result = initResultFuture.get();

    List<String> shellArgs = new ArrayList<>();

		// Omit the specific parameter configuration code...

    long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;

		// Initialize JNIFlutterJNI. Init (applicationContext, shellArgs. ToArray (new String[0]),
        kernelPath,
        result.appStoragePath,
        result.engineCachesPath,
        initTimeMillis);

    initialized = true;
  } catch(Exception e) {log.e (TAG,"Flutter initialization failed."E);throw newRuntimeException(e); }}Copy the code

FlutterJNI initialization

public void init(
      @NonNullThe Context Context,@NonNullString [] args,@NullableString bundlePath,@NonNullString appStoragePath,@NonNullString engineCachesPath,long initTimeMillis) {
    if(FlutterJNI initCalled) {Log. W (TAG,"FlutterJNI.init called more than once");
    }
		// Call the JNI flutter initialization methodNativeInit (Context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis); FlutterJNI.initCalled =true;
  }
Copy the code

After initializing the resources, the load of the flutter Engine is started. This is the result of the compilation of the source code for the Flutter Engine. When running, it is loaded into virtual memory by the Android virtual machine. (So is a standard ELF executable file with.data and.text sections, which contain data and instructions, respectively. After loading the flutter, the instructions can be executed by the CPU.) The JNI methods for FlutterMain, PlatformView, and VSyncWaiter are registered.

//src/flutter/shell/platform/android/library_loader.cc

JNIEXPORT jint JNI_OnLoad(JavaVM * vm,void* reserved) {
  // Start Java VM initialization by saving the current Java VM object into a global variable
  fml::jni::InitJavaVM(vm);

	// Associate the current thread with JavaVM
  JNIEnv* env = fml::jni::AttachCurrentThread(a);bool result = false;

  // register FlutterMain, which links native methods in the Java layer to methods in the C++ layer
  result = flutter::FlutterMain::Register(env);
  FML_CHECK(result);

  / / register PlatformView
  result = flutter::PlatformViewAndroid::Register(env);
  FML_CHECK(result);

  / / register VSyncWaiter.
  result = flutter::VsyncWaiterAndroid::Register(env);
  FML_CHECK(result);

  return JNI_VERSION_1_4;
}
Copy the code

After system initialization is completed, the native method NativeInit and the corresponding flutterMain. cc::Init method will be called. The initialization here generates a Settings object based on the parameters passed in.

// src/flutter/shell/platform/android/flutter_main.cc
void FlutterMain::Init(JNIEnv* env, jClass clazz, Jobject Context, jobjectArray Jargs, JString kernelPath, JString appStoragePath, Jstring engineCachesPath, jLong initTimeMillis) {
  std::vector<std::string> args;
  args.push_back("flutter");
  for (auto& arg : fml::jni::StringArrayToVector(env, jargs)) {args.push_back(std::move(arg));
  }
  auto command_line = fml::CommandLineFromIterators(args.beginThe args ().end());

  auto settings = SettingsFromCommandLine(command_line);

  int64_t init_time_micros = initTimeMillis * 1000;
  settings.engine_start_timestamp =
      std::chrono::microseconds(Dart_TimelineGetMicros() - init_time_micros);

  flutter::DartCallbackCache::SetCachePath(
      fml::jni::JavaStringToString(env, appStoragePath));

  fml::paths::InitializeAndroidCachesPath(
      fml::jni::JavaStringToString(env, engineCachesPath));

  flutter::DartCallbackCache::LoadCacheFromDisk(a);if(! flutter::DartVM::IsRunningPrecompiledCode() && kernelPath) {
    auto application_kernel_path =
        fml::jni::JavaStringToString(env, kernelPath);

    if (fml::IsFile(application_kernel_path)) {
      settings.application_kernel_asset = application_kernel_path;
    }
  }

  settings.task_observer_add = [](intptr_tClosure callback) {FML ::MessageLoop::GetCurrent().AddTaskObserver(key, STD: :move(callback));
  };

  settings.task_observer_remove = [](intptr_t key) {
    fml::MessageLoop::GetCurrent().RemoveTaskObserver(key);
  };

  settings.log_message_callback = [](constSTD: : string & tag,constSTD ::string& message) {__android_log_print(ANDROID_LOG_INFO, tag.c_str(),"%.*s", (int)message.size(), the message.c_str());
  };

#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
  auto make_mapping_callback = [](const uint8_t* the mapping,size_t size) {
    return[the mapping, the size] () {returnSTD: : make_unique < FML: : NonOwnedMapping > (mapping, size); }; }; settings.dart_library_sources_kernel =make_mapping_callback(kPlatformStrongDill kPlatformStrongDillSize);#endif  
	// Create the Flutter global variable
  g_flutter_main.reset(new FlutterMain(std::move(settings)));
  g_flutter_main->SetupObservatoryUriCallback(env);
}
Copy the code

From the args parsing process of the Settings in flutter_engine/shell/common/switches. The cc, here is the most important is the path of the snapshot build, construct the complete path is the process to initialize the copy to the local path, Finally, a FlutterMain object is generated and stored in a global static variable.

if (aot_shared_library_name.size(a) >0) {
    for (std::string_view name : aot_shared_library_name) {
      settings.application_library_path.emplace_back(name); }}else if (snapshot_asset_path.size(a) >0) {
    settings.vm_snapshot_data_path =
        fml::paths::JoinPaths({snapshot_asset_path, vm_snapshot_data_filename});
    settings.vm_snapshot_instr_path = fml::paths::JoinPaths({snapshot_asset_path, vm_snapshot_instr_filename}); settings.isolate_snapshot_data_path = fml::paths::JoinPaths({snapshot_asset_path, isolate_snapshot_data_filename}); settings.isolate_snapshot_instr_path = fml::paths::JoinPaths({snapshot_asset_path, isolate_snapshot_instr_filename}); }Copy the code

Afterword.

The above is mainly the initialization process of Flutter 2 FlutterEngineGroup. In the next section, we will start to learn the process of creating FlutterEngine and binding UI pages through FlutterEngineGroup.