flutter不支持热更新_Flutter 在安卓上可以实现热更新了
本文由 句號君 授權投稿
原文鏈接:https://blog.csdn.net/qizewei123/article/details/102963340
Flutter 官方在 GitHub 上聲明是暫時不支持熱更新的,但是在 Flutter 的源碼里,是有一部分預埋的熱更新相關的代碼,并且通過一些我們自己的手段,在Android端是能夠實現動態更新的功能的。
Flutter 產物的探究
不論是創建完全的 Flutter項目,還是 Native以 Moudle的方式集成 Flutter,亦或是 Native以 aar方式集成 ?Flutter,最終 ?Flutter在 Andorid端的 App 都是以 Native項目+ Flutter 的UI產物存在的。所以在這里拆開一個 Flutter在 release模式下編譯后生成 aar包來做分析:
我們關注重點在 assets,jni,libs 這 3 個目錄中,其他的文件都是 Nactive層殼工程的產物。
jni :該目錄下存在文件 libflutter.so,該文件為 Flutter Engine (引擎) 層的 C++實現,提供skia(繪制引擎),Dart,Text(紋理繪制)等支持。
libs:該目錄下存在文件為 flutter.jar,該文件為 Flutter embedding (嵌入) 層的 Java實現,該層提供給 Flutter 許多Native層平臺系統功能的支持,比如創建線程。
assets:該目錄下分為兩部分:
flutter_assets 目錄:該目錄下存放Flutter 我們應用層的資源,包括images,font等
isolate_snapshot_data,isolate_snapshot_instr,vm_snapshot_data,vm_snapshot_instr 文件:這 4 個文件分別對應 isolate、VM 的數據段和指令段文件,這就是我們自己的 Flutter 代碼的產物了。
Flutter 代碼的熱更新
代碼探究
在我們的 Native 項目中,會在 FlutterMainActivity 中,通過調用 Flutter 這個類來創建 View:
flutterView?=?Flutter.createView(this,?getLifecycle(),?route);layoutParams?=?new?FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT);
addContentView(flutterView,?layoutParams);
查看 Flutter 類代碼,發現 Flutter 類主要做了幾件事:
使用 FlutterNative 加載 View,設置路由,使用 lifecycle 綁定生命周期
使用 FlutterMain 初始化,重點關注這里。
FlutterMain.startInitialization(activity.getApplicationContext());
FlutterMain.ensureInitializationComplete(activity.getApplicationContext(),?(String[])null);
FlutterNativeView?nativeView?=?new?FlutterNativeView(activity);
所以,真正初始化的相關代碼是在 FlutterMian 中:
public?static?void?startInitialization(Context?applicationContext,?FlutterMain.Settings?settings)?{????if?(Looper.myLooper()?!=?Looper.getMainLooper())?{
????????throw?new?IllegalStateException("startInitialization?must?be?called?on?the?main?thread");
????}?else?if?(sSettings?==?null)?{
????????sSettings?=?settings;
????????long?initStartTimestampMillis?=?SystemClock.uptimeMillis();
????????initConfig(applicationContext);
????????initAot(applicationContext);
????????initResources(applicationContext);
????????System.loadLibrary("flutter");
????????long?initTimeMillis?=?SystemClock.uptimeMillis()?-?initStartTimestampMillis;
????????nativeRecordStartTimestamp(initTimeMillis);
????}
}
在 startInitialization 中,主要執行了三個初始化方法 initConfig(applicationContext),initAot(applicationContext),initResources(applicationContext),最后記錄了執行時間。
在 initConfig 中:
private?static?void?initConfig(Context?applicationContext)?{????try?{
????????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");
????????????sFlutterAssetsDir?=?metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY,?"flutter_assets");
?????????}
????}?catch?(NameNotFoundException?var2)?{
????????throw?new?RuntimeException(var2);
????}
}
在 initResources 中:
sResourceExtractor?=?new?ResourceExtractor(applicationContext);sResourceExtractor.addResource(fromFlutterAssets(sFlx)).addResource(fromFlutterAssets(sAotVmSnapshotData)).addResource(fromFlutterAssets(sAotVmSnapshotInstr)).addResource(fromFlutterAssets(sAotIsolateSnapshotData)).addResource(fromFlutterAssets(sAotIsolateSnapshotInstr)).addResource(fromFlutterAssets("kernel_blob.bin"));
if?(sIsPrecompiledAsSharedLibrary)?{
????sResourceExtractor.addResource(sAotSharedLibraryPath);
}?else?{
????sResourceExtractor.addResource(sAotVmSnapshotData).addResource(sAotVmSnapshotInstr).addResource(sAotIsolateSnapshotData).addResource(sAotIsolateSnapshotInstr);
}?
sResourceExtractor.start();
在 ResourceExtractor 類中,通過名字就能知道這個類是做資源提取的。把 add 的 Flutter 相關文件從 assets 目錄中取出來,該類中 ExtractTask 的 doInBackground 方法中:
File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext))
這句話指定了資源提取的目的地,即 data/data/包名/app_flutter,如下:
如圖,可以看到該目錄是的訪問權限是可讀可寫,所以理論上,我們只要把自己的 Flutter 產物下載后,從內存 copy 到這里,便能夠實現代碼的動態更新。
代碼實現
public?class?FlutterUtils?{????private?static?String?TAG?=?"FlutterUtils.class";
????private?static?String?flutterZipName?=?"flutter-code.zip";
????private?static?String?fileSuffix?=?".zip";
????private?static?String?zipPath?=?Environment.getExternalStorageDirectory().getPath()?+?"/k12/"?+?flutterZipName;
????private?static?String?targetDirPath?=?zipPath.replace(fileSuffix,?"");
????private?static?String?targetDirDataPath?=?zipPath.replace(fileSuffix,?"/data");
????/**
?* Flutter 代碼熱更新第一步:?解壓 Flutter 的壓縮文件
?*/
????public?static?void?unZipFlutterFile()?{
????????Log.i(TAG,?"unZipFile:?Start");
????????try?{
????????????unZipFile(zipPath,?targetDirPath);
????????????Log.i(TAG,?"unZipFile:?Finish");
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}
????}
????/**
?* Flutter 代碼熱更新第二步:?將 Flutter 的相關文件移動到 AppData 的相關目錄,APP啟動時調用
?*
?*?@param?mContext?獲取?AppData?目錄需要
?*/
????public?static?void?copyDataToFlutterAssets(Context?mContext)?{
????????String?appDataDirPath?=?PathUtils.getDataDirectory(mContext.getApplicationContext())?+?File.separator;
????????Log.d(TAG,?"copyDataToFlutterAssets-filesDirPath:"?+?targetDirDataPath);
????????Log.d(TAG,?"copyDataToFlutterAssets-appDataDirPath:"?+?appDataDirPath);
????????File?appDataDirFile?=?new?File(appDataDirPath);
????????File?filesDirFile?=?new?File(targetDirDataPath);
????????File[]?files?=?filesDirFile.listFiles();
????????for?(File?srcFile?:?files)?{
????????????if?(srcFile.getPath().contains("isolate_snapshot_data")
????????????????||?srcFile.getPath().contains("isolate_snapshot_instr")
????????????????||?srcFile.getPath().contains("vm_snapshot_data")
????????????????||?srcFile.getPath().contains("vm_snapshot_instr"))?{
????????????????File?targetFile?=?new?File(appDataDirFile?+?"/"?+?srcFile.getName());
????????????????FileUtil.copyFileByFileChannels(srcFile,?targetFile);
????????????????Log.i(TAG,?"copyDataToFlutterAssets-copyFile:"?+?srcFile.getPath());
????????????}
????????}
????????Log.i(TAG,?"copyDataToFlutterAssets:?Finish");
????}
????/**
?*?解壓縮文件到指定目錄
?*
?*?@param?zipFileString?壓縮文件路徑
?*?@param?outPathString?目標路徑
?*?@throws?Exception
?*/
????private?static?void?unZipFile(String?zipFileString,?String?outPathString)?{
????????try?{
????????????ZipInputStream?inZip?=?new?ZipInputStream(new?FileInputStream(zipFileString));
????????????ZipEntry?zipEntry;
????????????String?szName?=?"";
????????????while?((zipEntry?=?inZip.getNextEntry())?!=?null)?{
????????????????szName?=?zipEntry.getName();
????????????????if?(zipEntry.isDirectory())?{
????????????????????szName?=?szName.substring(0,?szName.length()?-?1);
????????????????????File?folder?=?new?File(outPathString?+?File.separator?+?szName);
????????????????????folder.mkdirs();
????????????????}?else?{
????????????????????File?file?=?new?File(outPathString?+?File.separator?+?szName);
????????????????????if?(!file.exists())?{
????????????????????????Log.d(TAG,?"Create?the?file:"?+?outPathString?+?File.separator?+?szName);
????????????????????????file.getParentFile().mkdirs();
????????????????????????file.createNewFile();
????????????????????}
????????????????????FileOutputStream?out?=?new?FileOutputStream(file);
????????????????????int?len;
????????????????????byte[]?buffer?=?new?byte[1024];
????????????????????while?((len?=?inZip.read(buffer))?!=?-1)?{
????????????????????????out.write(buffer,?0,?len);
????????????????????????out.flush();
????????????????????}
????????????????????out.close();
????????????????}
????????????}
????????????inZip.close();
????????}?catch?(Exception?e)?{
????????????Log.i(TAG,e.getMessage());
????????????e.printStackTrace();
????????}
????}
????/**
?*?使用FileChannels復制文件。
?*
?*?@param?source?原路徑
?*?@param?dest?目標路徑
?*/
????public?static?void?copyFileByFileChannels(File?source,?File?dest)?{
????????FileChannel?inputChannel?=?null;
????????FileChannel?outputChannel?=?null;
????????try?{
????????????inputChannel?=?new?FileInputStream(source).getChannel();
????????????outputChannel?=?new?FileOutputStream(dest).getChannel();
????????????outputChannel.transferFrom(inputChannel,?0,?inputChannel.size());
????????????refreshMedia(BaseApplication.getBaseApplication(),?dest);
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????try?{
????????????????inputChannel.close();
????????????????outputChannel.close();
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
????/**
?*?更新媒體庫
?*
?*?@param?cxt
?*?@param?files
?*/
????public?static?void?refreshMedia(Context?cxt,?File...?files)?{
????????for?(File?file?:?files)?{
????????????String?filePath?=?file.getAbsolutePath();
????????????refreshMedia(cxt,?filePath);
????????}
????}
????public?static?void?refreshMedia(Context?cxt,?String...?filePaths)?{
????????MediaScannerConnection.scanFile(cxt.getApplicationContext(),
????????????????????????????????????????filePaths,?null,
????????????????????????????????????????null);
????}
}
Flutter 資源的熱更新
我們的App安裝到手機上后,是很難再修改 Assets 目錄下的資源,所以關于資源的替換,目前的方案是使用 Flutter 的 API :Image.file() 來從存儲卡中讀取圖片。
通常我們的 Flutter 項目中應當存有關于 App 的圖片,盡量保證在熱更新的時候使用已經存在的圖片。
其次,我們可以使用 Image.network() 來加載網絡資源的圖片,如果還不能滿足需求,兜底的方案就是使用 Image.file(),將資源圖片放到Zip目錄下一起下發,并在Flutter代碼中使用 Image.file() 來加載。
通過 Native 層方法拿到圖片文件夾的內存地址 dataDir
判斷圖片是否存在,存在則加載,不存在則加載已經存在的圖片占位
new File(dataDir + 'hotupdate_test.png').existsSync()? Image.file(new File(dataDir + 'hotupdate_test.png')): Image.asset("images/net_error.png"),
總結
在 Flutter 代碼產物替換中,因為替換的 4 個文件皆為直接加載到內存中的引擎代碼,所以這部分優化空間有限。但在資源的熱更新中,資源是從Assets取得,所以這里應該有更優的方案。
Flutter 的熱更新意味著可以實在App的一個入口里,像 H5 一樣無窮的嵌入頁面,但又有和原生媲美的流暢體驗。
未來 Flutter 熱更新技術如果成熟,應用開發可能只需要 Android端和 IOS端實現本地業務功能模塊的封裝,業務和UI的代碼都放在 Flutter 中,便能夠真正的實現移動兩端一份業務代碼,并且賦予產品在不影響用戶體驗的情況下,擁有動態部署APP內容的能力。
推薦閱讀
如何寫出讓同事好維護的代碼?
一線大廠的程序員職級對標
真正的強者,敢于在寒冬里裸辭
編程·思維·職場
歡迎掃碼關注
總結
以上是生活随笔為你收集整理的flutter不支持热更新_Flutter 在安卓上可以实现热更新了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu 上网总结
- 下一篇: 学习SOX(1) 在VC中编译