Flutter是Google推出的可以高效構(gòu)建Android菜职、iOS界面的移動(dòng)UI框架肴盏,在國(guó)內(nèi)中大公司像閑魚(yú)/Now直播等app陸續(xù)出現(xiàn)它的影子燕鸽,當(dāng)然閑魚(yú)的最為成熟闪彼,閑魚(yú)也非常的高效產(chǎn)出了很多優(yōu)秀的文章婿滓。
本文是基于Flutter SDK : 0.7.3
在最新的SDK v0.11.13中或者說(shuō)運(yùn)行后發(fā)現(xiàn)沒(méi)有PathProviderPlugin / SharedPreferencesPlugin 對(duì)應(yīng)的目錄以及jar包老速,那是因?yàn)樾掳姹局幸呀?jīng)不需要了 自然就可以刪除。
可是
可是凸主,網(wǎng)上能找到的混合開(kāi)發(fā)方案或者動(dòng)態(tài)更新flutter的相關(guān)文章都沒(méi)法符合我自己理想的效果橘券。所以自己摸索了一套混合開(kāi)發(fā)和動(dòng)態(tài)更新的方案,這里記錄一下摸索過(guò)程卿吐。
Flutter源碼分析
如果說(shuō)把自家的app改造成純Flutter方案那是不可能的旁舰,頂多是某個(gè)模塊或者某些模塊改成Flutter,所以自然想到Flutter如何跟原生混合開(kāi)發(fā)嗡官,混合開(kāi)發(fā)不是說(shuō)java去調(diào)用dart中的方法更多的是指如何從當(dāng)前Activity跳轉(zhuǎn)到Flutter實(shí)現(xiàn)的界面箭窜,要像知道這些東西那么必須得弄懂Flutter源碼,不求深入但求知之一二三四衍腥。
Android的應(yīng)用那么自然先找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;
}
}
還行初始化的東西不多,直接進(jìn)入onCreate對(duì)應(yīng)的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);
}
不具體一行一行的看代碼紧阔,但是看到了幾個(gè)很關(guān)鍵的詞在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");
}
}
沒(méi)錯(cuò)就是vm_snapshot_data坊罢、vm_snapshot_instr、isolate_snapshot_data擅耽、isolate_snapshot_instr
為什么說(shuō)這幾個(gè)這么重要呢活孩?
看下上面這幾個(gè)編譯的產(chǎn)物,我們就知道這就Flutter的核心東西乖仇『度澹或者換句話說(shuō)只要弄懂了這個(gè)玩意很有可能我們就悟出混合開(kāi)發(fā)的方案了询兴,那么他們是怎么讀取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掛鉤確實(shí)很驚喜起趾,因?yàn)榭吹娇隙ㄊ菑腁ssets中把這些讀出來(lái)的诗舰。可是讀出來(lái)放哪里去训裆?
那最后的那個(gè)方法initResources
該方法就是涉及存放的位置眶根,跟著源碼一路看下去,在ExtractTask.extractResources
找到了一點(diǎn)貓膩:
File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext));
確實(shí)边琉,就是在data/data/xxx/flutter_assets/
路徑下:
大體知道了這些個(gè)產(chǎn)物之后属百,界面是怎么加載? 首先加載Flutter的界面是個(gè)Activity叫
FlutterActivity
主要是通過(guò)FlutterActivityDelegate
這個(gè)類(lèi)变姨,然后我們主要看FlutterActivity.onCreate => FlutterActivityDelegate.onCreate
這個(gè)流程:
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相關(guān)的初始化進(jìn)來(lái)然后使用FlutterView
進(jìn)行加載顯示:
ensureInitializationComplete:// 進(jìn)行初始化
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產(chǎn)物
public static String findAppBundlePath(Context applicationContext) {
String dataDirectory = PathUtils.getDataDirectory(applicationContext);
File appBundle = new File(dataDirectory, sFlutterAssetsDir);
return appBundle.exists() ? appBundle.getPath() : null;
}
然后每一個(gè)FlutterView
中包了一個(gè)FlutterNativeView
然后最終就是FlutterView->runFromBundle
調(diào)用FlutterNativeView->runFromBundle
最后渲染到界面上族扰。
到此我們大概了解了Flutter需要的產(chǎn)物vm_snapshot_data、vm_snapshot_instr定欧、isolate_snapshot_data渔呵、isolate_snapshot_instr
然后簡(jiǎn)單的了解了加載流程,最后附上大閑魚(yú)的一張編譯大圖:
混合開(kāi)發(fā)
所以我覺(jué)得Flutter應(yīng)該跟ReactNative類(lèi)似只要把相關(guān)的bundle文件放入我們app的assets即可砍鸠,所以拿這個(gè)方向開(kāi)始編譯Flutter代碼厘肮,開(kāi)開(kāi)心心的輸入flutter run
之后在AS中怎么就是找不到相關(guān)產(chǎn)物,作為Android開(kāi)發(fā)者知道肯定會(huì)有個(gè)build目錄怎么就是不顯示睦番。所以去電腦對(duì)應(yīng)的盤(pán)中看了下是有這么個(gè)build目錄但是AS不顯示类茂,這樣子辦事很慢所以這里需要先加一個(gè)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中產(chǎn)物給拷貝出來(lái),將結(jié)果放入工程的FlutterPlugin
目錄下:
紅色框內(nèi)的東西是Flutter的gradle插件產(chǎn)生的依賴(lài)包托嚣,我們也是需要的巩检,所以順便一起拷貝出來(lái),那需要在哪示启?看下面的這個(gè)類(lèi)就知道了兢哭。
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的產(chǎn)物都拷貝出來(lái),所以我們直接將這些產(chǎn)物放入我們的遠(yuǎn)程工程對(duì)應(yīng)的assets以及l(fā)ib路徑中去夫嗓〕俾荩可是對(duì)應(yīng)的FlutterActivity還是報(bào)紅,所以說(shuō)flutter還有一些產(chǎn)物沒(méi)有被我們發(fā)現(xiàn)舍咖。這時(shí)也不知道是什么玩意矩父,所以就找大閑魚(yú)的文章<貼在末尾>,最終找到了還有一個(gè)flutter.jar
包沒(méi)有引入排霉。
這就是最終在原生的工程下新建了一個(gè)
fluttermodule
模塊的最終層級(jí)關(guān)系了窍株。然后把demo中的類(lèi)相關(guān)拿進(jìn)來(lái)通過(guò)startActivity
成功的進(jìn)入到FlutterActivity。這里還是要把大閑魚(yú)說(shuō)的相關(guān)產(chǎn)物解釋附上:
混合開(kāi)發(fā)的巨坑:
很開(kāi)心的運(yùn)行然后用AS打開(kāi)一看對(duì)應(yīng)的flutter.so確是
armv8a
的框架,如果說(shuō)直接拿到我們app中去就掛了因?yàn)槲覀僡pp中:
ndk {
abiFilters "armeabi-v7a"
}
因?yàn)槲覀冎挥?code>v7a的框架球订,這就很頭痛了后裸。
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
我們的新建flutter項(xiàng)目有這么一個(gè)gradle文件,所以說(shuō)so兼容問(wèn)題肯定是這貨引起的冒滩。所以跟著進(jìn)去看看哪里有貓膩....
還算比較順利很快找到原因 原來(lái)這個(gè)gradle插件會(huì)自動(dòng)的幫你找到最適合當(dāng)前環(huán)境的so文件微驶,所以我們只需要強(qiáng)制讓它返回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'
也就是說(shuō)讓targetArch
為arm即可,所以說(shuō)flutter混合進(jìn)來(lái)的時(shí)候最大的坑就是我覺(jué)得就是so兼容問(wèn)題开睡,索性還是比較順利因苹。
Flutter動(dòng)態(tài)更新方案
當(dāng)我完成混合成功之后,我就在想能不能像其他的混合開(kāi)發(fā)庫(kù)能實(shí)現(xiàn)動(dòng)態(tài)更新士八。這里再次感謝大閑魚(yú)的思路:因?yàn)榇箝e魚(yú)說(shuō)直接把data/data/xxxxx
下的vm_snapshot_data、vm_snapshot_instr梁呈、isolate_snapshot_data婚度、isolate_snapshot_instr
替換成新編譯成功的那么界面加載出來(lái)的就是新的界面,所以說(shuō)這不就是動(dòng)態(tài)更新嗎官卡?
所以說(shuō)跟著節(jié)奏試試蝗茁,將編譯出來(lái)的打包成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);
}
}
}
將對(duì)應(yīng)的文件拷貝到data目錄下去,跑起來(lái)看看 總算是成功了...
看上面的gif圖寻咒,一開(kāi)的Flutter界面上顯示null 那么你完了線上的包顯示null錯(cuò)誤哮翘,所以這時(shí)就需要緊急發(fā)個(gè)補(bǔ)丁包,然后經(jīng)過(guò)Http下載下來(lái)重新打開(kāi)界面就修復(fù)了這個(gè)錯(cuò)誤饭寺。
所以說(shuō)這就是動(dòng)態(tài)更新的方案...
END...
感謝大閑魚(yú)的優(yōu)秀文章給的思路:
https://zhuanlan.zhihu.com/p/40528502
https://yq.aliyun.com/articles/607014