Android so的热升级尝试
一、so的热升级尝试
在android代码中,加载so库是通过调用system.loadlibrary函数实现的。但和android的许多特性一样,只提供了加载,而没有卸载和更换等功能。为了研究能否实现卸载和升级等功能,首先要了解清楚jni so加载的流程。
在以上流程中,使用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卸载。
如下便是我们写出的卸载函数:
void jnicall java_com_example_unloader_unload(jnienv* env, jobject obj) { void* handle = dlopen(“/data/data/com.example.unloader/lib/libtest.so”, rtld_global); if(!handle) return; logd(“unload so: 0x%x\n”, (unsigned int)handle); void* symbol = dlsym(handle, “jni_onunload”); if(symbol) { onloadfunc func = (onloadfunc)symbol; javavm* jvm = 0; (*env)->getjavavm(env, &jvm); if(jvm) (*func)(jvm, 0); } int result = dlclose(handle); logd(“unload result %d\n”, result); result = dlclose(handle); result = dlclose(handle); logd(“unload result %d\n”, result); }
其中dlclose调用了2次,因为函数内的dlopen会增加handle的引用计数。
卸载之后如果我们先尝试调用原来的jni函数,会发生什么事呢?显而易见会出现crash。
究其原因,是由于so在加载或使用时已经在虚拟机中注册了jni函数的地址,卸载后原地址变为非法地址,导致crash。那我们再重新加载so会发生什么呢?
重新加载so
分析代码可得知,由于so已经使用system.loadlibrary加载过,我们之前在卸载时也没有触及到jni层,因此重复调用loadlibrary并不会重新加载so。我们可以按照dvmloadnativecode的流程,在native层用dlopen重新加载so。
按照之前的分析,很容易就能写出加载函数:
void jnicall java_com_example_unloader_load(jnienv* env, jobject obj) { void* handle = dlopen(“/data/data/com.example.unloader/lib/libtest.so”, rtld_global); if(!handle) return; logd(“load so: 0x%x\n”, (unsigned int)handle); void* symbol = dlsym(handle, “jni_onload”); if(symbol) { onloadfunc func = (onloadfunc)symbol; javavm* jvm = 0; (*env)->getjavavm(env, &jvm); if(jvm) (*func)(jvm, 0); } }
三、问题及解决
重新加载so后,再次调用原来的jni函数。发现有时候会成功,但有时候也会crash。经过追踪后注意到,报错的函数地址和卸载前一样,但so加载的地址变化了。
由于dlopen加载so时,并不能保证每次都加载在同一地址上。即使能够加载到同一地址,如果升级造成so文件变化,那函数地址也是不准确的。所以要使新的so工作,那我们也必须要设法更新虚拟机已经保存的函数指针,将其指向新加载so的正确地址。
这时候就需要我们之前忽略的registernatives登场了,这个函数可以用来手动注册jni函数地址。让我们重复与第一节文字相似但含义不同的这段话:
在以上流程中,so库在使用dlopen加载后,还需要调用jni_onload函数,通过系统提供的registernatives函数完成一些列初始化,向虚拟机注册新的jni函数地址。
static jninativemethod gmethods[] = { { “foo”, “()v”, (void*)java_com_tencent_example_foo }, }; // register several native methods for one class. static int registernativemethods(jnienv* env, const char* classname, jninativemethod* gmethods, int nummethods) { jclass clazz = (*env)->findclass(env, classname); if (clazz == null) { return jni_false; } if ((*env)->registernatives(env, clazz, gmethods, nummethods) < 0) { return jni_false; } return jni_true; }
使用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中完成。
版权所属,禁止转载
推荐阅读
-
Android Studio中导入JNI生成的.so库的实现方法
-
Android so的热升级尝试
-
Android so库的热更新问题
-
android studio 3.0 升级 项目遇到的问题及更改思路(问题小结)
-
Android增量升级的方法和原理详细介绍
-
Android Studio升级到3.0后遇到的坑
-
android studio怎么添加.so文件?android studio加载so文件的方法
-
Android Studio 升级到3.0后输入法中文状态下无法选词的终极解决方案
-
谷歌副总裁透露:已经有75%的Pixel设备升级至Android 9.0
-
Android app targetSdk升级到27碰到的一个bug补充说明