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

在本次 文章中,简单分析了一下 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、重启应用,完成修复

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354