轉載請註明出處: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的熱更新探索就先結束了,我將在下篇文章:Flutter源碼系列之《一》Flutter的熱更新探索(下) 中講解另一種實現熱修復的方式,最後感謝收看(#.#),測試代碼: https://github.com/llew2011/flutter_hotfix