随着App功能的不断增多,Native层的代码规模也在迅速膨胀,为了Native层的代码结构清晰,会按照模块分别构建成独立的so库,使用一个JNI层so库引用其他实现具体功能的功能实现so库,Java层只加载这个JNI层so库,来间接调用功能实现so库。
so库之间通过引用头文件和运行时指定共享库依赖的方式形成了依赖关系。但是这样也会有一些问题。
这个时候就需要在Native层直接动态加载so库,由JNI层so库动态加载功能实现so库。如下图所示,会有一个统一接口so库,在这个库中定义好不可轻易修改的接口函数,调用方只需要知道这些接口即可,不需要依赖头文件就能调用这些函数,这样调用方和so库之间就不存在直接的依赖,具体的工作就可以交给统一接口so库完成,它通过动态调用再去执行功能so库中的函数。
在Native层的C/C++代码环境,so库动态加载是使用 dlopen()
、 dlsym()
和 dlclose()
这三个函数实现的。它们的作用分别是: dlopen()
打开一个动态链接库,返回一个动态链接库的句柄; dlsym()
根据动态链接库句柄和符号名,返回动态链接库内的符号地址,这个地址既可以是变量指针,也可以是函数指针; dlclose()
关闭动态链接库句柄,并对动态链接库的引用计数减1,当这个库的引用计数为0,这个库将会被系统卸载。
一般使用C/C++实现so库动态加载的流程如下:
dlopen()
函数,这个函数所需的参数,一个是so库的路径,一个是加载模式。一般使用的加载模式有两个: RTLD_NOW
在返回前解析出所有未定义符号,如果解析不出来, dlopen()
返回 NULL
; RTLD_LAZY
则只解析当前需要的符号(只对函数生效,变量定义仍然是全部解析)。显然对于动态加载,加载方只需知道当前被加载的so库里面自己需要用的函数和变量定义,所以这里选择的是后者。如果这个调用成功将返回一个so库的句柄; dlsym()
函数,传入so库句柄和所需的函数或变量名称,返回相应的函数指针或变量指针;加载方这时就可以使用返回的指针调用被加载so库之中定义的函数和数据结构; dlclose()
函数关闭卸载so库; dlerror()
函数获取具体的错误原因。 比如,在硬件功能so库中有一个 int test_open(int port)
的函数,该如何最终调用到这个方法呢?
//1、声明函数接口 typedef int (*Func_test_open)(int); int open(int port){ //2、获取so句柄 void *handle = dlopen("libtest.so",RTLD_LAZY); if(!handle ){ LOGE("%s",dlerror()); return -1; } //3、获取函数指针 Func_test_open func_test_open = (Func_test_open) dlsym (handle,"test_open"); if(!func_test_open){ LOGE("%s",dlerror()); dlclose(handle); return -1; } //4、调用函数 int ret = func_test_open(8080); //5、关闭so dlclose(handle); return ret; } 复制代码
这样JNI层只需要去调用 open(int port)
方法就可以调用到硬件功能so库中的 test_open(int port)
函数
刚开始使用动态加载so库的方案时,会比较担心性能问题,但在实测时跟直接依赖对比,对性能并没有明显的影响,功能实现的so库与JNI层完全解耦,有高度的独立内聚性。同时支持动态加载卸载so库,也一定程度上减少了Native层的常驻内存。