Flutter混合開發和動態更新的探索歷程 Android版
ofollow,noindex">Flutter 是Google推出的可以高效構建Android、iOS介面的移動UI框架,在國內中大公司像閒魚/Now直播等app陸續出現它的影子,當然閒魚的最為成熟,閒魚也非常的高效產出了很多優秀的文章。
可是
可是,網上能找到的混合開發方案或者動態更新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/
路徑下:

在這裡插入圖片描述
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...