温馨提示
请滑动到文章末尾,长按识别「抽奖」小程序,每日现金红包。
作者:左手木亽
链接:
https://blog.csdn.net/Neacy_Zz/article/details/84636738
本文由作者授权发布。
前言
Flutter 是 Google 推出的可以高效构建 Android、IOS 界面的移动 UI 框架,在国内中的大公司像闲鱼 /Now 直播等 app 陆续出现它的影子,当然闲鱼的最为成熟,闲鱼也非常的高效产出了很多优秀的文章。
本文是基于Flutter SDK :0.7.3
在最新的SDK v0.11.13中或者说运行后发现没有PathProviderPlugin / SharedPreferencesPlugin 对应的目录以及jar包,那是因为新版本中已经不需要了 自然就可以删除。
可是
可是,网上能找到的混合开发方案或者动态更新flutter的相关文章都没法符合我自己理想的效果。所以自己摸索了一套混合开发和动态更新的方案,这里记录一下摸索过程。
Flutter源码分析
如果说把自家的 app 改造成纯 Flutter 方案那是不可能的,顶多是某个模块或者某些模块改成 Flutter,所以自然想到 Flutter 如何跟原生混合开发,混合开发不是说 java 去调用 dart 中的方法更多的是指如何从当前 Activity 跳转到 Flutter 实现的界面,要像知道这些东西那么必须得弄懂 Flutter 源码,不求深入但求知之一二三四。
Android 的应用那么自然先找 Application,所以很快找到了FlutterApplication:
public class FlutterApplication extends Application {
private Activity mCurrentActivity = null;
public FlutterApplication() {
}
@CallSuper
public void onCreate() {
super.onCreate();
FlutterMain.startInitialization(this);
}
public Activity getCurrentActivity() {
return this.mCurrentActivity;
}
public void setCurrentActivity(Activity mCurrentActivity) {
this.mCurrentActivity = mCurrentActivity;
}
}
还行初始化的东西不多,直接进入 onCreate 对应的FlutterMain.startInitialization
中去看看:
public static void startInitialization(Context applicationContext, FlutterMain.Settings settings) {
long initStartTimestampMillis = SystemClock.uptimeMillis();
initConfig(applicationContext);
initAot(applicationContext);
initResources(applicationContext);
System.loadLibrary("flutter");
long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
nativeRecordStartTimestamp(initTimeMillis);
}
不具体一行一行的看代码,但是看到了几个很关键的词在initConfig
方法中:
private static void initConfig(Context applicationContext) {
Bundle metadata = applicationContext.getPackageManager().getApplicationInfo(applicationContext.getPackageName(), 128).metaData;
if (metadata != null) {
sAotSharedLibraryPath = metadata.getString(PUBLIC_AOT_AOT_SHARED_LIBRARY_PATH, "app.so");
sAotVmSnapshotData = metadata.getString(PUBLIC_AOT_VM_SNAPSHOT_DATA_KEY, "vm_snapshot_data");
sAotVmSnapshotInstr = metadata.getString(PUBLIC_AOT_VM_SNAPSHOT_INSTR_KEY, "vm_snapshot_instr");
sAotIsolateSnapshotData = metadata.getString(PUBLIC_AOT_ISOLATE_SNAPSHOT_DATA_KEY, "isolate_snapshot_data");
sAotIsolateSnapshotInstr = metadata.getString(PUBLIC_AOT_ISOLATE_SNAPSHOT_INSTR_KEY, "isolate_snapshot_instr");
sFlx = metadata.getString(PUBLIC_FLX_KEY, "app.flx");
sSnapshotBlob = metadata.getString(PUBLIC_SNAPSHOT_BLOB_KEY, "snapshot_blob.bin");
sFlutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, "flutter_assets");
}
}
没错就是vm_snapshot_data、vm_snapshot_instr、=
isolate_snapshot_data、isolate_snapshot_instr
为什么说这几个这么重要呢?
看下上面这几个编译的产物,我们就知道这就 Flutter 的核心东西。或者换句话说只要弄懂了这个玩意很有可能我们就悟出混合开发的方案了,那么他们是怎么读取 assets 目录下的这些玩意呢?
private static void initAot(Context applicationContext) {
Set<String> assets = listAssets(applicationContext, "");
sIsPrecompiledAsBlobs = assets.containsAll(Arrays.asList(sAotVmSnapshotData, sAotVmSnapshotInstr, sAotIsolateSnapshotData, sAotIsolateSnapshotInstr));
sIsPrecompiledAsSharedLibrary = assets.contains(sAotSharedLibraryPath);
if (sIsPrecompiledAsBlobs && sIsPrecompiledAsSharedLibrary) {
throw new RuntimeException("Found precompiled app as shared library and as Dart VM snapshots.");
}
}
看到方法跟 Assets 挂钩确实很惊喜,因为看到肯定是从 Assets 中把这些读出来的。可是读出来放哪里去?
那最后的那个方法 initResources 该方法就是涉及存放的位置,跟着源码一路看下去,在 ExtractTask.extractResources 找到了一点猫腻:
File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext));
确实,就是在 data/data/xxx/flutter_assets/ 路径下:
大体知道了这些个产物之后,界面是怎么加载?首先加载 Flutter 的界面是个Activity 叫 FlutterActivity 主要是通过 FlutterActivityDelegate 这个类,然后我们主要看:
FlutterActivity.onCreate => FlutterActivityDelegate.onCreate
这个流程:
public void onCreate(Bundle savedInstanceState) {
// 沉浸式模式
if (VERSION.SDK_INT >= 21) {
Window window = this.activity.getWindow();
window.addFlags(-2147483648);
window.setStatusBarColor(1073741824);
window.getDecorView().setSystemUiVisibility(1280);
}
String[] args = getArgsFromIntent(this.activity.getIntent());
FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
this.flutterView = this.viewFactory.createFlutterView(this.activity);
if (this.flutterView == null) {
FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
this.flutterView.setLayoutParams(matchParent);
this.activity.setContentView(this.flutterView);
this.launchView = this.createLaunchView();
if (this.launchView != null) {
this.addLaunchView();
}
}
}
所以界面最重要的方法就是 ensureInitializationComplete 也就是把flutter 相关的初始化进来然后使用 FlutterView 进行加载显示:
ensureInitializationComplete:// 进行初始化
String appBundlePath = findAppBundlePath(applicationContext);
String appStoragePath = PathUtils.getFilesDir(applicationContext);
nativeInit(applicationContext, (String[])shellArgs.toArray(new String[0]), appBundlePath, appStoragePath);
// 找到data/data/xxx/flutter_assets下的flutter产物
public static String findAppBundlePath(Context applicationContext) {
String dataDirectory = PathUtils.getDataDirectory(applicationContext);
File appBundle = new File(dataDirectory, sFlutterAssetsDir);
return appBundle.exists() ? appBundle.getPath() : null;
}
然后每一个 FlutterView 中包了一个 FlutterNativeView 然后最终就是FlutterView->runFromBundle 调用:
FlutterNativeView->runFromBundle
最后渲染到界面上。
到此我们大概了解了Flutter需要的产物vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr然后简单的了解了加载流程,最后附上大闲鱼的一张编译大图:
混合开发
所以我觉得 Flutter 应该跟 ReactNative 类似只要把相关的 bundle 文件放入我们 app 的 assets 即可,所以拿这个方向开始编译 Flutter 代码,开开心心的输入 flutter run 之后在 AS 中怎么就是找不到相关产物,作为Android 开发者知道肯定会有个 build 目录怎么就是不显示。所以去电脑对应的盘中看了下是有这么个 build 目录但是 AS 不显示,这样子办事很慢所以这里需要先加一个 gradle task:
task flutterPlugin << {
println "工程目录 = ${project.rootDir}/"
println "编译成功的位置 = ${this.buildDir}/"
def projectName = this.buildDir.getPath()
projectName = projectName.substring(0, projectName.length() - "app/".length())
def rDir = new File("${this.rootDir}/FlutterPlugin/")
def bDir = new File(projectName)
if (!rDir.exists()) {
rDir.mkdirs()
} else {
rDir.deleteDir()
}
bDir.eachDir {File dir ->
def subDir = dir.getPath()
def flutterJarDirName = subDir.replace("${projectName}/", "")
def flutterJarDir = null
if (subDir.contains("app")) {// 如果是app目录的话 拷贝编译后生成的flutter目录
flutterJarDir = new File("${subDir}/intermediates/assets/")
} else {
flutterJarDir = new File("${subDir}/intermediates/intermediate-jars/")
}
project.copy {
from flutterJarDir
into "${rDir}/${flutterJarDirName}"
}
}
}
把看不到的build中产物给拷贝出来,将结果放入工程的 FlutterPlugin 目录下:
红色框内的东西是 Flutter 的 gradle 插件产生的依赖包,我们也是需要的,所以顺便一起拷贝出来,那需要在哪?看下面的这个类就知道了。
public final class GeneratedPluginRegistrant {
public static void registerWith(PluginRegistry registry) {
PathProviderPlugin.registerWith(registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"));
}
}
到此为止我们把编译 flutter 的产物都拷贝出来,所以我们直接将这些产物放入我们的远程工程对应的 assets 以及 lib 路径中去。可是对应的FlutterActivity 还是报红,所以说 flutter 还有一些产物没有被我们发现。这时也不知道是什么玩意,所以就找大闲鱼的文章<贴在末尾>,最终找到了还有一个 flutter.jar 包没有引入。
这就是最终在原生的工程下新建了一个 fluttermodule 模块的最终层级关系了。然后把 demo 中的类相关拿进来通过 startActivity 成功的进入到FlutterActivity。
这里还是要把大闲鱼说的相关产物解释附上:
混合开发的巨坑:
很开心的运行然后用 AS 打开一看对应的 flutter.so 确是 armv8a 的框架,如果说直接拿到我们 app 中去就挂了因为我们 app 中:
ndk {
abiFilters "armeabi-v7a"
}
因为我们只用v7a的框架,这就很头痛了。
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
我们的新建 flutter 项目有这么一个 gradle 文件,所以说 so 兼容问题肯定是这货引起的。所以跟着进去看看哪里有猫腻…
还算比较顺利很快找到原因 原来这个 gradle 插件会自动的帮你找到最适合当前环境的 so 文件,所以我们只需要强制让它返回 v7a 的即可:
Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
String targetArch = 'arm'
// if (project.hasProperty('target-platform') &&
// project.property('target-platform') == 'android-arm64') {
// targetArch = 'arm64'
// }
// targetArch = 'arm'
也就是说让 targetArch 为 arm 即可,所以说 flutter 混合进来的时候最大的坑就是我觉得就是 so 兼容问题,索性还是比较顺利。
Flutter动态更新方案
当我完成混合成功之后,我就在想能不能像其他的混合开发库能实现动态更新。这里再次感谢大闲鱼的思路:因为大闲鱼说直接把 data/data/xxxxx 下的vm_snapshot_data、vm_snapshot_instr、
isolate_snapshot_data、isolate_snapshot_instr 替换成新编译成功的那么界面加载出来的就是新的界面,所以说这不就是动态更新吗?
所以说跟着节奏试试,将编译出来的打包成 zip 放入 sd 卡中去…
第一步:
/**
* 解压SD路径下的flutter包
*/
public static void doUnzipFlutterAssets() throws Exception {
String sdCardPath = Environment.getExternalStorageDirectory().getPath() + File.separator;
String zipPath = sdCardPath + "flutter_assets.zip";
File zipFile = new File(zipPath);
if (zipFile.exists()) {
ZipFile zFile = new ZipFile(zipFile);
Enumeration zList = zFile.entries();
ZipEntry zipEntry;
byte[] buffer = new byte[1024];
while (zList.hasMoreElements()) {
zipEntry = (ZipEntry) zList.nextElement();
Log.w("Jacyuhou", "==== zipEntry Name = " + zipEntry.getName());
if (zipEntry.isDirectory()) {
String destPath = sdCardPath + zipEntry.getName();
Log.w("Jayuchou", "==== destPath = " + destPath);
File dir = new File(destPath);
dir.mkdirs();
continue;
}
OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(sdCardPath + zipEntry.getName())));
InputStream is = new BufferedInputStream(zFile.getInputStream(zipEntry));
int len;
while ((len = is.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
out.close();
is.close();
}
zFile.close();
}
}
第二步:
/**
* 拷贝到data/data路径下
*/
public static void doCopyToDataFlutterAssets(Context mContext) throws Exception {
String destPath = PathUtils.getDataDirectory(mContext.getApplicationContext()) + File.separator + "flutter_assets/";
String originalPath = Environment.getExternalStorageDirectory().getPath() + File.separator + "flutter_assets/";
Log.w("Jayuchou", "===== dataPath = " + destPath);
Log.w("Jayuchou", "===== originalPath = " + originalPath);
File destFile = new File(destPath);
File originalFile = new File(originalPath);
File[] files = originalFile.listFiles();
for (File file : files) {
Log.w("Jayuchou", "===== file = " + file.getPath());
Log.w("Jayuchou", "===== file = " + file.getName());
if (file.getPath().contains("isolate_snapshot_data")
|| file.getPath().contains("isolate_snapshot_instr")
|| file.getPath().contains("vm_snapshot_data")
|| file.getPath().contains("vm_snapshot_instr")) {
doCopyToDestByFile(file.getName(), originalFile, destFile);
}
}
}
将对应的文件拷贝到 data 目录下去,跑起来看看 总算是成功了…
看上面的 gif 图,一开的 Flutter 界面上显示 null 那么你完了线上的包显示null 错误,所以这时就需要紧急发个补丁包,然后经过 Http 下载下来重新打开界面就修复了这个错误。
所以说这就是动态更新的方案…
END…
感谢大闲鱼的优秀文章给的思路:
https://zhuanlan.zhihu.com/p/40528502
https://yq.aliyun.com/articles/607014
推荐阅读:
长按识别小程序,参与抽奖
目前100000+人已关注加入我们