Android 开发基础 [腾讯 TMQ] Android so 的热升级尝试

匿名 · 2017年11月28日 · 508 次阅读

一、So 的热升级尝试

在 Android 代码中,加载 so 库是通过调用 System.loadLibrary 函数实现的。但和 Android 的许多特性一样,只提供了加载,而没有卸载和更换等功能。

为了研究能否实现卸载和升级等功能,首先要了解清楚 JNI so 加载的流程。网上有很多加载流程的解析,例如《Dalvik 虚拟机 JNI 方法的注册过程分析》(http://blog.csdn.net/luoshengyang/article/details/8923483)这篇文章中分析出的流程:

在以上流程中,使用 dlopen 加载 so 之后,会继续调用 JNI_Onload 函数,通过系统提供的 RegisterNatives 函数完成一些列初始化,向虚拟机注册 so 库提供的 JNI 函数。

So 库也可以不实现 JNI_Onload 函数,而是采用自动查找的方式。

Android 虚拟机会在首次调用 JNI 函数时按照 JNI 规范的命名规则自动查找。通过分析 Android 代码,这种方法最终也会调用到上图中的 dvmSetNativeFunc 等函数,将函数地址保存到虚拟机中供下次调用。

二、卸载及重新加载

如果想要提供热升级的能力,首先要做的是关闭已打开的 so 文件。但 Android 虚拟机没有提供 unloadLibrary 这样的接口,因此需要我们自己自己实现。

根据上一节的分析,loadLibrary 在 native 层加载文件使用的是 dlopen,与之对应的系统接口是 dlclose。而接下来的 RegisterNatives 由于没有对应的 unRegister,我们暂且先放一放,看看卸载的效果再来处理。

卸载 so

提供卸载能力的接口需要完成以下几项任务:

1、找到要卸载 so 的句柄;

2、调用 JNI_OnUnload;

3、调用 dlclose 卸载。

如下便是我们写出的卸载函数:

其中 dlclose 调用了 2 次,因为函数内的 dlopen 会增加 handle 的引用计数。

卸载之后如果我们先尝试调用原来的 JNI 函数,会发生什么事呢?显而易见会出现 crash。

究其原因,是由于 so 在加载或使用时已经在虚拟机中注册了 JNI 函数的地址,卸载后原地址变为非法地址,导致 crash。那我们再重新加载 so 会发生什么呢?

重新加载 so

分析代码可得知,由于 so 已经使用 System.loadLibrary 加载过,我们之前在卸载时也没有触及到 JNI 层,因此重复调用 loadLibrary 并不会重新加载 so。我们可以按照 dvmLoadNativeCode 的流程,在 native 层用 dlopen 重新加载 so。

按照之前的分析,很容易就能写出加载函数:

三、问题及解决

重新加载 so 后,再次调用原来的 JNI 函数。发现有时候会成功,但有时候也会 crash。经过追踪后注意到,报错的函数地址和卸载前一样,但 so 加载的地址变化了。

由于 dlopen 加载 so 时,并不能保证每次都加载在同一地址上。即使能够加载到同一地址,如果升级造成 so 文件变化,那函数地址也是不准确的。所以要使新的 so 工作,那我们也必须要设法更新虚拟机已经保存的函数指针,将其指向新加载 so 的正确地址。

这时候就需要我们之前忽略的 RegisterNatives 登场了,这个函数可以用来手动注册 JNI 函数地址。让我们重复与第一节文字相似但含义不同的这段话:

在以上流程中,so 库在使用 dlopen 加载后,还需要调用 JNI_Onload 函数,通过系统提供的 RegisterNatives 函数完成一些列初始化,向虚拟机注册新的 JNI 函数地址。

使用 RegisterNatives 注册后,即使 so 的地址发生变化,也能够更新虚拟机中记录的函数地址。

本篇小结

如果想要在运行时更新 so,则新的 so 文件必须要实现 JNI_Onload 函数,并且在 JNI_Onload 中调用系统提供的 RegisterNatives 注册所有的 JNI 函数,不能使用自动查找 JNI 函数名的方式。

四、其他问题

以上方案主要解决了 so 的卸载,重加载和 JNI 函数调用问题。但除了这些问题之外,so 代码的细节上还有许多要注意的地方。

CRASH

卸载 so 后,除了 JNI 函数的指针,其它指向 so 地址的指针也都会失效,包括指向静态变量,常量,native 函数的指针等。所有引用到该 so 地址的指针都需要更新。

内存和资源泄漏

native 代码中可能存在各种分配内存和资源的行为,使用以上方法更新 so 前,如果没有仔细处理这些资源,就会丢失原指针,造成内存泄漏。

1、malloc/mmap/shmem 等方式分配的内存。

2、socket, pipe, mutex, thread 等各种系统资源。

3、使用 NewGlobalRef 分配并持有 Java 对象,丢失指针后会造成虚拟机的 Java 内存泄漏。

综上所述,对于所有可能丢失,造成泄露的资源,必须在卸载 so 前设法保存或删除。这些工作可以在卸载时调用的 JNI_OnUnload 中完成。

关注微信公众号腾讯移动品质中心 TMQ,获取更多测试干货!

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册