Flutter源码系列之《一》Flutter的热更新探索(下)

转载请注明出处:https://blog.csdn.net/llew2011/article/details/104075883

在上篇文章Flutter源码系列之<一>Flutter的热更新探索(上)我们分析了Flutter的加载流程,找到了实现热更新的方法,接下来我们开始实现热更新功能。考虑到Google可能会在后续版本中对FlutterLoader类做修改,因此我们先定义一个适配版本,代码如下:

public enum FlutterVersion {
    /**
     * Flutter Version: 1.14.0
     */
    VERSION_011400
}

VERSION_011400表示1.14.0版本,这代表着我们热更新适配从1.14.0版本开始,定义完版本后我们还要定义与之相对应的加载器FlutterLoaderV011400,然后在定义一个FlutterManager,它是替代FlutterMain的,代码如下:

public class FlutterManager {

    private static final String TAG = "FlutterManager";

    public static void startInitialization(Context context) {
        startInitialization(context, null, FlutterVersion.VERSION_011400);
    }

    public static void startInitialization(Context context, File aotFile, FlutterVersion version) {
        startInitialization(context, aotFile, version, new FlutterMain.Settings());
    }

    public static void startInitialization(Context context, File aotFile, FlutterVersion version, FlutterMain.Settings settings) {
        ensureInitializeOnMainThread();
        FlutterCallback flutterCallback = generateFlutterCallback(version);
        if (null != flutterCallback) {
            flutterCallback.startInitialization(context, aotFile, getFlutterLoaderSettings(settings));
        } else {
            FlutterLogger.w(TAG, "Flutter Version not supported: " + version);
            FlutterMain.startInitialization(context);
        }
    }

    private static void ensureInitializeOnMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalStateException("startInitialization must be called on the main thread");
        }
    }

    private static FlutterLoader.Settings getFlutterLoaderSettings(FlutterMain.Settings settings) {
        FlutterLoader.Settings setting = new FlutterLoader.Settings();
        if (null != settings) {
            setting.setLogTag(settings.getLogTag());
        }
        return setting;
    }

    private static FlutterCallback generateFlutterCallback(FlutterVersion version) {
        if (FlutterVersion.VERSION_011400 == version) {
            return FlutterLoaderV011400.getInstance();
        }
        return null;
    }

    public interface FlutterCallback {
        void startInitialization(Context context, File aotFile, FlutterLoader.Settings settings);
    }
}

FlutterManager的职责是代替FlutterMain进行Flutter引擎的初始化等操作,它提供了一系列startInitialization()方法,这些方法最终执行的是含有4个参数的startInitialization()方法,它的执行流程首先检验时候运行在主线程,如果不是则抛异常,然后调用generateFlutterCallback()方法获取一个FlutterCallback实例,FlutterCallback为了适配后续Flutter版本变更添加的一个接口,如果获取到FlutterCallback就执行其startInitialization()方法否则执行FlutterMain的默认初始化流程,由于我们目前仅支持1.14.0版本,所以返回了一个FlutterLoaderV011400实例,FlutterLoaderV011400是和VERSION_011400对应的加载类,其源码如下:

/**
 * Flutter Version: 1.14.0
 */
public class FlutterLoaderV011400 extends FlutterLoader implements FlutterManager.FlutterCallback {

    private static final String TAG = "FlutterLoader";

    // Must match values in flutter::switches
    private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name";
    private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path";
    private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data";
    private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data";
    private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir";

    // XML Attribute keys supported in AndroidManifest.xml
    private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME =
            FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME;
    private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY =
            FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY;
    private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY =
            FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY;
    private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY =
            FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY;

    // Resource names used for components of the precompiled snapshot.
    private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
    private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data";
    private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data";
    private static final String DEFAULT_LIBRARY = "libflutter.so";
    private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin";
    private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets";

    // Mutable because default values can be overridden via config properties
    private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME;
    private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA;
    private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA;
    private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR;


    private static FlutterLoaderV011400 instance;
    /**
     * Returns a singleton {@code FlutterLoader} instance.
     * <p>
     * The returned instance loads Flutter native libraries in the standard way. A singleton object
     * is used instead of static methods to facilitate testing without actually running native
     * library linking.
     */
    public static FlutterLoaderV011400 getInstance() {
        if (instance == null) {
            instance = new FlutterLoaderV011400();
        }
        return instance;
    }


    private boolean initialized = false;
    private ResourceExtractor resourceExtractor;
    private Settings settings;
    /**
     * Starts initialization of the native system.
     * @param applicationContext The Android application context.
     */
    public void startInitialization(Context applicationContext) {
        startInitialization(applicationContext, new Settings());
    }

    /**
     * Starts initialization of the native system.
     * <p>
     * This loads the Flutter engine's native library to enable subsequent JNI calls. This also
     * starts locating and unpacking Dart resources packaged in the app's APK.
     * <p>
     * Calling this method multiple times has no effect.
     *
     * @param applicationContext The Android application context.
     * @param settings Configuration settings.
     */
    public void startInitialization(Context applicationContext, Settings settings) {
        FlutterLogger.i(TAG, "FlutterEngine start initialization.");
        // Do not run startInitialization more than once.
        if (this.settings != null) {
            FlutterLogger.i(TAG, "FlutterEngine already initialized.");
            return;
        }
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalStateException("startInitialization must be called on the main thread");
        }

        this.settings = settings;

        // Ensure that the context is actually the application context.
        applicationContext = applicationContext.getApplicationContext();


        long initStartTimestampMillis = SystemClock.uptimeMillis();
        initConfig(applicationContext);
        initResources(applicationContext);

        System.loadLibrary("flutter");

        VsyncWaiter
                .getInstance((WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE))
                .init();

        // We record the initialization time using SystemClock because at the start of the
        // initialization we have not yet loaded the native library to call into dart_tools_api.h.
        // To get Timeline timestamp of the start of initialization we simply subtract the delta
        // from the Timeline timestamp at the current moment (the assumption is that the overhead
        // of the JNI call is negligible).
        long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
        FlutterJNI.nativeRecordStartTimestamp(initTimeMillis);
        FlutterLogger.i(TAG, "FlutterEngine finish initialization.");
    }

    /**
     * Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background
     * thread, then invoking {@code callback} on the {@code callbackHandler}.
     */
    public void ensureInitializationCompleteAsync(
            Context applicationContext,
            String[] args,
            Handler callbackHandler,
            Runnable callback
    ) {
        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");
        }
        if (initialized) {
            callbackHandler.post(callback);
            return;
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (resourceExtractor != null) {
                    resourceExtractor.waitForCompletion();
                }
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        ensureInitializationComplete(applicationContext.getApplicationContext(), args);
                        callbackHandler.post(callback);
                    }
                });
            }
        }).start();
    }

    private ApplicationInfo getApplicationInfo(Context applicationContext) {
        try {
            return applicationContext
                    .getPackageManager()
                    .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Initialize our Flutter config values by obtaining them from the
     * manifest XML file, falling back to default values.
     */
    private void initConfig(Context applicationContext) {
        Bundle metadata = getApplicationInfo(applicationContext).metaData;

        // There isn't a `<meta-data>` tag as a direct child of `<application>` in
        // `AndroidManifest.xml`.
        if (metadata == null) {
            return;
        }

        aotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME);
        flutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR);

        vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA);
        isolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA);
    }

    /**
     * Extract assets out of the APK that need to be cached as uncompressed
     * files on disk.
     */
    private void initResources(Context applicationContext) {
        new ResourceCleaner(applicationContext).start();

        if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
            final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
            final String packageName = applicationContext.getPackageName();
            final PackageManager packageManager = applicationContext.getPackageManager();
            final AssetManager assetManager = applicationContext.getResources().getAssets();
            resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);

            // In debug/JIT mode these assets will be written to disk and then
            // mapped into memory so they can be provided to the Dart VM.
            resourceExtractor
                    .addResource(fullAssetPathFrom(vmSnapshotData))
                    .addResource(fullAssetPathFrom(isolateSnapshotData))
                    .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));

            resourceExtractor.start();
        }
    }

    public String findAppBundlePath() {
        return flutterAssetsDir;
    }

    /**
     * Returns the file name for the given asset.
     * The returned file name can be used to access the asset in the APK
     * through the {@link AssetManager} API.
     *
     * @param asset the name of the asset. The name can be hierarchical
     * @return      the filename to be used with {@link AssetManager}
     */
    public String getLookupKeyForAsset(String asset) {
        return fullAssetPathFrom(asset);
    }

    /**
     * Returns the file name for the given asset which originates from the
     * specified packageName. The returned file name can be used to access
     * the asset in the APK through the {@link AssetManager} API.
     *
     * @param asset       the name of the asset. The name can be hierarchical
     * @param packageName the name of the package from which the asset originates
     * @return            the file name to be used with {@link AssetManager}
     */
    public String getLookupKeyForAsset(String asset, String packageName) {
        return getLookupKeyForAsset(
                "packages" + File.separator + packageName + File.separator + asset);
    }

    private String fullAssetPathFrom(String filePath) {
        return flutterAssetsDir + File.separator + filePath;
    }



    // *************************************************** hot fix code start  ***************************************************//

    private static final String FIELD_NAME = "instance";

    private File aotSharedLibraryFile;

    /**
     * Blocks until initialization of the native system has completed.
     * <p>
     * Calling this method multiple times has no effect.
     *
     * @param applicationContext The Android application context.
     * @param args Flags sent to the Flutter runtime.
     */
    public void ensureInitializationComplete(Context applicationContext, String[] args) {
        FlutterLogger.i(TAG, "ensure initialization complete.");
        if (initialized) {
            FlutterLogger.i(TAG, "initialization already completed.");
            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 {
            if (resourceExtractor != null) {
                FlutterLogger.i(TAG, "wait for resourceExtractor complete.");
                resourceExtractor.waitForCompletion();
            }

            List<String> shellArgs = new ArrayList<>();
            shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");

            ApplicationInfo applicationInfo = getApplicationInfo(applicationContext);
            shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY);

            if (args != null) {
                Collections.addAll(shellArgs, args);
            }

            String kernelPath = null;
            if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
                FlutterLogger.i(TAG, "build in DEBUG or JIT_RELEASE model.");
                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);
            } else {
                // replace libapp.so fie here if aotSharedLibraryFile is valid

                FlutterLogger.i(TAG, "build in RELEASE model.");
                if (null != aotSharedLibraryFile
                        && aotSharedLibraryFile.exists()
                        && aotSharedLibraryFile.isFile()
                        && aotSharedLibraryFile.canRead()
                        && aotSharedLibraryFile.length() > 0) {
                    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getName());

                    // Most devices can load the AOT shared library based on the library name
                    // with no directory path.  Provide a fully qualified path to the library
                    // as a workaround for devices where that fails.
                    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getAbsolutePath());

                    FlutterLogger.i(TAG, "initialize with fixed file: " + aotSharedLibraryFile.getAbsolutePath());
                } else {
                    // aotSharedLibraryFile is not valid, and use origin file here

                    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);

                    // Most devices can load the AOT shared library based on the library name
                    // with no directory path.  Provide a fully qualified path to the library
                    // as a workaround for devices where that fails.
                    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);

                    FlutterLogger.i(TAG, "initialize with origin file: " + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
                }
            }

            shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext));
            if (settings.getLogTag() != null) {
                shellArgs.add("--log-tag=" + settings.getLogTag());
            }

            String appStoragePath = PathUtils.getFilesDir(applicationContext);
            String engineCachesPath = PathUtils.getCacheDirectory(applicationContext);
            FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]),
                    kernelPath, appStoragePath, engineCachesPath);

            initialized = true;
            FlutterLogger.i(TAG, "initialization complete.");
        } catch (Exception e) {
            FlutterLogger.e(TAG, "initialization failed: " + e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public void startInitialization(Context context, File aotFile, Settings settings) {
        aotSharedLibraryFile = aotFile;
        hookFlutterLoaderIfNecessary();
        FlutterLoader.getInstance().startInitialization(context, settings);
    }

    private void hookFlutterLoaderIfNecessary() {
        try {
            if (!flutterLoaderHookedSuccess()) {
                FlutterLogger.i(TAG, "FlutterLoader hook start.");
                FlutterLoaderV011400 instance = FlutterLoaderV011400.getInstance();
                FieldUtils.writeStaticField(FlutterLoader.class, FIELD_NAME, instance);
                FlutterLogger.i(TAG, "FlutterLoader hook finish.");

                if (flutterLoaderHookedSuccess()) {
                    FlutterLogger.i(TAG, "FlutterLoader hook success.");
                } else {
                    FlutterLogger.i(TAG, "FlutterLoader hook failure.");
                }
            } else {
                FlutterLogger.i(TAG, "FlutterLoader already hooked.");
            }
        } catch (Throwable error) {
            FlutterLogger.w(TAG, "FlutterLoader hook " + (flutterLoaderHookedSuccess() ? "success" : "failure") + " and error occured: " + error);
        }
    }

    private boolean flutterLoaderHookedSuccess() {
        return FlutterLoader.getInstance() instanceof FlutterLoaderV011400;
    }

    // *************************************************** hot fix code finish ***************************************************//
}

FlutterLoaderV011400继承FlutterLoader并实现了FlutterCallback接口,它的代码除了从父类FlutterLoader拷贝过来外我们又给FlutterLoaderV011400添加了File类型的aotSharedLibraryFile属性,aotSharedLibraryFile表示需要Flutter引擎加载我们指定的so文件,它的初始化在FlutterCallback的startInitialization()方法中进行的,startInitialization()方法核心功能有三个:

  1. 初始化aotSharedLibraryFile
  2. 替换FlutterLoader的instance实例
  3. 执行初始化操作

FlutterLoaderV011400在重写的ensureInitializationComplete()方法内在加载libapp.so文件的时候做了校验,如果aotSharedLibraryFile校验成功则加载aotSharedLibraryFile文件,否则加载aotSharedLibraryName对应的文件,代码如下:

if (null != aotSharedLibraryFile
        && aotSharedLibraryFile.exists()
        && aotSharedLibraryFile.isFile()
        && aotSharedLibraryFile.canRead()
        && aotSharedLibraryFile.length() > 0) {
    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getName());

    // Most devices can load the AOT shared library based on the library name
    // with no directory path.  Provide a fully qualified path to the library
    // as a workaround for devices where that fails.
    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getAbsolutePath());

    FlutterLogger.i(TAG, "initialize with fixed file: " + aotSharedLibraryFile.getAbsolutePath());
} else {
    // aotSharedLibraryFile is not valid, and use origin file here

    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);

    // Most devices can load the AOT shared library based on the library name
    // with no directory path.  Provide a fully qualified path to the library
    // as a workaround for devices where that fails.
    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);

    FlutterLogger.i(TAG, "initialize with origin file: " + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
}

由于我们把FlutterLoader的instance实例替换成FlutterLoaderV011400的实例后,在后续调用FlutterLoader的getInstance()方法时返回的都是替换后的FlutterLoaderV011400实例,所以相关方法的调用都是执行FlutterLoaderV011400的相关方法,通过以上操作我们就实现了Flutter代码的热修复。

现在我们修改测试工程,新添加一个页面并修改我们首页的累加值,打包后分离出libapp.so文件并把修复后的文件导入,测试结果如下:
在这里插入图片描述
运行结果达到预期,目前是使用的Flutter1.14.0版本的FlutterLoader代码,为了防止FlutterLoader的实现做了更改,我们可以时常关注FlutterLoader的提交记录:https://github.com/flutter/engine/commits/master/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java
在这里插入图片描述
最近有查看了FlutterLoader的更新历史,发现修改了两个bug,因此我们直接修改下我们的FlutterLoaderV011400代码即可:
在这里插入图片描述
另外需要注意的是,由于我们是使用反射技术替换FlutterLoader的instance实例,如果在打包过程中把FlutterLoader混淆过了,就会导致找不到instance属性从而修复失败,所以不要混淆FlutterLoader类:

-keep class io.flutter.** {
   *;
}

由于篇幅原因,Flutter的热更新在实际项目里使用还会牵涉到以下几个常见技术点:

  • 文件下发
    不同平台下发不同的so文件
  • 文件校验
    保证加载的so文件是完整的
  • 增量更新
    避免下发的so文件过大

有关Flutter的热更新探索就结束了,感谢收看(#.#),测试代码: https://github.com/llew2011/flutter_hotfix

发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/104075883