It has been some time since the release of Flutter. When the Flutter module is embedded in a project, it is obvious that it brings a lot of package size to APK. Besides the source code introduced with the Flutter SDK, these “artifacts” are also visible below.Therefore, if these products can be delivered dynamically, they can not only reduce the package size but also give their business code the ability to hot update, which is a kind of double effect.

Because:

Libfutter. so: Run the Flutter dependency so file

Libapp. so: This is the compiled dart code

Flutter_implies: This store stores the resources used in the project

Here, we directly put these products according to their favorite directory into a ZIP package, and then upload the server; Finally, you only need to add a logic in your own project to download the ZIP package. It is recommended that you download it to the data/data path because the SD card permission may be closed.

The following logic is implemented after the zip package is downloaded successfully:

Dynamically replace the so file

To find out how to replace the so file, look at the source code: loading libfutter.so and libapp.so into the SDK provided by flutter is handled in the FlutterLoader file.

FlutterLoader.java
// Select only key code and omit other code...

// The two constants that are declared correspond to the so file
private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
private static final String DEFAULT_LIBRARY = "libflutter.so";

// Initialize the entry to libflutter. So
public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {... System.loadLibrary("flutter"); . }// Initialize the entry to libapp.so
public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {...try {
        String kernelPath = null;
        if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
        	...
        } else {
        	// aotSharedLibraryName = "libapp.so";
            shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);
			/ / here applicationInfo. NativeLibraryDir + File. The separator + aotSharedLibraryName
			// refers to our so path /libapp.so
            shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "="+ applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName); }... initialized =true;
    } catch (Exception e) {
        throw newRuntimeException(e); }}Copy the code

According to the source, we know that if you want to replace the corresponding dynamic so file is start here, and then take a look at the FlutterLoader. Java statement:

    public static FlutterLoader getInstance(a) {
        if (instance == null) {
            instance = new FlutterLoader();
        }
        return instance;
    }
Copy the code

Turned out to be a singleton, so do it as long as change a good and the source is not much, so I practice is a custom class implements FlutterLoader. Java:

// Only the key code is written here, the rest is omitted
public class MFlutterLoader extends FlutterLoader {
	private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
    private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data";

    /** * libapp.so file */
    private File aotSharedLibraryFile;
    /** * libflutter. So path */
    private String flutterSoStr;

    public void setAotSharedLibrarySo(File soFile) {
        aotSharedLibraryFile = soFile;
    }

    public void setFlutterSoStr(String soPath) {
        flutterSoStr = soPath;
    }

	// Initialize the libflutter. So entry modification
	public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {...// Load the so file if there is a path value passed to libflutter
        if (!TextUtils.isEmpty(flutterSoStr)) {
            System.load(flutterSoStr);
        } 
        ...
    }

// Initialize the libapp.so entry modification
public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {...try{...// If the passed libapp.so file exists
            // Replace the original path to read so /libapp.so with the path we passed
            if (null! = aotSharedLibraryFile && aotSharedLibraryFile.exists() && aotSharedLibraryFile.isFile() && aotSharedLibraryFile.canRead() && aotSharedLibraryFile.length() >0) {
                shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getName());
                shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "="+ aotSharedLibraryFile.getAbsolutePath()); }}catch (Exception e) {
            throw new RuntimeException(e);
        }

    /** * Replace FlutterLoader with our custom MFlutterLoader */
    public void hookFlutterLoaderIfNecessary(a) {
        try {
            if(! flutterLoaderHookedSuccess()) { MFlutterLoader instance = MFlutterLoader.getInstance(); writeStaticField(FlutterLoader.class,"instance", instance); }}catch(Throwable error) { ... }}private static void writeStaticField(finalClass<? > cls,final String fieldName, final Object value) throws Exception {
        final Field field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(null, value); }}Copy the code

It can be seen that the first step to replace the SO file is relatively convenient, but the specific use of the time need to pay attention to the reflection and if the replacement failed logic.

Dynamic resource replacement

In Flutter we place image resources in an images directory and declare them in the usual way:

AssetImage("images/icon.png")
Copy the code

If you look at the source code, you can see that it actually goes into the AssetBundle class, and it’s loaded by its subclasses like PlatformAssetBundle, and this AssetBundle we can specify whether we want it to be the default or the implementation, So we can customize the AssetBundle to load the image resources in our images download directory.

Here is my custom AssetBundle:

class HotAssetBundle extends CachingAssetBundle {

  HotAssetBundle() {
  	/// here is the image resource path of the successful download
    dataPath = ""
    LogUtil.d("-------------- HotAssetBundle = $dataPath");
  }

  / / / path joining together the prefix Android = / data/data/XXX. XXX. XXX/cache
  String dataPath = "";

  @override
  Future<ByteData> load(String key) async {
    LogUtil.d("======== HotAssetBundle start load = $key");
    if (key == "AssetManifest.json") {
      LogUtil.d("======== HotAssetBundle start AssetManifest load =====");

      /// key = AssetManifest.json
      File jsonFile = File("$dataPath/AssetManifest.json");
      Uint8List bytes = await jsonFile.readAsBytes();
      ByteData jsonByteData = bytes.buffer.asByteData();
      return jsonByteData;
    }
    if (key == "FontManifest.json") {
      LogUtil.d("======== HotAssetBundle start FontManifest load =====");

      /// key = FontManifest.json
      File jsonFile = File("$dataPath/FontManifest.json");
      Uint8List bytes = await jsonFile.readAsBytes();
      ByteData jsonByteData = bytes.buffer.asByteData();
      return jsonByteData;
    }

    String dir = "$dataPath/";

    /// key = packages/xxx/images/icon.png
    LogUtil.d("======== HotAssetBundle key = $key");
    File file = File("$dir$key");
    LogUtil.d("======== HotAssetBundle file = ${file.path}");
    Uint8List bytes = await file.readAsBytes();
    ByteData byteData = bytes.buffer.asByteData();
    returnbyteData; }}Copy the code

Main processing is based on the incoming key and then load the corresponding files, it is important to note, there are two special key: FontManifest. Json, AssetManifest. Json, look at the original mainly analyzes and gets the corresponding key – value format of the data.

The last step is to use our custom AssetBundle configuration instead of the default PlatformAssetBundle:

runApp(
      Container(
        child: DefaultAssetBundle(
	      bundle: HotAssetBundle(),
	      child:MaterialApp( ... ) )));Copy the code

In order to reduce apK size, you need to configure the project directory to remove the so file and flutter_assert.

        // Remove so files associated with Flutter using dynamic delivery
        exclude 'lib/xxxx/libapp.so'
        exclude 'lib/xxxx/libflutter.so'

        variant.mergeAssets.doLast {
            // Delete flutter_assets in the assets folder. Dynamic delivery is adopted
            delete(fileTree(dir: variant.mergeAssets.outputDir, includes: ['flutter_assets'.'flutter_assets/**'))}Copy the code

The last By contrast, adding this dynamic delivery can reduce the packet size of APK considerably.

Here’s a summary:

  1. Since entering the Flutter initialization process directly before libflutter. So and libapp.so have been downloaded will cause an error, we need to add additional logic to the Flutter initialization process until they have been downloaded.
  2. Things like libflutter. So are generally updated only with version updates and do not need to be downloaded with each update, so it is faster to download each update with a separate download package than with each update.