Flutter Android 端热修复(热更新)实践

4,877 阅读5分钟

上一篇 文章中,简单分析了一下 Flutter 在 Android 端的启动流程,虽然没有更深入的分析,但是我们可以了解到,对于 Flutter 端的 Dart VM 的启动等,是通过 Android 传递的资源(或者说路径)过去,Dart VM 加载这些资源完成初始化的,那么我们可以通过动态替换资源就可以达到热更新的目的。

注意:

  • 不同版本的 Flutter 代码与逻辑可能有所不同,但整体流程大同小异。
  • 同样的,不同版本 Flutter 编译之后的产物不同,
  • Release 模式 和 Debug 模式下的编译产物不同,这里以 Release 为例,代码也是 Release 版本的代码。

本次测试的开发环境:

  • Android Studio 3.5
  • Flutter 1.10.3-pre.39 chanel master
  • Dart 2.6.0

一、资源复制

通过之前文章的分析,可以知道,FlutterMain 这个类中,会传递指定资源路径,提供给 Dart VM 进行初始化。

这里面有两个重要的资源,一个是 libflutter.so ,一个是 libapp.so。 通过名字就可以看出来,libflutter.so 是框架相关的库,而 libapp.so 就是我们写的代码编译成的 so 库,我们就是要通过动态替换这个文件,达到热更新的目的。

为了能够让 Dart VM 加载我们修改之后的 so 库,我们肯定需要将修改后的 so 库放到 app 的私有目录下。这里直接从手机根目录下获取,当然从网络下载等都是同样的道理。 先定义一个辅助类,将文件复制到手机私有目录下。

public class FlutterFileUtils {
    ///将文件拷贝到私有目录
    public static String copyLibAndWrite(Context context, String fileName){
        try {
            File dir = context.getDir("libs", Activity.MODE_PRIVATE);
            File destFile = new File(dir.getAbsolutePath() + File.separator + fileName);
            if (destFile.exists() ) {
                destFile.delete();
            }

            if (!destFile.exists()){
                boolean res = destFile.createNewFile();
                if (res){

                    String path = Environment.getExternalStorageDirectory().toString();
                    FileInputStream is = new FileInputStream(new File(path + "/" + fileName));

                    FileOutputStream fos = new FileOutputStream(destFile);
                    byte[] buffer = new byte[is.available()];
                    int byteCount;
                    while ((byteCount = is.read(buffer)) != -1){
                        fos.write(buffer,0,byteCount);
                    }
                    fos.flush();
                    is.close();
                    fos.close();
                    return destFile.getAbsolutePath();
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
        return "";
    }

}

在程序启动的时候,我们调用这个方法,将文件复制过去,也就是在 MainActivity 的 onCreate 方法中。


  @Override
  protected void onCreate(Bundle savedInstanceState) {

    String path = FlutterFileUtils.copyLibAndWrite(MainActivity.this,"libapp_fix.so");
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }

复制文件等操作都需要读写权限,不要忘了。

二、自定义 FlutterActivity 和 FlutterActivityDelegat

在之前分析启动流程的文章中,提到过,MainActivity 继承自 FlutterActivity,而 FlutterActivity 只是一个代理类,真正的操作都是在 FlutterActivityDelegate 这个类中进行的,而在 FlutterActivityDelegate 中会调用 FlutterMain 中的方法进行 Dart VM 等的初始化。 因此我们要做的就是,修改 FlutterActivity 和 FlutterActivityDelegate 这两个类,以达到修改 FlutterMain 的目的。这里为了方便,只是简单的复制了一份代码,将 FlutterActivity 改为 HotFixFlutterActivity,FlutterActivityDelegate 改为 HotFixFlutterActivityDelegate ,然后修改里面的代码,当然还有其他的方法,这里不在演示。

1、修改 MainActivity 为继承自我们自己的 HotFixFlutterActivity
public class MainActivity extends HotFixFlutterActivity implements EasyPermissions.PermissionCallbacks
2、HotFixFlutterActivity 中将 FlutterActivityDelegate 替换为我们自己的 HotFixFlutterActivityDelegate
public class HotFixFlutterActivity extends Activity implements FlutterView.Provider, PluginRegistry, HotFixFlutterActivityDelegate.ViewFactory {

    private final HotFixFlutterActivityDelegate delegate = new HotFixFlutterActivityDelegate(this, this);
    private final FlutterActivityEvents eventDelegate;
    private final FlutterView.Provider viewProvider;
    private final PluginRegistry pluginRegistry;

    public HotFixFlutterActivity() {
        this.eventDelegate = this.delegate;
        this.viewProvider = this.delegate;
        this.pluginRegistry = this.delegate;
    }
    ...
    }
3、修改 HotFixFlutterActivityDelegate

代码修改到这里,当程序运行后,MainActivity 的 onCreate 方法里面会执行到 HotFixFlutterActivityDelegate 的 onCreate 方法中,而在这里,会调用 FlutterMain 里面的方法进行初始化操作,因此我们还需要修改 onCreate 这个方法。

onCreate 中默认调用的代码如下:

FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);

我们肯定需要自己定义一个类似的文件,修改里面的方法,来提供我们调用达到替换资源的目的。比如我们定义的类似的类叫 MyFlutterMain,那么 这里的代码修改为如下:

    public void onCreate(Bundle savedInstanceState) {
        if (Build.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());
        MyFlutterMain.startInitialization(this.activity.getApplicationContext());
        MyFlutterMain.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();
            }
        }

        if (!this.loadIntent(this.activity.getIntent())) {
            String appBundlePath = MyFlutterMain.findAppBundlePath();
            if (appBundlePath != null) {
                this.runBundle(appBundlePath);
            }

        }
    }

注意,这里多了一行:

 MyFlutterMain.startInitialization(this.activity.getApplicationContext());

主要是在ensureInitializationComplete这里,会进行一个判断:

  if (Looper.myLooper() != Looper.getMainLooper()) {
                throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
            } else if (sSettings == null) {
                throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
            } 

而只有在 startInitialization 之后,sSettings 才会被初始化,正常情况下,FlutterMain.startInitialization 这个方法是在 Application 的 onCreate 中调用的:

public class FlutterApplication extends Application {
    private Activity mCurrentActivity = null;

    public FlutterApplication() {
    }

    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }

    public Activity getCurrentActivity() {
        return this.mCurrentActivity;
    }

    public void setCurrentActivity(Activity mCurrentActivity) {
        this.mCurrentActivity = mCurrentActivity;
    }
}

因为我们没有修改这里的代码,所以我们要自己初始化一下,当然也可以自己在定义一个 Application 然后修改这里的代码。

三、加载自己的 so

这里主要是修改 MyFlutterMain 中的 ensureInitializationComplete 方法,加载我们自己复制到手机私用目录下的那个 so 就行了。

public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
   if (!isRunningInRobolectricTest) {
            if (Looper.myLooper() != Looper.getMainLooper()) {
                throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
            } else if (sSettings == null) {
                throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
            } else if (!sInitialized) {
                try {
                    if (sResourceExtractor != null) {
                        sResourceExtractor.waitForCompletion();
                    }
                    List<String> shellArgs = new ArrayList();
                    shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");
                    ApplicationInfo applicationInfo = getApplicationInfo(applicationContext);
                    shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + "libflutter.so");
                    if (args != null) {
                        Collections.addAll(shellArgs, args);
                    }

                    String kernelPath = null;
                    shellArgs.add("--aot-shared-library-name=" + sAotSharedLibraryName);
                    
                    File dir = applicationContext.getDir("libs", Activity.MODE_PRIVATE);
                    String libPath =  dir.getAbsolutePath() + File.separator + "libapp_fix.so";
                
                    shellArgs.add("--aot-shared-library-name=" + libPath);
                    shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext));
                    if (sSettings.getLogTag() != null) {
                        shellArgs.add("--log-tag=" + sSettings.getLogTag());
                    }

                    String appStoragePath = PathUtils.getFilesDir(applicationContext);
                    String engineCachesPath = PathUtils.getCacheDirectory(applicationContext);
                    FlutterJNI.nativeInit(applicationContext, (String[])shellArgs.toArray(new String[0]), (String)kernelPath, appStoragePath, engineCachesPath);
                    sInitialized = true;
                } catch (Exception var7) {
                    throw new RuntimeException(var7);
                }
            }
        }
    }

这里的路径和名称需要对应上,我已将修复后的 so 重命名为 libapp_fix.so ,并通过

  shellArgs.add("--aot-shared-library-name=" + sAotSharedLibraryName);

这行代码传递给底层。 同时,so 库路径通过如下代码传递:

File dir = applicationContext.getDir("libs", Activity.MODE_PRIVATE);
                    String libPath =  dir.getAbsolutePath() + File.separator + "libapp_fix.so";

                    shellArgs.add("--aot-shared-library-name=" + libPath);

至此,我们修改了代码,让程序初始化的时候,加载我们修改过的资源文件了。

四、测试

修复步骤:

1、打 release 包,拿到 libapp.so,重命名为 libapp_fix.so

由于上面的代码已经修改为加载私有目录下的 libapp_fix.so ,如果 app 直接运行肯定是不行的,因此我们需要先打一个 release 包,解压拿到里面的 libapp.so ,并修改为 libapp_fix.so,然后放到手机根目录下,这样程序启动后,会把这个文件复制到私有目录。

这里注意一下,打 release 包需要配置一下签名文件 。

代码就是初始化项目的代码,修改为点击按钮,数字加2 :

2、安装并运行 app

效果如下:

3、修改代码,重新打包

修改代码如下 :

同样,解压 apk,重命名 libapp.so 为 libapp_fix.so,放到手机根目录下。

4、重启应用,完成修复

先杀掉进程,重启应用,查看效果:

可以看到,已经完成了修复。

github

最后

欢迎关注「Flutter 编程开发」微信公众号 。