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中完成。

版权所属,禁止转载

(0)

相关推荐

  • Android so库的热更新问题

    本来想写资源的热修复的,虽然方案差不多已经完成了,但是考虑到一些敏感问题,资源修复就不写了.那就来写写so的热修复,其原理和class的修复是一样的,但是so的热修复的需求并不高,就当做学习吧. 首先来总结一下Android的ClassLoader方式的热更新,这种方式类的查找过程是通过BaseDexClassLoader来完成的,最终会通过成员变量DexPathList对象中的findClass方法来查找类,代码如下: public Class findClass(String name, L

  • Android添加glide库报错Error: Failed to resolve: com.android.support:support-annotations:26.0.2的解决

    前言 Glide是 Google推荐的图片加载库,它可以支持来自url,Android资源,文件,Uri中的图片加载,同时还支持gif图片的加载,以及各种图片显示前的bitmap处理(例如:圆角图片,圆形图片,高斯模糊,旋转,灰度等等),缓存处理,请求优先级处理,动画处理,缩略图处理,图片大小自定义等等.可谓是非常的强大. 在Glide的使用方面,它和Picasso的使用方法是比较相似的,并且他们的运行机制也有很多相似的地方,很多博文会把二者进行比较,此文也采取一样的方式,通过比较二者来学习他们

  • Android so的热升级尝试

    一.So的热升级尝试 在Android代码中,加载so库是通过调用System.loadLibrary函数实现的.但和Android的许多特性一样,只提供了加载,而没有卸载和更换等功能.为了研究能否实现卸载和升级等功能,首先要了解清楚JNI so加载的流程. 在以上流程中,使用dlopen加载so之后,会继续调用JNI_Onload函数,通过系统提供的RegisterNatives函数完成一些列初始化,向虚拟机注册so库提供的JNI函数.So库也可以不实现JNI_Onload函数,而是采用自动查

  • 浅析Android Studio 3.0 升级各种坑(推荐)

    点击 Check for Updates 增量更新: 下载完成,会提示更新 您将 Gradle 更新至 4.1: 这里建议您对老项目先暂时点击 Don't remind me on this project,以防有坑.当然我不入地狱谁入地狱,我点 Update,于是问题来了,一直处于下载中,不过,莫担心,我下载好了,公众号聊天界面回复「 gradle-4.1-all 」,下载 gradle-4.1-all.zip 文件,放到: 重启 Android Studio. gradle 目录: Mac系

  • android studio 3.0 升级 项目遇到的问题及更改思路(问题小结)

    Android Studio从3.0版本新增了许多功能,当然首当其冲就是从3.0版本新增了对 Kotlin 开发语言的支持,除此之外还有其他一些新功能,例如:Android Profiler (其中包含了: CPU Profiler.Memory Profiler.Network Profiler ),APK Debugger,Device File Explorer,Java 8 Language Features等. android studio 3.0版本升级问题修改: ===> 问题一

  • 详解Android中实现热更新的原理

    这篇文章就来介绍一下Android中实现热更新的原理. 一.ClassLoader 我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLoader类作为Android的默认的类加载器,PathClassLoader其实实现的就是简单的从文件系统中加载类文件.PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法

  • Android开发中数据库升级且表添加新列的方法

    本文实例讲述了Android开发中数据库升级且表添加新列的方法.分享给大家供大家参考,具体如下: 今天突然想到我们android版本升级的时候经常会遇到升级版本的时候在新版本中数据库可能会修改,今天我们就以数据库升级且表添加新列为例子写一个测试程序. 首先在要创建一个数据库,一般我们先创建一个DbHelper,继承SQLiteOpenHelper,构造函数我们使用传递版本号的: public DbHelper(Context context, String name, int version){

  • 解决Android studio 2.3升级到Android studio 3.0 后apt报错问题

    1.现象描述 原来项目在Android studio 2.3一切正常,升级3.0之后报如下错误: Error:Cannot choose between the following configurations of project :android_sdk: - debugApiElements - debugRuntimeElements - releaseApiElements - releaseRuntimeElements All of them match the consumer

  • Android studio2.3.3升级到3.1.2坑(小记)

    1.grade配置Error: Could not find com.android.tools.build:gradle:2.2.1. 解决方法与Maven仓库有点像:进入 D:\software\android\android-studio-ide-145.3276617-windows\android-studio\gradle\m2repository\com\android\tools\build\gradle 将项目中的build.gradle文件中 dependencies { c

  • Android解析XML文件升级APK的方法

    安装APK public class DownLoadApk { public static SharedPreferences sharedPrederences = null; //启动安装界面 public static void DownId(Context context, long downId){ DownloadManager mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOA

  • Android实现创建或升级数据库时执行语句

    本文实例讲述了Android创建或升级数据库时执行的语句,如果是创建或升级数据库,请使用带List参数的构造方法,带SQL语句的构造方法将在数据库创建或升级时执行. 具体程序代码如下: import java.util.List; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFac

  • android实现程序自动升级到安装示例分享(下载android程序安装包)

    复制代码 代码如下: //程序下载升级 zhouxiang@JavascriptInterfacepublic void UpdateCAECP(final String path){try{AlertDialog.Builder builder = new Builder((Context)obj);builder.setMessage("检测到有新版本发布,是否进行下载升级?");builder.setTitle("程序更新提示");builder.setPos

随机推荐