Flutter混合开发和动态更新的探索历程Android版

[颤振]是谷歌推出的可以高效构建的Android,iOS的界面的移动UI框架,在国内中大公司像闲鱼/现在直播等应用陆续出现它的影子,当然闲鱼的最为成熟,闲鱼也非常的高效产出了很多优秀的文章。

可是

可是,网上能找到的混合开发方案或者动态更新扑的相关文章都没法符合我自己理想的效果。所以自己摸索了一套混合开发和动态更新的方案,这里记录一下摸索过程。

扑源码分析

如果说把自家的应用改造成纯扑方案那是不可能的,顶多是某个模块或者某些模块改成扑,所以自然想到扑如何跟原生混合开发,混合开发不是说java的去调用镖中的方法更多的是指如何从当前活动跳转到颤振实现的界面,要像知道这些东西那么必须得弄懂颤振源码,不求深入但求知之一二三四。

安卓的应用那么自然先找应用,所以很快找到了FlutterApplication:

公共类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; } }

还行初始化的东西不多,直接进入的onCreate的对应FlutterMain.startInitialization中去看看:

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); }

不具体一行一行的看代码,但是了看到很几个关键的词在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为什么说这几个这么重要呢?

在这里插入图片描述

看下上面这几个编译的产物,我们就知道这就扑动的核心东西。或者换句话说只要弄懂了这个玩意很有可能我们就悟出混合开发的方案了,那么他们是怎么读取资产目录下的这些玩意呢?

private static void initAot(Context applicationContext){ 设置 assets = listAssets(applicationContext,“”); sIsPrecompiledAsBlobs = assets.containsAll(Arrays.asList(sAotVmSnapshotData,sAotVmSnapshotInstr,sAotIsolateSnapshotData,sAotIsolateSnapshotInstr)); sIsPrecompiledAsSharedLibrary = assets.contains(sAotSharedLibraryPath); if(sIsPrecompiledAsBlobs && sIsPrecompiledAsSharedLibrary){ 抛出新的RuntimeException(“找到预编译的应用程序作为共享库和Dart VM快照。”); } }

看到方法跟资产挂钩确实很惊喜,因为看到肯定是从资产中把这些读出来的。可是读出来放哪里去?

求最后那那个的方法initResources该方法就是涉及存放的位置,跟着源码一路看下去,在ExtractTask.extractResources找到了一点一猫腻:

File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext));

确实,在就是data/data/xxx/flutter_assets/路径下:

在这里插入图片描述

大体知道了这些个产物之后,界面是怎么加载?首先加载Flutter的界面是个活动叫
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也。就是把扑的相关初始化进来然后使用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); 文件appBundle =新文件(dataDirectory,sFlutterAssetsDir); 返回appBundle.exists()?appBundle.getPath():null; }

每然后一个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

task flutterPlugin << { 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 =新文件(projectName) if(!rDir.exists()){ rDir.mkdirs() } else { rDir.deleteDir() } bDir.eachDir {File目录 - > def subDir = dir.getPath() def flutterJarDirName = subDir.replace(“ {projectName} /”,“”) def flutterJarDir = null if(subDir.contains(“app”)){//如果是app目录的话拷贝编译后生成的flutter目录 flutterJarDir =新文件(“ {subDir} / intermediates / intermediate-jars /”) } project.copy { 来自flutterJarDir 成为“ {flutterJarDirName}” } } }

把看不到的建设中产物给拷贝出来,查询查询结果将工程放入的FlutterPlugin目录下:

在这里插入图片描述

红色框内的东西是扑的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”)); } }

到此为止我们把编译扑的产物都拷贝出来,所以我们直接将这些产物放入我们的远程工程对应的资产以及LIB路径中去。可是对应的FlutterActivity还是报红,所以说扑还有一些产物没有被我们发现。这时也不知道是什么玩意,所以就找大闲鱼的文章<贴在末尾>,找到名单最终了还有一个flutter.jar包没有引入。

在这里插入图片描述

就是这名单最终在原生的工程下新建了一个fluttermodule模块的名单最终层级关系了。然后把演示的中类相关拿进来通过startActivity成功的展示进入到FlutterActivity。

这里还是要把大闲鱼说的相关产物解释附上:

在这里插入图片描述

混合开发的巨坑:

很开心的运行然后用AS打开一看对应的flutter.so确是
armv8a

的框架,如果说直接拿到我们的应用程序中去就挂了因为我们的应用程序中:

ndk { abiFilters“armeabi-v7a” }

我们因为只用v7a的框架,这就很头痛了。

申请自:“$ flutterRoot / packages / flutter_tools / gradle / flutter.gradle”

我们的新建扑项目有这么一个gradle这个文件,所以说这样兼容问题肯定是这货引起的。所以跟着进去看看哪里有猫腻....

还算比较顺利很快找到原因原来这个gradle插件会自动的帮你找到最适合当前环境的所以文件,所以我们只需要强制让它返回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为臂即可,所以说扑混合进来的时候最大的坑就是我觉得就是这样兼容问题,索性还是比较顺利。

颤振动态更新方案

当我完成混合成功之后,我就在想能不能像其他的混合开发库能实现动态更新这里再次感谢大闲鱼的思路:因为大闲鱼说直接把data/data/xxxxx下的vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr替换分类中翻译新compile-成功的那么界面加载出来的就是新的界面,所以说这不就是动态更新吗?

所以说跟着节奏试试,将编译出来的打包成ZIP放入SD卡中去......

第一步:

/ ** *解压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(); } }

第二步:

/ ** *拷贝到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); } } }

将对应的文件拷贝到数据目录下去,跑起来看看总算是成功了...

在这里插入图片描述

看上面的gif图,一开的Flutter界面上显示null那么你完了线上的包显示null错误,所以这时就需要紧急发个补丁包,然后经过Http下载下来重新打开界面就修复了这个错误。

所以说这就是动态更新的方案...

结束...

你可能感兴趣的:(Flutter混合开发和动态更新的探索历程Android版)