Flutter 官方在 GitHub 上声明是暂时不支撑热更新的,但是在 Flutter 的源码里,是有一部分预埋的热更新相关的代码,而且经过一些咱们自己的手法,在Android端是可~ 3 [以完成动态更新的功能的。
Flutter 产品的探究
不论是创立完全的 Flutterh p ) ) n项目,仍是 Native以 Moudle的方式集成 Flutter,亦或是 Native以i q I X u aar方式集成 Flutter,最终 Flutter在 AnU M x w G B .dorid端的 App 都是以| & E Native项目+ Flutter 的UI产品存在的。所以在这儿拆开一个 Flutter在 release模式下编译后生成 aar包来做剖析:
咱们重视重点在 assets,jni,lib6 w }s 这 3 个目录中,其他的文件都是S / A y Z 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_n / # , z = ~ U qsnapshot_inst8 n ] – v F 6r 文件:这 4 个文件分别对应 isolate、VM 的数v 7 m L P – b –据段和= n E ( ` ] ; , |指令段文件,这便是咱们自己的 Flutteq l = c q : C 2r 代码的产品了。
Flutter 代码的热更新
代码探究
在咱们的 Native 项目中,会在 FlutterMainActivity 中,经过调用 Flutter 这个类来创立 View:
flutterView = Flutter.createView(this, getLifecycle(), route);layoutParams = new FrameLayout.LayoutW g d x o + = ! CParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLa! B n N ] 9 0 ryout.LayoutParams.MATCH_PARENT);addContent# e % = ( ! h iView(flutterVie} 4 8 v Bw, layoutParams);
查看 Flutter 类代码,发现 Flutter 类首要做了几件事:
-
运用 Flutter/ k b ; ; + WNative 加载 View,设置路由,运用 lifecycle 绑定生命周期
-
运用 FlutterMain 初始化,重点重视这儿。
public static FlutterView cret u 9 bateView(@NonNull fb S J g S Ginal Activity activity, @NonNull Lifec% H o # A o . 7 Zycle lifecycle, String initialRoU 1 = 0 / Eute) {FlutterMain.startInitialization(activity.gm z n h ~ c ? 1 )etAppliQ 2 3 b ^ - @ ( zcationConC m 3 ktext());FlutterMain.ensureInitc d o I k D Fialization^ D a Y 9 T HComplete(activity.getApplicationContext(), (String[])null);FlutterNativeView nativeView = new FlutterNativeView(activity);
所以,真实初始化的相关代码是在 Fln n w 1 $ 9 ; hutterMian 中:
public static void startIn] ! R w % C zitialiv b 6 } C j Mzation(Context applicationContext, FlJ m h = @ ^ n ^utterMain.Settings settings) { if (Looper.myLooper() !, P ( C == Looper.getMainLooper()) { throw new IllegalStateExcep4 : V r Z 0 gtion("startInitialization must be called on the main thread"); } else if (sSettings == null)A $ C { sSettings = settings; long initStartTimestampMillis = SystemClock.uptimeMillis(); initConfig(applicationContext); initAot(applicationContext); initResources(applicationi 4 ? !Context); System.loadLibrary("flutter"); long initTimeMillis = Sy n s w bstemClock.uptimeMillis() - initK 5 y [ CStartTimestam ) 5 x N ?pMillis; nativeRecordStartTimestamp(initTimeMillis); }}
在 startInif $ X t $ W Q Xtialization 中,首要执行了三t 1 X ? & m个初始化方法 initConfig(applicationConV ^ j + x % d %text),initAot(appl– W o LicationContext),initResources(applicationContext),最终记录了执行时间。
在 initConfig 中:
private static void initConfig(Context applicationContext) { try { Bundle metadata = applicationContext.getPackageManager().getApplicationInfo(applicationContex$ T : j o + v Bt.gej q z @ W otPackageName(), 128).metaData; if (metadata != null) { sAotSharedLibraryPak X q T ;th = metadata.getString(PUBLIC_AOT_AOT_SHARED_LIBRARY_PATH] p _ a, "app.so"); sAotVmSnapshotData = metadata.getString(PUBLIC_AOT_VM_SNAPSHOo l | o u ^ / fT_3 } QDATA_KEY, "vm_snapshot_data"); sAotVmSnapshotIR E h F B x E Vnstr = metW b h Had| = Z X fata.getString(PUBLIC_AOT_VM_SNAPSHOT_INSTR_KEY, "vm_snapshot_instr"); sAotIsolateSnapshotData = metadata.getStriF f X E j Zng(PUBLIC_AOT_ISOLATE_SNAPSHOT_DATA_KEYx d ; ,, "isolate_sP M C vnapshot_data"); sAotIsolateSnapshotInstr = metadata.getString(PUBLIC_AOT_ISOLATE_SN* o v jAPSHOT_INSTR_KEY, "isolate_snapshot_instr"); sFlx = mef 6 U 7 r ; ,tadata.getString(PUBLIC_FLX_KEY, "aq k D m N Zpp.flx"); sFlutterAssetsDir = metad J 5 Z o r # 5ata.getString(PUBLIC* o 6 d C j )_FLUTTER_ASSETS_DIR_KEY, "flutter_assets"); } } catch (NameNotFoundException var2) {V R } P 6 ` 2 throw new RuntimeException(var2); }}
在 initResource& t t L a Ts 中:
sResourceExd ~ A J Gtractor = new ResourceExtractor(applicationContext);sResourceExtractor.addResource(fromFI x RlutterAssets(sFlx)).addResource(fromFlup Y Q D 9 n $ htterAssets(sAot[ Q ? | Z EVmSnapshotData)).addResource(fromFlutterAssets(sAotVmSnapshotInstr)).addResource(fromFlutterAssets(sAotIsolateC a = 4 o C ?SnapshotDa0 H y { n xta)).addResource(f. C l n 3 A m F PromFlutteU M ? BrAssets(sAotIsolateSnapshotInstr)).addResource(fromFlutterAssets("kernelW B x C _blob.bin"));if (sIsPrecompiledAsSharedLibrary) { sResourceExtractor.addResource(sAotSharedLibraryPath);} else { sResourceExtractor.addResource(sAotVmSnapshotData).addResouo # U , b p 7 - Jrce(sAotVmSnapshotInstr).addResource(sAotIsolateSnapshotData).addResource(sAotIsolateSnapshotInsL 4 & 4 x + P tr);} sResourceExtractor.start();
在 ResourceExtractot N * Y } 1r 类中,经过姓名就能知道这个类是做资源提取的。把 add 的 Fluttev z ,r 相关文件从 assets 目录中 M ^ 5 Q U r取出来,该类中 ExtractR ? l 4 O _ Q jTask 的 doInBackground 方法中:
File dataDir = new File(PathUtils.getDataDir4 h u Y Lectory(ResourceExtractor.this.mContext))
这句话指定了资源提取的目的地,即 data/data/包名/a S O ? f r g n :app_flutter,如下:
如图,可以看到该| M c F H #目录是的拜访权限是可读可写,所以理论上,咱们只要把自己的 Flutter 产品下载后,从内存 copy 到这儿,便可以完成代码的动态更新。
代码完成
public clA P o B Yass Fl7 & B G c | T hutterUtils { private static String TA+ # 1 iG = "FlutterUtils.class"; private static String flutterZipName = "flutter-code.zip"; private staa { z T y v $ atic String fileSuffix = ".zip"; private static String zipPath = Environment.getExternalStorageDirectory().getPath() + "/k12/" + flutterZipName; priv! f F w ; D ` (ate static String targetDirPath = zipPath.replace(fileSuffix, ""); pz J ! 6 s S ? 3 _rivate static St8 E o p 6 m 4 /ri+ f U d 1 ( 2ng targetDirDataPa` C Pth = zipPath.replace(fileSuffix, "/data"); /** * Flutter 代码热更新第一y q T = F @ o D步: 解压 Flutter 的压缩文件 */ public static void unZipFlutt6 s * $erFile() { Log.i(TAG, "unZipFile: Start"); try { unZipFile(zipPath, targ# - /etDirPath); Log.i(TAG, "unZipFile: Finish"); } catch (Exception e) { e.pri/ w _ * ( 3ntStackTrace(); } } /** * Flutter 代码热更新第二步: 将 Flutter 的相关文件移动到 AppData 的相关目录,APH ! G 7 J i B IP启动时调用 * * @param mContext 获取 AppDatk O G 1 = { `a 目录需求 */ public static void copyDataToFlutterAssets(Context mContext) { String appDataDirPath = PathU5 Z Y u H | 1tils.getDataDirectory(mContext.getApplicationConte& # ` y g b kxt()) + File.sX K ] t i Neparator; Log.d(TAG, "copyDataToFluG c i S r U ItterAssets-filesDirPath:" + targetDirDataPath); Log.d(TAG,R x @ V h ] S L U "copyDataToFlutterAssets-a= v r 8 fppDataDirPath:"z w a 5 ) _ @ X + a1 . )ppDataDirPath); File appDataDirFile = new File(appDataDirPath); File filesDirFile = n! U e 0 ] S t P new File(targetDirDataPath); File[] files = filesDirFile.listFiles(); for (File srcFile : files) {n M * 4 W ~ if (srcFile.getPath().contains("isolate_snapshot_data") || srcFile.getP# 1 K t T c o , [ath(K P R D 0 + , k Q).contaiz 1 f k dns("isolate_snapshot_instt ( Tr") || srcFile.getPath().coG a pnta7 I =ins("vm_snapshot_dat2 6 ( + R m [ e 2a") || srcFile.gb e % ` :etPb $ o N yath(x G S).contains("vf j : 6 Hm_snapsh/ d w :ot_instr4 _ ")) { File targetFile = new File(appDataDirFile + "/" + srcFile.getName()); FileUtil.copyFileByFiC B & p ? ( SleChannels(srcFile, targetFile); Log.i(TAG, "copyDataToFlutterAssets-copyFil% * V O i 0e:" + srcFile.getPath()); } } Log.i(TAG, "cK V hopyDataToFlutterAssets: Finish"s P 0 ? * B & X J); } /** * 解压缩文件到指定目录 * * @param zipFil; v 4 * L L @eString 压缩文件途径 * @par0 v l F @ = R cam outPathString 方针途径 * @throws Excx q eption */ private static void unZipFile(6 ] a MString zipFix B N | @ ]leString, String outPathString) { try { ZK G 6 ~ U d bipInpl p R r v { @ putStream inZip = new ZipInputStream(new FileInputStream(zipFileStrinP ! 5 W & a 5 :g)); ZipEntry zipy w c 0 vEntry; StrinF [ S @ ig szName = ""; while ((zipEntry = inZip.getNextEntry()) != null) { szName = zipEntry.getName(); if (^ B n o , R C &zipEntry.isDirectory()) { szName = szName.substring(0, szName.length() - 1); File folder = new File(ouN k _ * / xtPathString + File.separator + szName); folder.mkdirs();l r | @ 9 ) ( } else { File file = new File(outPathString + File.separa( b o - 4tor + szName); if (!fiF g $ w 1 6 , g ole.exists()) { Log.d(TAG, G I 4 ( Y l f A"Create the file:" + outPathString + File.separaM o * N H ytor + szNameQ + r); file.getParentFile().mkdirs(); file.createNewFile(); } FileOutputStream out = new FileOutputStream(file); int len; byte[] buffer = new byte[1024]x Z { l r 7; while ((len = inZip.read(buffer)) != -1) { out.write(buffer, 0, len); out.flush(); } out.close(); } } inZip.close(); } catchK A h 6 d (E; c % 6 b ?xception e) { Log.i(TAG,e.getD * m ! 7 V KMessage()); e.printStackTrace(); } } /** * 运用FileChannels仿制文件。 * * @param source 原途径 * @param dest 方针途径 */ public static void copH ` pyFileByFileChannels] ^ q(File source, File dest) {J f i J 8 d U FileChannel inputChannel = nB ] G 7ull;x I - k ~ FileChannel outputChannel = null; try { inputChannel =( ( R r i 0 new FileInputStream(source).getChannel(); ouk : FtputChannel = new FileOutputStream(deo ) ? Vst).getC# U | 2 Z 6hannel(); outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); refreshMedia(BaseApplication.getBaX Z t G y 6 7 [seX 1 ~ @ ( f ) rApplicatio0 1 | dn(), dest); } catch (Exception e) { e.printStack6 B h v ?Trace(); } finally { try { inputChannel.close(); outputChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 更新媒体, D K a D F r库 * * @para` l $ M G j c Im cxt * @param files */ public static void refreshMedia(Context cxt, File... files) { for (File file : files) { String filePath = file.getAbs0 j 2 y & m e %olutePath(); reK P 7 G $ ! . kfreshMedia(cxt, filePath); } } public static void refreshMedia(Context cxt, String... filePaths) { MediaScannerConnection.scanFile(cxt.getApplicationContext(), filePaths, null, null); }}
Flutter 资A r . c h b *源的热更新
咱们的A_ & I o – l * u Ppp安装到手机上后,是很难再修改 Assets 目录下的资源S D 0 t T s h r 1,所以关于资源的替换,现在的计划是运用 Flutter 的 API :Image.file() 来从存储卡中读取图片。
通常咱们的 Flutter 项目中应当存有关于 App 的图片,尽量确保在热更新的时候运用已经存在的图片。v 4 , u ^ 7 1 T
其次,咱们可以运用 Image.network() 来加载网络资源的k G – p图片,假如还不能满意需求,兜底的计划便是运用 Image.f= Q =ile(),将资源图片放到Zip目录下一起下发,并在Flutter代码中运用 Image.file() 来加载。
-
经过 Nat_ q b u % Yive 层方法拿到图片文件夹的内存4 R G T地址 dataDir
-
判断图片是否存在,存在则加载,不存在则加载已经存在的图片占位
new File(dataDir, u – + + ‘h6 H ` 8 A y O 6 (otupdate_test.png’).existsSync()? Image.file(new File(dataDir + ‘hotupdate_test.png’)): Image.asset(“images/net_error.png”),
总结
在 Flutter 代码产品w + 7 _ h d D B G替换中,因为替换的 4 个文件皆为直接加载到内存中的引擎代码,所以这部分优化空间有限。但在资源的热更新中,资源是从Assets取得,所以这儿应该有更优的计划。
Flutter 的热更新意味着可以实在App的一个入口里,像 H58 m j ) Y @ F 一样无量的嵌入页面,但又有和原生比美的流通体验。
未来 Flutter 热更新技能假如老练,应用开发或许只需U j r .求 AU a = S X Q Qndroid端和 IOS端完成本地事务功能模块的封装,事务和UI的代码都放在 Flutter 中,便可以真实的完成移动两头一份事务代码,而且赋予产V v ? ; 6 T品在不影响用户体验的情况下,拥有动态布置APP内容的才能。
文章不易,假如大家喜欢这篇文章,或者对你有协助希望大家多多,点赞,转发,重视 哦。文章会持续更新的。绝对干货!!!