Flutter 热修复实现

1,505 阅读3分钟

在移动端开发我们一般非常需要我们的项目拥有热修复能力,而在flutter目前为止官方仍旧未支持热修复,本篇文章就探索下热修复这个功能如何实现。flutter版本基于1.22.5。要实现热修复需要对flutter原始项目和打包产物需要了解一下。flutter打包后会将我们的dart代码打进到libapp.so这个so文件,所以在我们启动的时候动态替换这个so文件就可以达到热修复。
flutter默认工程会配置一个io.flutter.app.FlutterApplication作为我们的application,这个类中只做了一件事,加载flutter相关的环境

public class FlutterApplication extends Application {
  @Override
  @CallSuper
  public void onCreate() {
    super.onCreate();
    //加载flutter相关环境
    FlutterInjector.instance().flutterLoader().startInitialization(this);
  }

  private Activity mCurrentActivity = null;

  public Activity getCurrentActivity() {
    return mCurrentActivity;
  }

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

所以我们需要关注的就是FlutterInjector这个类了

public final class FlutterInjector {

  private static FlutterInjector instance;

  public static FlutterInjector instance() {
    if (instance == null) {
      1.这里通过Builder构建则去构建实例
      instance = new Builder().build();
    }
    return instance;
  }

  @NonNull
  public FlutterLoader flutterLoader() {
    return flutterLoader;
  }

  public static final class Builder {
    private FlutterLoader flutterLoader;
    
    public Builder setFlutterLoader(@NonNull FlutterLoader flutterLoader) {
      this.flutterLoader = flutterLoader;
      return this;
    }

    private void fillDefaults() {
      if (flutterLoader == null) {
        flutterLoader = new FlutterLoader();
      }
    }
    
    public FlutterInjector build() {
      2.默认注入一个FlutterLoader实例
      fillDefaults();
      
      3.构建FlutterInjector实例,FlutterInjector持有flutterLoader
      return new FlutterInjector(shouldLoadNative, flutterLoader);
    }
  }
}

这个类的通过构建者模式将会构建出FlutterInjector这个类,而这个类关键就是去持有FlutterLoader这个类的实例,通过命名我们就很容易发现FlutterInjector的作用以单例注入到其他类中,而FlutterLoader就是重头戏,他的作用是加载相关资源,如果需要在任意地方获取flutrterLoader就可以使用FlutterInjector的flutterLoader()方法拿到。

/** Finds Flutter resources in an application APK and also loads Flutter's native library. */
public class FlutterLoader {
  private FlutterApplicationInfo flutterApplicationInfo;
  
  public void ensureInitializationComplete(
      @NonNull Context applicationContext, @Nullable String[] args) {
    try {
      InitResult result = initResultFuture.get();

      List<String> shellArgs = new ArrayList<>();
      shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");

      shellArgs.add(
          "--icu-native-lib-path="
              + flutterApplicationInfo.nativeLibraryDir
              + File.separator
              + DEFAULT_LIBRARY);
      if (args != null) {
        Collections.addAll(shellArgs, args);
      }

      String kernelPath = null;
    
    <------------------关键拼接开始
    shellArgs.add(
            "--" + AOT_SHARED_LIBRARY_NAME + "=" +  flutterApplicationInfo.aotSharedLibraryName);

        // Most devices can load the AOT shared library based on the library name
        // with no directory path.  Provide a fully qualified path to the library
        // as a workaround for devices where that fails.
      shellArgs.add(
            "--"
                + AOT_SHARED_LIBRARY_NAME
                + "="
                + flutterApplicationInfo.nativeLibraryDir
                + File.separator
                + flutterApplicationInfo.aotSharedLibraryName);
      关键拼接结束------------->          

      shellArgs.add("--cache-dir-path=" + result.engineCachesPath);
      if (!flutterApplicationInfo.clearTextPermitted) {
        shellArgs.add("--disallow-insecure-connections");
      }
      if (flutterApplicationInfo.domainNetworkPolicy != null) {
        shellArgs.add("--domain-network-policy=" + flutterApplicationInfo.domainNetworkPolicy);
      }
      if (settings.getLogTag() != null) {
        shellArgs.add("--log-tag=" + settings.getLogTag());
      }

      long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;

      if (FlutterInjector.instance().shouldLoadNative()) {
        FlutterJNI.nativeInit(
            applicationContext,
            shellArgs.toArray(new String[0]),
            kernelPath,
            result.appStoragePath,
            result.engineCachesPath,
            initTimeMillis);
      }

      initialized = true;
    } catch (Exception e) {
      Log.e(TAG, "Flutter initialization failed.", e);
      throw new RuntimeException(e);
    }
  }
  
  public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
  	flutterApplicationInfo = ApplicationInfoLoader.load(appContext);
  }
}


public final class FlutterApplicationInfo {
  private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";

  final String aotSharedLibraryName;
  final String nativeLibraryDir;

  public FlutterApplicationInfo(
      String aotSharedLibraryName,
      String vmSnapshotData,
      String isolateSnapshotData,
      String flutterAssetsDir,
      String domainNetworkPolicy,
      String nativeLibraryDir,
      boolean clearTextPermitted) {
    this.aotSharedLibraryName =
        aotSharedLibraryName == null ? DEFAULT_AOT_SHARED_LIBRARY_NAME : aotSharedLibraryName;

    this.nativeLibraryDir = nativeLibraryDir;
  }
}

flutterLoader中的关键方法ensureInitializationComplete拼接shell命令给dartVM启动我们的程序,拼接好后的关键内容是 -- aot-shared-library-name=libapp.so --aot-shared-library-name=nativeLibraryDir/libapp.so,我已我们就仅需要将我们下发的补丁so文件路径替换成FlutterApplicationInfo.aotSharedLibraryName字段的值,那么flutter启动后就是执行我们下发的内容了。由于FlutterLoader中的FlutterApplicationInfo是私有的并且FlutterApplicationInfo中的aotSharedLibraryName是final修饰的,所以我们不可能通过常规手段去修改FlutterApplicationInfo.aotSharedLibraryName的值,只能通过反射去修改了,所以我们现在我们实现的思路很清晰了就:

  1. 实现一个FlutterLoader的子类重写ensureInitializationComplete方法,修改FlutterApplicationInfo.aotSharedLibraryName
public class HIFlutterLoader extends FlutterLoader {
    private static HIFlutterLoader instance;
    private static final String FIX_SO = "libapp_fix.so";

    @NonNull
    public static HIFlutterLoader getInstance() {
        if (instance == null) {
            instance = new HIFlutterLoader();
        }
        return instance;
    }

    @Override
    public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
        File filesDir = applicationContext.getFilesDir();
        File soFile = new File(filesDir, FIX_SO);
        
        if (soFile.exists()) {
            try {
            	//1.拿到flutterApplicationInfo字段
                Field flutterApplicationInfoField = FlutterLoader.class.getDeclaredField("flutterApplicationInfo");
                flutterApplicationInfoField.setAccessible(true);
                FlutterApplicationInfo flutterApplicationInfo = (FlutterApplicationInfo) flutterApplicationInfoField.get(this);

		//2.拿到aotSharedLibraryName并修改为我们的so路径
                Field aotSharedLibraryNameField = FlutterApplicationInfo.class.getDeclaredField("aotSharedLibraryName");
                aotSharedLibraryNameField.setAccessible(true);
                aotSharedLibraryNameField.set(flutterApplicationInfo, soFile.getAbsolutePath());

                Log.i("HIFlutterLoader", "load so name:" + aotSharedLibraryNameField.get(flutterApplicationInfo));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        super.ensureInitializationComplete(applicationContext, args);
    }
}
  1. 定义一个Application类,类中将自己实现的FlutterLoader注入到FlutterInjector中
public class MApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        HIFlutterLoader flutterLoader = HIFlutterLoader.getInstance();

        FlutterInjector flutterInjector = new FlutterInjector.Builder().setFlutterLoader(flutterLoader).build();

        FlutterInjector.setInstance(flutterInjector);

        flutterLoader.startInitialization(this);
    }
}

现在我们已经完成了热修复核心的实现,为了验证我们可以通过将测试修复的libapp_fix.so放到我们的assets目录下,在application中的oncreate方法中将该文件复制到applicationContext.getFilesDir()/libapp_fix.so以验证是否可以工作。

public class MApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        File filesDir = getFilesDir();
        File fix = new File(filesDir, "libapp_fix.so");

        Log.i("MApplication", "libapp_fix.so path:" + fix.getAbsolutePath());
        if (!fix.exists()) {
            InputStream inputStream = null;
            OutputStream outputStream = null;

            try {
                inputStream = getResources().getAssets().open("libapp_fix.so");
                outputStream = new FileOutputStream(fix);
                byte[] buf = new byte[1024];

                int bytesRead;
                while ((bytesRead = inputStream.read(buf)) > 0) {
                    outputStream.write(buf);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        HIFlutterLoader flutterLoader = HIFlutterLoader.getInstance();

        FlutterInjector flutterInjector = new FlutterInjector.Builder().setFlutterLoader(flutterLoader).build();

        FlutterInjector.setInstance(flutterInjector);

        flutterLoader.startInitialization(this);
    }
}