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:该目录下分为两部分:

  1. flutter_assets 目录:该目录下寄存Flutter 咱们应用层的资源,包含images,font等

  2. 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 类首要做了几件事:

  1. 运用 Flutter/ k b ; ; + WNative 加载 View,设置路由,运用 lifecycle 绑定生命周期

  2. 运用 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内容的才能。

文章不易,假如大家喜欢这篇文章,或者对你有协助希望大家多多,点赞,转发,重视 哦。文章会持续更新的。绝对干货!!!