Flutter混合開發和動態更新的探索歷程Android版
[顫振]是谷歌推出的可以高效構建的Android,iOS的介面的移動UI框架,在國內中大公司像閒魚/現在直播等應用陸續出現它的影子,當然閒魚的最為成熟,閒魚也非常的高效產出了很多優秀的文章。
可是
可是,網上能找到的混合開發方案或者動態更新撲的相關文章都沒法符合我自己理想的效果。所以自己摸索了一套混合開發和動態更新的方案,這裡記錄一下摸索過程。
撲原始碼分析
如果說把自家的應用改造成純撲方案那是不可能的,頂多是某個模組或者某些模組改成撲,所以自然想到撲如何跟原生混合開發,混合開發不是說java的去呼叫鏢中的方法更多的是指如何從當前活動跳轉到顫振實現的介面,要像知道這些東西那麼必須得弄懂顫振原始碼,不求深入但求知之一二三四。
安卓的應用那麼自然先找應用,所以很快找到了FlutterApplication:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">公共類FlutterApplication擴充套件Application { private Activity mCurrentActivity = null;
public FlutterApplication(){ }
@CallSuper public void onCreate(){ super.onCreate(); FlutterMain.startInitialization(本); }
public Activity getCurrentActivity(){ return this.mCurrentActivity; }
public void setCurrentActivity(Activity mCurrentActivity){ this.mCurrentActivity = mCurrentActivity; } }</pre>
還行初始化的東西不多,直接進入的onCreate的對應FlutterMain.startInitialization
中去看看:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public static void startInitialization(Context applicationContext,FlutterMain.Settings settings){ long initStartTimestampMillis = SystemClock.uptimeMillis(); initConfig(的applicationContext); initAot(的applicationContext); initResources(的applicationContext); 的System.loadLibrary( “撲”); long initTimeMillis = SystemClock.uptimeMillis() -initStartTimestampMillis; nativeRecordStartTimestamp(initTimeMillis); }</pre>
不具體一行一行的看程式碼,但是了看到很幾個關鍵的詞在initConfig
方法中:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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”); } }</pre>
就是沒錯vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr
為什麼說這幾個這麼重要呢?
在這裡插入圖片描述
看下上面這幾個編譯的產物,我們就知道這就撲動的核心東西。或者換句話說只要弄懂了這個玩意很有可能我們就悟出混合開發的方案了,那麼他們是怎麼讀取資產目錄下的這些玩意呢?
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">private static void initAot(Context applicationContext){ 設定<String> assets = listAssets(applicationContext,“”); sIsPrecompiledAsBlobs = assets.containsAll(Arrays.asList(sAotVmSnapshotData,sAotVmSnapshotInstr,sAotIsolateSnapshotData,sAotIsolateSnapshotInstr)); sIsPrecompiledAsSharedLibrary = assets.contains(sAotSharedLibraryPath); if(sIsPrecompiledAsBlobs && sIsPrecompiledAsSharedLibrary){ 丟擲新的RuntimeException(“找到預編譯的應用程式作為共享庫和Dart VM快照。”); } }</pre>
看到方法跟資產掛鉤確實很驚喜,因為看到肯定是從資產中把這些讀出來的。可是讀出來放哪裡去?
求最後那那個的方法initResources
該方法就是涉及存放的位置,跟著原始碼一路看下去,在ExtractTask.extractResources
找到了一點一貓膩:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext));</pre>
確實,在就是data/data/xxx/flutter_assets/
路徑下:
在這裡插入圖片描述
大體知道了這些個產物之後,介面是怎麼載入?首先載入Flutter的介面是個活動叫
FlutterActivity
主要是通過
FlutterActivityDelegate
這個類,然後我們主要看
FlutterActivity.onCreate => FlutterActivityDelegate.onCreate
這個流程:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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(); } } }</pre>
所以最介面的重要方法就是ensureInitializationComplete
也。就是把撲的相關初始化進來然後使用FlutterView
進行載入顯示:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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); 檔案appBundle =新檔案(dataDirectory,sFlutterAssetsDir); 返回appBundle.exists()?appBundle.getPath():null; }</pre>
每然後一個FlutterView
中包了一個FlutterNativeView
然後名單最終就是FlutterView->runFromBundle
呼叫FlutterNativeView->runFromBundle
求最後渲染到介面上。
到此我們大概瞭解了顫振的需要產物vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr
然後簡單的瞭解了載入流程,最後附上大閒魚的一張編譯大圖:
在這裡插入圖片描述
混合開發
所以我覺得撲應該跟ReactNative類似只要把相關的捆綁檔案放入我們的應用程式的資產即可,所以拿這個方向開始編譯撲程式碼,開心開心的輸入側flutter run
之後在AS中怎麼就是找不到相關產物,作為Android的開發者知道肯定會有個建目錄怎麼就是不顯示所以去電腦對應的盤中看了下是有這麼個建立目錄但是AS不顯示,這樣子辦事很慢所以這裡需要先加一個。gradle task
:
{this.buildDir} /”
def projectName = this.buildDir.getPath() projectName = projectName.substring(0,projectName.length() - “app /”。length())
def rDir = new File(“{flutterJarDirName}” } } }</pre>
把看不到的建設中產物給拷貝出來,查詢查詢結果將工程放入的FlutterPlugin
目錄下:
在這裡插入圖片描述
紅色框內的東西是撲的gradle這個外掛產生的依賴包,我們也是需要的,所以順便一起拷貝出來,那需要在哪?看下面的這個類就知道了。
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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”)); } }</pre>
到此為止我們把編譯撲的產物都拷貝出來,所以我們直接將這些產物放入我們的遠端工程對應的資產以及LIB路徑中去。可是對應的FlutterActivity還是報紅,所以說撲還有一些產物沒有被我們發現。這時也不知道是什麼玩意,所以就找大閒魚的文章<貼在末尾>,找到名單最終了還有一個flutter.jar
包沒有引入。
在這裡插入圖片描述
就是這名單最終在原生的工程下新建了一個fluttermodule
模組的名單最終層級關係了。然後把演示的中類相關拿進來通過startActivity
成功的展示進入到FlutterActivity。
這裡還是要把大閒魚說的相關產物解釋附上:
在這裡插入圖片描述
混合開發的巨坑:
很開心的執行然後用AS開啟一看對應的flutter.so確是
armv8a
的框架,如果說直接拿到我們的應用程式中去就掛了因為我們的應用程式中:
<pre style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">ndk { abiFilters“armeabi-v7a” }</pre>
我們因為只用v7a
的框架,這就很頭痛了。
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">申請自:“$ flutterRoot / packages / flutter_tools / gradle / flutter.gradle”</pre>
我們的新建撲專案有這麼一個gradle這個檔案,所以說這樣相容問題肯定是這貨引起的。所以跟著進去看看哪裡有貓膩....
還算比較順利很快找到原因原來這個gradle外掛會自動的幫你找到最適合當前環境的所以檔案,所以我們只需要強制讓它返回v7a
的即可:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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'</pre>
讓也就是說targetArch
為臂即可,所以說撲混合進來的時候最大的坑就是我覺得就是這樣相容問題,索性還是比較順利。
顫振動態更新方案
當我完成混合成功之後,我就在想能不能像其他的混合開發庫能實現動態更新這裡再次感謝大閒魚的思路:因為大閒魚說直接把data/data/xxxxx
下的vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr
替換分類中翻譯新compile-成功的那麼介面加載出來的就是新的介面,所以說這不就是動態更新嗎?
所以說跟著節奏試試,將編譯出來的打包成ZIP放入SD卡中去......
第一步:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">/ ** *解壓SD路徑下的flutter包 * / public static void doUnzipFlutterAssets()throws Exception { String sdCardPath = Environment.getExternalStorageDirectory()。getPath()+ File.separator; String zipPath = sdCardPath +“flutter_assets.zip”; 檔案zipFile =新檔案(zipPath); if(zipFile.exists()){ ZipFile zFile = new ZipFile(zipFile); 列舉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(); 繼續; }
OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(sdCardPath + zipEntry.getName()))); InputStream = 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(); } }</pre>
第二步:
<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">/ ** *拷貝到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); 檔案destFile = new File(destPath); 檔案originalFile = new File(originalPath);
File [] files = originalFile.listFiles(); for(檔案檔案:files){ Log.w(“Jayuchou”,“===== file =”+ file.getPath()); Log.w(“Jayuchou”,“===== file =”+ file.getName()); if(file.getPath()。contains(“isolate_snapshot_data”) || file.getPath()。包含( “isolate_snapshot_instr”) || file.getPath()。包含( “vm_snapshot_data”) || file.getPath()。contains(“vm_snapshot_instr”)){ doCopyToDestByFile(file.getName(),originalFile,destFile); } } }</pre>
將對應的檔案拷貝到資料目錄下去,跑起來看看總算是成功了...
在這裡插入圖片描述
看上面的gif圖,一開的Flutter介面上顯示null那麼你完了線上的包顯示null錯誤,所以這時就需要緊急發個補丁包,然後經過Http下載下來重新開啟介面就修復了這個錯誤。
所以說這就是動態更新的方案...
結束...