关于动态更新
- Google 考虑自家 Android 应用安全和苹果策略, 本来在 2019 的 roadmap 里面有这样一项,roadmap公布后,过一段时间后面又移除了这一项,目前不是主航道。
- Android 目前可以通过整包方式实现动态更新, iOS 目前还不支持。
Android Flutter 整包动态更新实现
Flutter 构建产物加载流程
- 在 Flutter Android 启动流程 一文中,我们知道了 Android Flutter 启动流程,启动时,会去加载 data/data/{AppPackage}/app_flutter 下面如下文件。
isolate_snapshot_instr - 应用程序指令段isolate_snapshot_data - 应用程序数据段vm_snapshot_instr - DartVM 指令段vm_snapshot_data - DartVM 数据段

Flutter 构建产物
- 在 Flutter Android 混合工程实践 一文中,详细阐述了生成 Flutter 构建产物的流程。我们可以把构建产物打成压缩包上传到 CDN,然后 Native 通过下载服务器 Flutter 整体更新包进行更新。
**
import android.Manifest;import android.app.DownloadManager;import android.content.BroadcastReceiver;import android.content.Context;import android.content.Intent;import android.content.IntentFilter;import android.content.pm.PackageManager;import android.net.Uri;import android.os.Bundle;import android.os.Environment;import android.support.v4.app.ActivityCompat;import android.support.v4.content.ContextCompat;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.Toast;import com.happy.R;import com.happy.core.BaseNativeActivity;import com.happy.core.NativeFlutterActivity;import com.happy.utils.AppUtils;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.util.Enumeration;import java.util.zip.ZipEntry;import java.util.zip.ZipFile;import io.flutter.util.PathUtils;public class UpdateActivity extends BaseNativeActivity implements View.OnClickListener {private static final String TAG = "UpdateFlutterActivity";private static final String FLUTTER_UPDATE_PACKAGE_DIR = "flutter_package";private static final String FLUTTER_UPDATE_PACKAGE_NAME = "Flutter-package.zip";private static final String FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator + FLUTTER_UPDATE_PACKAGE_DIR + File.separator + FLUTTER_UPDATE_PACKAGE_NAME;private static final String FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR = Environment.getExternalStorageDirectory().toString() + File.separator + "happy_flutter_update" + File.separator;private Button btnUpdate;private Button btnPage;private CompleteReceiver mDownloadCompleteReceiver = new CompleteReceiver();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_update);setActionToolBar();btnUpdate = findViewById(R.id.btn_update);btnPage = findViewById(R.id.btn_page);btnUpdate.setOnClickListener(this);btnPage.setOnClickListener(this);registerReceiver(mDownloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.btn_update:checkPermission();break;case R.id.btn_page:Intent intent = new Intent(this, NativeFlutterActivity.class);intent.setAction(Intent.ACTION_RUN);intent.putExtra("route", "flutter://test/update");startActivity(intent);break;}}private void onFlutterUpdatePackageDownLoaded() throws Exception {final File file = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE);if (file == null || !file.exists()) {Log.i(TAG, "flutter package file download error, check URL or network state");return;}File fileDir = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR);if (!fileDir.exists()) {fileDir.mkdirs();}ZipFile zipFile = new ZipFile(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE);Enumeration zipList = zipFile.entries();ZipEntry zipEntry;byte[] buffer = new byte[1024];while (zipList.hasMoreElements()) {zipEntry = (ZipEntry) zipList.nextElement();if (zipEntry.isDirectory()) {String destPath = FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR + zipEntry.getName();File dir = new File(destPath);dir.mkdirs();continue;}OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR + zipEntry.getName())));InputStream is = new BufferedInputStream(zipFile.getInputStream(zipEntry));int len;while ((len = is.read(buffer)) != -1) {out.write(buffer, 0, len);}out.flush();out.close();is.close();}zipFile.close();}public void copyToDataFlutterAssets() throws Exception {// String destPath = PathUtils.getDataDirectory(this.getApplicationContext()) + File.separator + "flutter_assets/";String destPath = PathUtils.getDataDirectory(this.getApplicationContext()) + File.separator;File[] files = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR).listFiles();for (File file : files) {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")) {AppUtils.copyFile(file.getPath(), destPath + File.separator + file.getName());}}Toast.makeText(this, "更新包更新成功", Toast.LENGTH_LONG).show();}private class CompleteReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {try {Toast.makeText(UpdateActivity.this, "更新包下载成功", Toast.LENGTH_LONG).show();onFlutterUpdatePackageDownLoaded();copyToDataFlutterAssets();} catch (Exception e) {e.printStackTrace();}} else {}}}private void deleteFlutterPackage() {File zipFile = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE);if (zipFile.exists()) {zipFile.delete();}File dir = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR);if (dir.isDirectory()) {for (File file : dir.listFiles()) {if (file.exists()) {file.delete();}}}}private void downloadFlutterUpdatePackage(String url) {DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));request.setTitle("Flutter动态更新");request.setDescription("Flutter动态更新包下载");request.setAllowedOverRoaming(false);request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);request.setDestinationInExternalPublicDir(FLUTTER_UPDATE_PACKAGE_DIR, FLUTTER_UPDATE_PACKAGE_NAME);// request.setDestinationUri(Uri.fromFile(new File(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE)));DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);dm.enqueue(request);}private void startDownLoadFlutterPackage() {deleteFlutterPackage();String url = getFlutterUpdatePackageUrl(this);downloadFlutterUpdatePackage(url);}private void checkPermission() {if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED&& ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {startDownLoadFlutterPackage();} else {ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, 1000);}}@Overridepublic void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {switch (requestCode) {case 1000: {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {startDownLoadFlutterPackage();}}}}private static String getFlutterUpdatePackageUrl(Context context) {if (AppUtils.isApkInDebug(context)) {return "https://xxxx/flutter-update-debug.zip";}return "https://xxxx/flutter-update-release.zip";}}

