关于动态更新

  • Google 考虑自家 Android 应用安全和苹果策略, 本来在 2019 的 roadmap 里面有这样一项,roadmap公布后,过一段时间后面又移除了这一项,目前不是主航道。
  • Android 目前可以通过整包方式实现动态更新, iOS 目前还不支持。

Android Flutter 整包动态更新实现

Flutter 构建产物加载流程

  • Flutter Android 启动流程 一文中,我们知道了 Android Flutter 启动流程,启动时,会去加载 data/data/{AppPackage}/app_flutter 下面如下文件。
  1. isolate_snapshot_instr - 应用程序指令段
  2. isolate_snapshot_data - 应用程序数据段
  3. vm_snapshot_instr - DartVM 指令段
  4. vm_snapshot_data - DartVM 数据段

apk-update.png

Flutter 构建产物

  • Flutter Android 混合工程实践 一文中,详细阐述了生成 Flutter 构建产物的流程。我们可以把构建产物打成压缩包上传到 CDN,然后 Native 通过下载服务器 Flutter 整体更新包进行更新。

**

  1. import android.Manifest;
  2. import android.app.DownloadManager;
  3. import android.content.BroadcastReceiver;
  4. import android.content.Context;
  5. import android.content.Intent;
  6. import android.content.IntentFilter;
  7. import android.content.pm.PackageManager;
  8. import android.net.Uri;
  9. import android.os.Bundle;
  10. import android.os.Environment;
  11. import android.support.v4.app.ActivityCompat;
  12. import android.support.v4.content.ContextCompat;
  13. import android.util.Log;
  14. import android.view.View;
  15. import android.widget.Button;
  16. import android.widget.Toast;
  17. import com.happy.R;
  18. import com.happy.core.BaseNativeActivity;
  19. import com.happy.core.NativeFlutterActivity;
  20. import com.happy.utils.AppUtils;
  21. import java.io.BufferedInputStream;
  22. import java.io.BufferedOutputStream;
  23. import java.io.File;
  24. import java.io.FileOutputStream;
  25. import java.io.InputStream;
  26. import java.io.OutputStream;
  27. import java.util.Enumeration;
  28. import java.util.zip.ZipEntry;
  29. import java.util.zip.ZipFile;
  30. import io.flutter.util.PathUtils;
  31. public class UpdateActivity extends BaseNativeActivity implements View.OnClickListener {
  32. private static final String TAG = "UpdateFlutterActivity";
  33. private static final String FLUTTER_UPDATE_PACKAGE_DIR = "flutter_package";
  34. private static final String FLUTTER_UPDATE_PACKAGE_NAME = "Flutter-package.zip";
  35. 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;
  36. private static final String FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR = Environment.getExternalStorageDirectory().toString() + File.separator + "happy_flutter_update" + File.separator;
  37. private Button btnUpdate;
  38. private Button btnPage;
  39. private CompleteReceiver mDownloadCompleteReceiver = new CompleteReceiver();
  40. @Override
  41. protected void onCreate(Bundle savedInstanceState) {
  42. super.onCreate(savedInstanceState);
  43. setContentView(R.layout.activity_update);
  44. setActionToolBar();
  45. btnUpdate = findViewById(R.id.btn_update);
  46. btnPage = findViewById(R.id.btn_page);
  47. btnUpdate.setOnClickListener(this);
  48. btnPage.setOnClickListener(this);
  49. registerReceiver(mDownloadCompleteReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
  50. }
  51. @Override
  52. public void onClick(View v) {
  53. switch (v.getId()) {
  54. case R.id.btn_update:
  55. checkPermission();
  56. break;
  57. case R.id.btn_page:
  58. Intent intent = new Intent(this, NativeFlutterActivity.class);
  59. intent.setAction(Intent.ACTION_RUN);
  60. intent.putExtra("route", "flutter://test/update");
  61. startActivity(intent);
  62. break;
  63. }
  64. }
  65. private void onFlutterUpdatePackageDownLoaded() throws Exception {
  66. final File file = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE);
  67. if (file == null || !file.exists()) {
  68. Log.i(TAG, "flutter package file download error, check URL or network state");
  69. return;
  70. }
  71. File fileDir = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR);
  72. if (!fileDir.exists()) {
  73. fileDir.mkdirs();
  74. }
  75. ZipFile zipFile = new ZipFile(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE);
  76. Enumeration zipList = zipFile.entries();
  77. ZipEntry zipEntry;
  78. byte[] buffer = new byte[1024];
  79. while (zipList.hasMoreElements()) {
  80. zipEntry = (ZipEntry) zipList.nextElement();
  81. if (zipEntry.isDirectory()) {
  82. String destPath = FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR + zipEntry.getName();
  83. File dir = new File(destPath);
  84. dir.mkdirs();
  85. continue;
  86. }
  87. OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR + zipEntry.getName())));
  88. InputStream is = new BufferedInputStream(zipFile.getInputStream(zipEntry));
  89. int len;
  90. while ((len = is.read(buffer)) != -1) {
  91. out.write(buffer, 0, len);
  92. }
  93. out.flush();
  94. out.close();
  95. is.close();
  96. }
  97. zipFile.close();
  98. }
  99. public void copyToDataFlutterAssets() throws Exception {
  100. // String destPath = PathUtils.getDataDirectory(this.getApplicationContext()) + File.separator + "flutter_assets/";
  101. String destPath = PathUtils.getDataDirectory(this.getApplicationContext()) + File.separator;
  102. File[] files = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR).listFiles();
  103. for (File file : files) {
  104. if (file.getPath().contains("isolate_snapshot_data")
  105. || file.getPath().contains("isolate_snapshot_instr")
  106. || file.getPath().contains("vm_snapshot_data")
  107. || file.getPath().contains("vm_snapshot_instr")) {
  108. AppUtils.copyFile(file.getPath(), destPath + File.separator + file.getName());
  109. }
  110. }
  111. Toast.makeText(this, "更新包更新成功", Toast.LENGTH_LONG).show();
  112. }
  113. private class CompleteReceiver extends BroadcastReceiver {
  114. @Override
  115. public void onReceive(Context context, Intent intent) {
  116. if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
  117. try {
  118. Toast.makeText(UpdateActivity.this, "更新包下载成功", Toast.LENGTH_LONG).show();
  119. onFlutterUpdatePackageDownLoaded();
  120. copyToDataFlutterAssets();
  121. } catch (Exception e) {
  122. e.printStackTrace();
  123. }
  124. } else {
  125. }
  126. }
  127. }
  128. private void deleteFlutterPackage() {
  129. File zipFile = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE);
  130. if (zipFile.exists()) {
  131. zipFile.delete();
  132. }
  133. File dir = new File(FLUTTER_UPDATE_PACKAGE_LOCAL_FILE_DIR);
  134. if (dir.isDirectory()) {
  135. for (File file : dir.listFiles()) {
  136. if (file.exists()) {
  137. file.delete();
  138. }
  139. }
  140. }
  141. }
  142. private void downloadFlutterUpdatePackage(String url) {
  143. DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
  144. request.setTitle("Flutter动态更新");
  145. request.setDescription("Flutter动态更新包下载");
  146. request.setAllowedOverRoaming(false);
  147. request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
  148. request.setDestinationInExternalPublicDir(FLUTTER_UPDATE_PACKAGE_DIR, FLUTTER_UPDATE_PACKAGE_NAME);
  149. // request.setDestinationUri(Uri.fromFile(new File(FLUTTER_UPDATE_PACKAGE_LOCAL_ZIP_FILE)));
  150. DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
  151. dm.enqueue(request);
  152. }
  153. private void startDownLoadFlutterPackage() {
  154. deleteFlutterPackage();
  155. String url = getFlutterUpdatePackageUrl(this);
  156. downloadFlutterUpdatePackage(url);
  157. }
  158. private void checkPermission() {
  159. if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
  160. && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
  161. startDownLoadFlutterPackage();
  162. } else {
  163. ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, 1000);
  164. }
  165. }
  166. @Override
  167. public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
  168. switch (requestCode) {
  169. case 1000: {
  170. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
  171. startDownLoadFlutterPackage();
  172. }
  173. }
  174. }
  175. }
  176. private static String getFlutterUpdatePackageUrl(Context context) {
  177. if (AppUtils.isApkInDebug(context)) {
  178. return "https://xxxx/flutter-update-debug.zip";
  179. }
  180. return "https://xxxx/flutter-update-release.zip";
  181. }
  182. }

Android 动态更新效果演示

flutter_update.mp4 (1.36MB)