转载请注明出处: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()方法核心功能有三个:
- 初始化aotSharedLibraryFile
- 替换FlutterLoader的instance实例
- 执行初始化操作
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