关于 Tinker 用于 Flutter 热更新的思考

最近一篇文章提到使用 Tinker 替换 so 来实现 Android 平台上 Flutter 热更新。为什么 Tinker 可以更新/替换 so,它是如何实现换的?这个方案用于 Flutter 可能会存在哪些限制,原因是什么?一起来研究下吧。篇幅较长,水平有限,如有疏漏欢迎指正。

[TOC]

本文围绕 Tinker so 替换技术用于 Flutter 更新来讨论以下几个问题:

  • Tinker 如何实现 so 更新/替换?
  • System.load() 的执行流程是怎样的?
  • Flutter 大致的编译流程是怎样的?
  • Flutter 编译产物是什么,有什么变更?
  • so 替换如何应用于 Flutter 热更新?
  • Flutter 及其编译产物的变更对上述方案有哪些潜在的影响?

前言

Tinker 是 Android 平台上一个流行的热更新框架,官网介绍如下:

Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.

Tinker 更新 dex 的原理如下,

Tinker 原理

从介绍及原理图可以看出 Tinker 的主打功能是通过差分包更新 dex 来实现热更新,介绍中虽然简短地提到支持 library 热修复,但原理图中并没有给出相关内容。

Tinker 与其他主流热修复框架功能特性对比如下,

Tinker 功能特性

(图片来自 Tinker wiki)

只有 Tinker 支持 so 替换,是不是非常特别?我好奇它是如何实现的。

Tinker 如何更新 so 库

这是 Tinker API 概览:

Tinker API 概览

TinkerLoadLibrary 负责加载更新/替换后的 so,大致过程如下:

TinkerLoadLibrary

实际上 Tinker 会根据一些条件来选择不同的加载过程,限于篇幅这里我们只讨论左边这种情况。涉及到的主要代码是:

TinkerLoadLibrary.loadLibraryFromTinker(Context context, String relativePath, String libName) 的流程如下:

TinkerLoadLibrary 加载 so

  • 第一步是条件判断,检查是否支持加载 so、是否已构建 TinkertinkerLoadResult 是否存在,任一条件失败就直接结束 (忽略, 我们不关注)
  • 第二步是遍历 tinkerLoadResult.libs,从中找到跟 relativePath 匹配的那一项并构建出 patchLibraryPath
  • 第三步,通过 MD5 校验则调用 System.load() 加载 so 文件(由 patchLibraryPath 指定),否则结束

单看这个流程的话,我们很快就能得出结论:Tinker 更新/替换 so,本质上不过是对 System.load() 的包装。

所以剩下的问题是梳理这里的 tinkerLoadResult.libs 到底指向了哪个文件?那个文件是如何来的?

继续分析之前先了解 Tinker 的几个背景知识:

  • 第一,Tinker 合并差分包后生成新的 so 保存在相应目录下。目录名类似于 tinker/patch-641e634c/lib

Tinker so 文件目录结构

1
2
3
4
5
6
7
8
9
10
11
12
import com.tencent.tinker.anno.DefaultLifeCycle;

@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
...
@Override
public void onCreate() {
Log.d(TAG, "onCreate");
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
*
* Generated application for tinker life cycle
*
*/
public class %APPLICATION% extends TinkerApplication {

public %APPLICATION%() {
super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
}
}

简单总结一下:

  • @DefaultLifeCycle 注解器从 SampleApplicationLike 生成 SampleApplication
  • 生成的 SampleApplication 继承自 TinkerApplication
  • SampleApplicationLikeSampleApplication 的代理 (理解这个关系需要留意模板文件中的 %APPLICATION_LIFE_CYCLE% 以及 TinkerApplication 构造方法第二个参数)

有了这些背景知识,再回头看 tinkerLoadResult.libs

第一步,应用启动时调用 TinkerApplication.loadTinker(),这个方法反射调用 TinkerLoader.tryLoad()。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class TinkerApplication extends Application {
private Intent tinkerResultIntent;

private void onBaseContextAttached(Context base) {
...
loadTinker();
...
}

private void loadTinker() {
try {
//reflect tinker loader, because loaderClass may be define by user!
Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, TinkerApplication.class.getClassLoader());
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
} catch (Throwable e) {
//has exception, put exception error code
...
}
}
}

第二步,TinkerLoader.tryLoad() 最终调用 TinkerSoLoader 来加载 so 文件。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TinkerLoader extends AbstractTinkerLoader {
@Override
public Intent tryLoad(TinkerApplication app) {
Log.d(TAG, "tryLoad test test");
Intent resultIntent = new Intent();

long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
...

final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);

if (isEnabledForNativeLib) {
//tinker/patch.info/patch-641e634c/lib
boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!libCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
return;
}
}
...
}
}

第三步,TinkerSoLoader.checkComplete() 生成一个 HashMap 对象用于保存 so 文件相关的信息。而 HashMap 本身又保存在 Intent 中,字段名为 intent_patch_libs_path。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TinkerSoLoader {
public static boolean checkComplete(String directory,
ShareSecurityCheck securityCheck, Intent intentResult) {
...
//tinker//patch-641e634c/lib
String libraryPath = directory + "/" + SO_PATH + "/";

HashMap<String, String> libs = new HashMap<>();

for (ShareBsDiffPatchInfo info : libraryList) {
...
String middle = info.path + "/" + info.name;

//unlike dex, keep the original structure
libs.put(middle, info.md5);
}
...

//if is ok, add to result intent
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_LIBS_PATH, libs);
return true;
}
}

以这里的 so 文件为例,

Tinker so 文件目录结构

HashMap 中包含如下数据:

  • key - tinker/patch-3a9eeb8d/lib/lib/armeabi/libapp.so
  • value - 该 so 文件的 MD5

注意,第三步中用于保存 HashMap 的 Intent 即第一步中的 TinkerApplication.tinkerResultIntent


第四步,这一步涉及到的内容较多,是第三步和第五步之间的衔接,整理如下供参考。

两个关键点:

  • TinkerApplication.createInlineFence() 反射方式创建 TinkerApplicationInlineFence
  • TinkerApplicationInlineFence.createDelegate() 反射方式创建 ApplicationLike (ApplicationLikeTinkerApplication 的代理)

主要流程:

1
2
3
4
5
6
TinkerApplication.attachBaseContext()
-> TinkerApplication.onBaseContextAttached()
-> ITinkerInlineFenceBridge.attachBaseContext()
-> TinkerApplicationInlineFence.attachBaseContext()
-> TinkerApplicationInlineFence.attachBaseContextImpl_$noinline$()
-> ApplicationLike.onBaseContextAttached()

从这个流程可以看出 TinkerApplicationApplicationLike 之间的关联。

再来结合 demo 来看 ApplicationLike.onBaseContextAttached() 这个回调,观察 ApplicationLike 是如何跟 TinkerInstaller 关联起来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
...
TinkerManager.installTinker(this);
...
}
}

public class TinkerManager {
public static void installTinker(ApplicationLike appLike) {
...
TinkerInstaller.install(appLike,
loadReporter, patchReporter, patchListener,
SampleResultService.class, upgradePatchProcessor);
...
}
}

第五步,调用 TinkerInstaller.install() 方法,一系列调用后将生成 TinkerLoadResult 对象。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class TinkerInstaller {
public static Tinker install(ApplicationLike applicationLike) {
Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
Tinker.create(tinker);
tinker.install(applicationLike.getTinkerResultIntent());
return tinker;
}
}

public class Tinker {
TinkerLoadResult tinkerLoadResult;

public void install(Intent intentResult, Class<? extends AbstractResultService> serviceClass, AbstractPatch upgradePatch) {
...
tinkerLoadResult = new TinkerLoadResult();
tinkerLoadResult.parseTinkerResult(getContext(), intentResult);
...
}
}

public class TinkerLoadResult {
//@Nullable
public HashMap<String, String> libs;
public boolean parseTinkerResult(Context context, Intent intentResult) {

...
switch (loadCode) {
case ShareConstants.ERROR_LOAD_OK:
TinkerLog.i(TAG, "oh yeah, tinker load all success");
tinker.setTinkerLoaded(true);
// get load dex
dexes = ShareIntentUtil.getIntentPatchDexPaths(intentResult);
libs = ShareIntentUtil.getIntentPatchLibsPaths(intentResult);
...
}
}
}

从以上代码可以看到,TinkerLoadResult.libs 来自于 ApplicationLike.getTinkerResultIntent() 返回的 Intent 中保存 HashMap 对象,其字段名为 intent_patch_libs_path

前面提到过 ApplicationLikeTinkerApplication 的代理。ApplicationLike.getTinkerResultIntent() 返回的 Intent 其实就是 TinkerApplication.tinkerResultIntent,具体可参考 tinker/TinkerApplicationInlineFence.java

TinkerLoadResult 代表加载结果,其 libs 字段保存了 so 文件地址。如果以 TinkerLoadResult 作为关注点,整个过程总结如下:

TinkerLoadResult

  1. TinkerApplication 及相关类是生产者,负责生产 TinkerLoadResult
  2. Tinker 是管理者,负责管理 TinkerLoadResult
  3. TnikerLoadLibrary 是消费者,负责消费 TinkerLoadResult

可见,无论这个过程多复杂,Tinker 最终仍然是调用 System.load() 加载 so 库。

System.load() 简介

前面提到 Tinker 更新/替换 so 库其实不过是对 System.load() 方法的包装。Flutter 引擎初始化 时也调用了 System.load() 方法。所以我们有必要了解这个方法。

考虑到已经有很多优秀的文章很好地总结了相关知识,我这里直接搬一些上来。

loadLibrary动态库加载过程分析 提到加载动态库的调用栈如下:

1
2
3
4
5
6
7
8
System.loadLibrary()
-> Runtime.loadLibrary()
-> Runtime.doLoad()
-> Runtime_nativeLoad() //1
-> LoadNativeLibrary() //2
-> dlopen()
-> dlsym()
-> JNI_OnLoad()

(注:无论 System.loadLibrary() 还是 System.load() 最后都会调用到 LoadNativeLibrary() 方法,所以这里对二者未加区分)

调用栈中最主要代码是:

LoadNativeLibrary() 方法执行如下几个操作:

  • 通过 dlopen() 打开动态共享库;
  • 通过 dlsym 获取 JNI_OnLoad 符号所对应的方法;
  • 调用该加载库中的 JNI_OnLoad() 方法

dlopen()dlsym() 是 Linux 系统为动态链接器提供的接口,允许应用程序在运行时加载和链接动态库。这个 demo 演示了这些接口的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// hello.c
typedef int (*SIMPLE_FUNC)();

int main() {
void *handle = dlopen("libworld.so", RTLD_NOW);
if (!handle) {
printf("error: %s\n", dlerror());
return -1;
}
void *symSayWorld = dlsym(handle, "sayWorld");
if (!symSayWorld) {
printf("error: %s\n", dlerror());
return -1;
}
SIMPLE_FUNC sayWorld = symSayWorld;
printf("%s", "hello");
sayWorld();
return 0;
}

// world.c
#include <stdio.h>

void sayWorld() {
printf("%s\n", ",world");
}

dlopen 的用法

从 so 库到 Flutter 热更新

将 so 更新/替换应用于 Flutter 热更新技术上完全是可行的。《深入理解计算机系统》第七章第 7.11 节(P468)中讨论了应用程序如何加载和链接共享库。

应用程序还可以在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。

书中还举了微软 Windows 应用更新的例子:

分发软件。微软 Windows 应用的开发者常常利用共享库来分发软件更新。他们生成一个共享库的新版本。然后用户可以下载,并用它替换当前的版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库

所以理论上只要 Flutter 不出于系统安全等原因阻止应用动态链接和加载更新后的共享库,动态链接技术同样可以用于热更新。但实践中可能遇到以下几个问题:

  • 首先你得要有 so !

我们知道 Dart 有 JIT 和 AOT 两种不同的编译模式,而 Flutter 是使用 Dart 作为开发语言,其编译产物在 debug 模式和 release 模式下并不相同。Dart 和 Flutter 都在快速演进,编译产物常常随版本发生变化。所以并不是所有编译模式或 Flutter 版本都有 so。没有 so,也就无法替换 so 文件来热更新。好在 Flutter v1.7 版本开始 release 模式下编译产物输出为 so 文件,所以 so 更新/替换方案变得可行

  • 其次是加载 so 的过程要可控

我们很容易控制如何加载自己开发的 so 库文件。但第三方库通常会主动加载自带的 so 库文件,典型的代码如下:

1
2
3
4
5
6
7
8
9
10
public class FooSdk {

// 在 static 初始化语句块中加载 libfoo.so
static {
System.loadLibrary('foo');
}

// libfoo.so 实现 bar() 方法
public native void bar();
}

第三方库通常会主动加载 so 的好处是对开发者透明和友好,坏处是不灵活不可控。Flutter 会不会也使用这种写死的加载方式呢?

我们会一步步详细展开,看看实践中是否存在这些问题。如果有,要如何克服?主要话题包括:

  • Flutter 编译流程及产物简介
  • Flutter 引擎初始化简介

Flutter 编译流程及产物

Dart 支持 JIT 和 AOT 两种编译模式。Flutter 在 debug 模式和 release 模式分别使用 JIT 和 AOT,前者开发调试速度快,后者运行效率高。

使用 Android Studio 或 VS code 来打包 Flutter 应用时,实际都会调用 flutter run 来构建 APK 包。关于 flutter run 命令浅谈Flutter构建中有比较详细的讨论。

忽略一些不必要的细节,我们可以简单地认为:

  • 编译 debug 包对应于 flutter build bundle 这条命令
  • 编译 release 包对应于 flutter build aot; flutter build bundle --precompiled 这两条命令

现在来动手实践一下,观察这些命令的编译产物。我的 Flutter 版本如下:

1
2
3
4
Flutter 1.10.15-pre.234 • channel master • https://github.com/flutter/flutter.git
Framework • revision e7236796bd (6 weeks ago) • 2019-10-24 03:21:06 -0400
Engine • revision 5ded729f6a
Tools • Dart 2.6.0 (build 2.6.0-dev.8.1 d43cd7e909)

flutter create flutter_empty_demo 创建一个新的项目,不对代码作任何改动,利用这个项目进行验证。

编译 debug 包

运行 flutter build bundle 并观察建产物。这条命令本质是通过以下代码构造出一条类似于 dart <参数1> <参数2> ... <参数n> 带复杂参数的命令。主要代码包括:

  • compile.dart - 对 Flutter SDK 中 dart 命令的包装,提供 KernelCompiler
  • bundle.dart - 基于 KernelCompiler 提供 BundleBuilder
  • build_bundle.dart - 基于 BundleBuilder 来实现 flutter build bundle 命令

这条命令没有任何提示,等待约2秒后项目根目录下多出 build/flutter_assets 文件夹。

debug 模式编译产物

  • kernel - kernel 是从 Dart 本身衍生出来的语言,用于 Dart 程序的中间格式。这里的 kernel_blob.bin 即我们应用中编写的 Dart 代码
  • snapshots - Dart 虚拟机的状态
  • assets - 图片、字体等资源

编译 release 包

运行 flutter build aot 并观察建产物。同样地,这条命令本质是通过以下代码构造出一条类似于 dart <参数1> <参数2> ... <参数n> 的带复杂参数的命令。主要代码包括:

  • build.dart - 对 Flutter SDK 中 dart 命令的包装,提供 AOTSnapshotter,可以将 Dart 代码编译成 so
  • build_aot.dart - 基于 BundleAOTSnapshotterBuilder 来实现 flutter build aot 命令

这条命令耗时比编译 debug 包稍长,输出如下:

flutter build aot
执行成功后项目根目录下多出 build/aot 目录,我们的 Dart 代码被编译成这里的 app.so

build /aot 目录
app.so 是可以将 Tinker so 替换用于 Flutter 更新的关键之一。实际上, Flutter 一开始并不是编译成 app.so,而是多次调整直到 1.7 版本开始输出为 app.so,直到目前最新版本 (V1.9)。

Flutter 编译产物演进

(注:图片来自 浅谈Flutter构建)

通过对比相关代码可以看到,编译产物的变更其实只不过是 snapshot_kind 参数的调整:从 V1.7 的 app-aot-blobs 调整到 V1.9 的 app-aot-elf

Flutter V1.9 AOTSnapshotter 代码片断

1
2
3
4
5
6
7
8
9
10
final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin_x64) {
...
} else {
final String aotSharedLibrary = fs.path.join(outputDir.path, 'app.so');
outputPaths.add(aotSharedLibrary);
genSnapshotArgs.add('--snapshot_kind=app-aot-elf');
genSnapshotArgs.add('--elf=$aotSharedLibrary');
genSnapshotArgs.add('--strip');
}

Flutter V1.5 AOTSnapshotter 代码片断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
if (buildSharedLibrary || platform == TargetPlatform.ios) {
...
} else {
// Blob AOT snapshot.
final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');
final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr');
final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');
outputPaths.addAll(<String>[vmSnapshotData, isolateSnapshotData, vmSnapshotInstructions, isolateSnapshotInstructions]);
genSnapshotArgs.addAll(<String>[
'--snapshot_kind=app-aot-blobs',
'--vm_snapshot_data=$vmSnapshotData',
'--isolate_snapshot_data=$isolateSnapshotData',
'--vm_snapshot_instructions=$vmSnapshotInstructions',
'--isolate_snapshot_instructions=$isolateSnapshotInstructions',
]);
}

Flutter 引擎初始化

FlutterMain 用于初始化 Flutter 引擎。这里只讨论跟 libapp.so 加载相关的部分 (libapp.so 即上文的 app.so)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* A class to intialize the Flutter engine.
*/
public class FlutterMain {
private static final String TAG = "FlutterMain";

private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
// Mutable because default values can be overridden via config properties
private static String sAotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME;

/**
* Initialize our Flutter config values by obtaining them from the
* manifest XML file, falling back to default values.
*/
private static void initConfig(@NonNull Context applicationContext) {
Bundle metadata = getApplicationInfo(applicationContext).metaData;

// There isn't a `<meta-data>` tag as a direct child of `<application>` in
// `AndroidManifest.xml`.
if (metadata == null) {
return;
}

sAotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME);
...
}

/**
* Blocks until initialization of the native system has completed.
* @param applicationContext The Android application context.
* @param args Flags sent to the Flutter runtime.
*/
public static void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
...
List<String> shellArgs = new ArrayList<>();
String kernelPath = null;
if (BuildConfig.DEBUG) {
String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + sFlutterAssetsDir;
kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB;
shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + sVmSnapshotData);
shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + sIsolateSnapshotData);
} else {
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + sAotSharedLibraryName);

// 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 + "=" + applicationInfo.nativeLibraryDir + File.separator + sAotSharedLibraryName);
}
...
}
}

带你不到80行代码搞定Flutter热更新 使用反射强行修改 FlutterMain.sAotSharedLibraryName,使其指向由 Tinker 管理的 so,从而实现热更新。不过从 V1.9 代码看似乎有更优雅的方案:

  • 首先 sAotSharedLibraryName 并非 final 字段,为的就是能从文件中读取配置
  • 其次,initConfig() 方法的注释提到在 AndroidManifest.xml 中添加一个名为 FlutterMain.aot-shared-library-name<meta-data> 可以为 sAotSharedLibraryName 指定值
  • 最后,FlutterMain.ensureInitializationComplete() 方法的第二个参数 args 也会被添加到 shellArgs 变量中。所以可以按照格式添加额外的参数 --AOT_SHARED_LIBRARY_NAME=<so文件> 让 Flutter 引擎加载指定的 so 库。

so 替换应用于 Flutter 热更新是可行的,原因包括:

  • Flutter 1.7 之后编译产物输出为 libapp.so 文件,完全可以直接复用 Tinker 作为热更新框架
  • 能使用反射等方式将 Flutter 加载 so 库的过程变得可控,比如从指定的路径加载更新/替换后的 so 库

总结

最后来简单回答下前面提出的几个问题,作为总结。

  • Tinker 如何实现 so 更新/替换?

Tinker 实际上是调用 System.load() 来 so 实现更新/替换。 虽然 so 更新/替换并不是 Tinker 独有的,我们完全可以直接调用 System.load() 来实现 so 更新,但考虑 Tinker 的 patch 包生成和合并非常完善(可以生成很小的差分包),工程实际中是个不错的选择。

  • System.load() 的执行流程是怎样的?

System.load() 调用 Runtime 相关方法,最终进入到 Java 虚拟机中的 java_vm_ext.LoadNativeLibrary() 方法。该方法调用 dlopen()dlsysm() 加载 so 文件

  • Flutter 大致的编译流程是怎样的?

调用 dart 命令将应用中的 Dart 代码编译成某种类型的 snapshot 文件,常用的 snapshot-kind 包括 kernelapp-aot-elfapp-aot-blobs。 (这里仍然有个疑问,为什么 Dart 官网并没有提到 app-aot-elfapp-aot-blobs 这两种类型的 snapshot-kind?)

  • Flutter 编译产物是什么,有什么变更?

编译产物包括 1. 资源 (assets) 2. 代码 (kernel/so库 和 snapshots)

  • so 替换如何应用于 Flutter 热更新?

通过反射等方式修改 FlutterMain.sAotSharedLibraryName 字段,将缺省的 libapp.so 替换为指定的 so

  • Flutter 及其编译产物的变更对上述方案有哪些潜在的影响?

Fluter 1.7 版本之前编译产物并非 so 文件,所以不能直接利用 so 替换来实现热更新。1.7 版本之后编译产物输出为 so 文件,可以利用 so 替换来实现热更新。

但是要注意,so 替换方案虽然可行,Flutter 的代码仍在频繁变更中,所以使用反射等手段时要尤其注意 Flutter 版本带来的影响。比方说,Flutter 1.9 中反射方式修改 FlutterMain.sAotSharedLibraryName 是可行的,但 Flutter 1.12 中则会失败。原因很简单:1.12 新版本中 FlutterMain 被重构成 FlutterMainFlutterLoader,原先用于加载 libapp.so 的代码现移到 FlutterLoader 中了,包括 sAotSharedLibraryName 字段。具体可参考源码。

参考