Flutter打包成apk的产物
本文使用的是Android原生项目+Flutter Module 混合开发的模式,详见:将 Flutter 集成到现有应用
我们将项目打包成apk后,解压得到如下:
image.png其中,我们通过分析Flutter的源代码可知道:
libfutter.so:运行Flutter依赖so文件
libapp.so: 这里就是dart代码编译后的产物
flutter_asserts: 这里存放的项目中,Flutter模块用到包括图片、字体等资源
所以,如果这些产物能够动态替换,应该就能实现Android端热更新的功能。
Flutter在Android的运行分析
Flutter运行图查看FlutterEngine的构建函数,如下
public FlutterEngine(
@NonNull Context context,
@Nullable FlutterLoader flutterLoader,
@NonNull FlutterJNI flutterJNI,
@NonNull PlatformViewsController platformViewsController,
@Nullable String[] dartVmArgs,
boolean automaticallyRegisterPlugins,
boolean waitForRestorationData) {
AssetManager assetManager;
try {
assetManager = context.createPackageContext(context.getPackageName(), 0).getAssets();
} catch (NameNotFoundException e) {
assetManager = context.getAssets();
}
FlutterInjector injector = FlutterInjector.instance();
if (flutterJNI == null) {
flutterJNI = injector.getFlutterJNIFactory().provideFlutterJNI();
}
this.flutterJNI = flutterJNI;
this.dartExecutor = new DartExecutor(flutterJNI, assetManager);
this.dartExecutor.onAttachedToJNI();
DeferredComponentManager deferredComponentManager =
FlutterInjector.instance().deferredComponentManager();
accessibilityChannel = new AccessibilityChannel(dartExecutor, flutterJNI);
deferredComponentChannel = new DeferredComponentChannel(dartExecutor);
keyEventChannel = new KeyEventChannel(dartExecutor);
lifecycleChannel = new LifecycleChannel(dartExecutor);
localizationChannel = new LocalizationChannel(dartExecutor);
mouseCursorChannel = new MouseCursorChannel(dartExecutor);
navigationChannel = new NavigationChannel(dartExecutor);
platformChannel = new PlatformChannel(dartExecutor);
restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
settingsChannel = new SettingsChannel(dartExecutor);
systemChannel = new SystemChannel(dartExecutor);
textInputChannel = new TextInputChannel(dartExecutor);
if (deferredComponentManager != null) {
deferredComponentManager.setDeferredComponentChannel(deferredComponentChannel);
}
this.localizationPlugin = new LocalizationPlugin(context, localizationChannel);
if (flutterLoader == null) {
flutterLoader = injector.flutterLoader();
}
if (!flutterJNI.isAttached()) {
flutterLoader.startInitialization(context.getApplicationContext());
flutterLoader.ensureInitializationComplete(context, dartVmArgs);
}
flutterJNI.addEngineLifecycleListener(engineLifecycleListener);
flutterJNI.setPlatformViewsController(platformViewsController);
flutterJNI.setLocalizationPlugin(localizationPlugin);
flutterJNI.setDeferredComponentManager(injector.deferredComponentManager());
// It should typically be a fresh, unattached JNI. But on a spawned engine, the JNI instance
// is already attached to a native shell. In that case, the Java FlutterEngine is created around
// an existing shell.
if (!flutterJNI.isAttached()) {
attachToJni();
}
// TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if
// possible.
this.renderer = new FlutterRenderer(flutterJNI);
this.platformViewsController = platformViewsController;
this.platformViewsController.onAttachedToJNI();
this.pluginRegistry =
new FlutterEngineConnectionRegistry(context.getApplicationContext(), this, flutterLoader);
// Only automatically register plugins if both constructor parameter and
// loaded AndroidManifest config turn this feature on.
if (automaticallyRegisterPlugins && flutterLoader.automaticallyRegisterPlugins()) {
GeneratedPluginRegister.registerGeneratedPlugins(this);
}
}
我们发现有个关键方法:
flutterLoader.startInitialization(context.getApplicationContext());
flutterLoader.ensureInitializationComplete(context, dartVmArgs);
加载libfutter.so以及libapp.so就是在这里处理的,下面把相关的源码抠出来解释一下:
FlutterLoader.java
// 只截取关键代码 其他的代码省略...
// 声明的两个常量 看名字即可知道对应于哪个so文件
private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
private static final String DEFAULT_LIBRARY = "libflutter.so";
// 初始化libflutter.so的入口
public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
...
System.loadLibrary("flutter");
...
}
// 初始化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);
// 这里的 applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName
// 指的就是我们的so路径下的/libapp.so
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
}
...
initialized = true;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
我们再回过头来看FlutterEngine里的flutterLoader,发现他来着FlutterInjector类单例:
FlutterInjector injector = FlutterInjector.instance();
if (flutterLoader == null) {
flutterLoader = injector.flutterLoader();
}
动态替换so文件
通过以上分析,我们可以开始替换So文件里,本文通过实现FlutterLoader的子类,进而覆盖其ensureInitializationComplete,修改其加载so文件的路径,这样就完成so文件的替换了。
以下是参考代码
package com.ylzpay.androidtest.hotfix
import android.content.Context
import io.flutter.FlutterInjector
import io.flutter.Log
import io.flutter.embedding.engine.loader.FlutterApplicationInfo
import io.flutter.embedding.engine.loader.FlutterLoader
import java.io.File
class YHFlutterLoader : FlutterLoader() {
companion object {
///完成对FlutterInjector单例的重置,使其属性flutterLoader指向我们的子类
fun activation() {
//这里直接使用默认构造函数,与FlutterInjector类中初始化flutterLoader效果是一样的(详见FlutterInjector的fillDefaults()方法)
val flutterLoader: YHFlutterLoader = YHFlutterLoader()
val flutterInjector = FlutterInjector.Builder().setFlutterLoader(flutterLoader).build()
//重置FlutterInjector单例
FlutterInjector.reset()
FlutterInjector.setInstance(flutterInjector)
Log.i("------", "已重置FlutterInjector单例")
}
}
//返回准备好的热更新包的路径(本文方案是从服务端下载到zip文件并解压放置到这个路径)
private fun getHotAppBundlePath(applicationContext: Context): String {
return applicationContext.filesDir.absolutePath + File.separator + "hot/lib/libapp.so";
}
override fun ensureInitializationComplete(
applicationContext: Context,
args: Array<out String>?
) {
super.ensureInitializationComplete(applicationContext, args)
val soFile: File = File(getHotAppBundlePath(applicationContext))
if (soFile.exists()) {
try {
//1.拿到flutterApplicationInfo字段
val flutterApplicationInfoField = FlutterLoader::class.java.getDeclaredField("flutterApplicationInfo")
flutterApplicationInfoField.isAccessible = true
val flutterApplicationInfo = flutterApplicationInfoField[this] as FlutterApplicationInfo
Log.i(
"========",
"--aot-shared-library-name=" + flutterApplicationInfo.nativeLibraryDir + flutterApplicationInfo.aotSharedLibraryName
)
//2.拿到aotSharedLibraryName修改路径
val aotSharedLibraryNameField =
FlutterApplicationInfo::class.java.getDeclaredField("aotSharedLibraryName")
aotSharedLibraryNameField.isAccessible = true
aotSharedLibraryNameField[flutterApplicationInfo] = soFile.absolutePath
Log.i(
"========",
"--aot-shared-library-name=" + flutterApplicationInfo.nativeLibraryDir + flutterApplicationInfo.aotSharedLibraryName
)
super.ensureInitializationComplete(applicationContext, args)
} catch (e: Exception) {
e.printStackTrace()
e.message?.let { Log.e("----", it) }
}
} else {
Log.i("----", "load fail. 补丁不存在")
}
}
}
调用
在继承FlutterActivity的子Activity的onCreate方法中,提前调用YHFlutterLoader.activation()
或者,Application中调用
class App : Application() {
override fun onCreate() {
super.onCreate()
//激活YHFlutterLoader
YHFlutterLoader.activation()
}
}
到此第一步替换so文件完成。
动态替换资源
在flutter中我们会把图片资源放在一个images目录下并注册声明完后,通常的使用方式:
AssetImage("images/icon.png")
通过查看源码可以找到最终是走到AssetBundle类中去,最终是由它的子类比如PlatformAssetBundle进行加载,而这个AssetBundle我们可以自己指定是要系统默认的还是自己实现的,所以这里可以通过自定义AssetBundle从而实现加载我们下载目录下images中的相关图片资源。实现HotAssetBundle如下:
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
class HotAssetBundle extends PlatformAssetBundle {
HotAssetBundle() {
/// 这里是自己下载成功的图片资源路径
dataPath = "/data/data/com.ylzpay.androidtest/files/hot/flutter_assets";
debugPrint("-------------- HsaAssetBundle资源存放地址 = $dataPath");
}
/// 路径拼接前缀 Android = /data/data/xxx.xxx.xxx/cache
String dataPath = "";
@override
Future<ByteData> load(String key) async {
final ByteData? asset;
debugPrint("try load file : $dataPath/$key");
File file = File("$dataPath/$key");
if(file.existsSync()){
debugPrint("load file success! ${file.path}");
Uint8List bytes = await file.readAsBytes();
asset = bytes.buffer.asByteData();
}else{
debugPrint("load file faile!");
asset = await super.load(key);
}
return asset;
}
}
最后一步就是把这个我们自定义的AssetBundle配置使用,替换默认的PlatformAssetBundle,具体使用如下:
runApp(
Container(
child: DefaultAssetBundle(
bundle: HotAssetBundle(),
child: MaterialApp(
...
))
)
);
其他
工程目录下原有的so文件以及flutter_assert也可以移除掉,这样子能真减少apk大小,在自己的build.gradle进行配置:
// 移除Flutter相关的so文件 采用动态下发
exclude 'lib/xxxx/libapp.so'
exclude 'lib/xxxx/libflutter.so'
variant.mergeAssets.doLast {
//删除assets文件夹下的flutter_assets 采用动态下发
delete(fileTree(dir: variant.mergeAssets.outputDir, includes: ['flutter_assets', 'flutter_assets/**']))
}
网友评论