这篇文章通过实战案例,介绍了一种有条理的组织Native层代码层级结构的方法。并且,在良好的代码层级、作用分工的基础上,实现了动态的按需加载、卸载so库。文章的最后,还介绍了实践过程中遇到的困难以及对应的解决方案,能让读者少走弯路。 — 责任编辑 wingyipye
随着Android App发展的不断变化,App的性能和系统API框架外的功能拓展显得越来越重要。App从性能方面考虑,需要在Native层使用C/C++实现的方案,Native层再通过JNI的方式提供方案给实现应用基本功能的Java层调用,来拓展一些计算密集型的功能。例如App如果要支持播放手机自身不支持播放的音频格式,就需要在Native层实现App自己的音频解码功能。
随着项目规模的增大,Native层的代码规模也逐渐膨胀起来。为了更清晰的组织代码,Native层之间也会按照模块分别构建成独立的so库。其中为了简化Java层与Native层之间的通信方式,通常会特地使用一个JNI层so库引用其他实现具体功能的功能实现so库。Java层只加载这个JNI层so库,来间接调用功能实现so库。
so库之间通过引用头文件和运行时指定共享库依赖的方式形成了依赖关系。但是这种简单的模块划分方式存在着一些问题:
System.load()
或者System.loadLibrary()
方式实现。然而对于功能实现的so库,是通过JNI层so库被Java层间接引用的,自身没有直接与Java层对接的JNI函数。所以对于功能实现so库,无法再使用Java层动态加载的方法。为了解决这些问题,就不能再使用Java层动态加载so库的方法,而需要在Native层直接动态加载so库,由JNI层so库动态加载功能实现so库。被加载的so库可以声明一些不能轻易增删和修改其定义的接口函数,调用方只需知道这些接口函数的名字,不需要依赖头文件就能调用这些函数,这样调用方和so库之间就不存在直接的依赖,被加载的功能实现so库甚至可以不用打包到App也能被运行时加载,功能实现so库的独立性得到很大程度的保持,方便了热修复的so库替换。so库被调用时动态加载,结束调用时动态卸载,也能一定程度上减少so库加载需要的常驻内存。
在Native层的C/C++代码环境,so库动态加载是使用dlopen()
、dlsym()
和dlclose()
这三个函数实现的。这三个函数均在头文件<dlfcn.h>
中定义,它们的作用分别是:dlopen()
打开一个动态链接库,返回一个动态链接库的句柄;dlsym()
根据动态链接库句柄和符号名,返回动态链接库内的符号地址,这个地址既可以是变量指针,也可以是函数指针;dlclose()
关闭动态链接库句柄,并对动态链接库的引用计数减1,当这个库的引用计数为0,这个库将会被系统卸载。
一般使用C/C++实现so库动态加载的流程如下:
dlopen()
函数,这个函数所需的参数,一个是so库的路径,一个是加载模式。一般使用的加载模式有两个:RTLD_NOW
在返回前解析出所有未定义符号,如果解析不出来,dlopen()
返回NULL
;RTLD_LAZY
则只解析当前需要的符号(只对函数生效,变量定义仍然是全部解析)。显然对于动态加载,加载方只需知道当前被加载的so库里面自己需要用的函数和变量定义,所以这里选择的是后者。如果这个调用成功将返回一个so库的句柄;dlsym()
函数,传入so库句柄和所需的函数或变量名称,返回相应的函数指针或变量指针;加载方这时就可以使用返回的指针调用被加载so库之中定义的函数和数据结构;dlclose()
函数关闭卸载so库;dlerror()
函数获取具体的错误原因。确定动态加载的方案后,Native层代码模块的划分也有所修改:增加一个公共数据结构定义的so库,专门存放一些通用常量和基本的数据操作接口,例如一些基类的定义,JNI层so库操作基类对象,而在功能实现的so库则继承这些基类定义实现具体操作。由于基类数据结构定义需要事先获知,所以这个so库需要作为共享库被JNI层so库和功能实现so库在运行时依赖(具体表现就是在构建这些so库的Android.mk文件中,把这个公共定义的so库指定到LOCAL_SHARED_LIBRARIES
变量中),而JNI层so库则通过调用dlopen()
动态加载功能实现so库;
so库动态加载的流程如下:
dlopen()
打开功能实现so库之后,在调用dlsym()
获取这两个对外声明的函数的指针,然后调用构造函数获取操作接口对象,并把析构函数指针和so库句柄登记到一个以操作接口对象为键值的映射表中;dlclose()
函数,传入so库句柄,卸载so库,并删除析构函数指针和so库句柄在映射表中的登记。std::string
这种基本的数据类型声明,即使标准库所在头文件名字、命名空间名字和类型名字都一样,但因为在双方各自引用的实现也会不一样,实际上还是不一样的数据类型。这样调用方直接引用被加载so库里面的函数,就有可能因为参数类型错误而出错。
具体的解决方法,就是调用方和被动态加载的so库要同时构建,并且在统一的Application.mk文件里面的APP_STL
属性指定统一的运行时,这样构建出来的可执行文件都是使用同一个C++标准库。综合功能支持力度和开源限制的考虑,选择STLport运行时是相对较好的选择。使用时只需要指定APP_STL
属性为stlport_static
(静态链接)或者stlport_shared
(动态链接)即可。dlopen
直接加载存放在SD卡的so库,会出现权限禁止的问题
在尝试动态加载存放在SD卡的so库的时候,出现了原因是“Permission denied”的UnsatisfiedLinkError
。这是由于SD卡在Android系统上的挂载并不具有可执行文件的权限,所以SD卡的挂载目录不能直接用来作为可执行文件的运行目录,使用前应该把可执行文件复制到APP内部存储再运行。所以如果Android App要动态加载的so库存放在SD卡,就首先需要把so库拷贝到应用自身在/data里的存储目录,或者其他有可执行文件运行权限的目录(如/data/local/)。dlopen
函数的使用需要兼容C++
dlopen
、dlclose
、dlsym
函数是C语言库里面的函数,自身是没有考虑到C++的支持的,调用dlopen
无法直接加载C++的类及其成员函数。这是因为C语言直接把函数名当做符号名,dlsym
直接用符号名就能加载相对应的目标库内的函数,但是由于C++有类和类成员函数的概念,符号名的生成采用了”name managing”的方式,把函数名、类定义、类的成员函数采用复杂的方式将其转换为只能让机器读懂的符号,所以在C++,函数名和其对应的符号名不是直接对等的。
解决方法就是在调用方和被加载的so库都静态引用的公共数据定义中,定义一个虚基类作为操作接口。这个类的具体子类在被加载的so库中实现,调用方使用基类指针操作被加载的so库中的子类实例。
至于如何让调用方创建并获取被加载的so库里的子类实例,首先需要在被加载so库里的子类实现中定义两个前缀带有extern "C"
的非成员函数,因为在C++中带有extern "C"
这个前缀的函数,在符号名生成的处理将跟C语言的函数一样,是直接把函数名当做符号名,所以这两个函数就可以作为可以让调用方用名字获取其指针的接口函数,这两个函数再分别调用子类的构造函数和析构函数,就可以实现子类实例的构建和销毁。 //声明两个接口函数指针类型,这两个函数分别用来构造和销毁操作接口类的实例
typedef BaseClass* (*createClassFcn)();
typedef void (*destroyClassFcn)(BaseClass*);
//子类实现一个返回子类具体对象的extern “C”的非成员函数(子类定义在被动态加载的so库中)
extern "C" SubClass* create_SubClass() {
return new SubClass;
}
//子类实现一个销毁子类具体对象的extern “C”的非成员函数(子类定义在被动态加载的so库中)
extern "C" void destroy_SubClass(SubClass* p) {
delete p;
}
//动态加载时,传入子类定义的这两个非成员函数的名字,使用之前定义的两个函数指针分别指向这两个非成员函数。
*libHandler = dlopen(libNameStr, RTLD_LAZY);
if (!(*libHandler)) {
return ERROR;
}
createClassFcn *p_create_fcn = (createClassFcn)
dlsym(*libHandler, "create_SubClass");
const char* dlsym_error = dlerror();
if (dlsym_error) {
return ERROR;
}
destroyClassFcn *p_destroy_fcn = (destroyClassFcn)
dlsym(*libHandler, "destroy_SubClass");
const char* dlsym_error = dlerror();
if (dlsym_error) {
return ERROR;
}
//操作函数指针就能控制类对象的创建和销毁。
BaseClass* instance = p_create_fcn();
p_destroy_fcn(instance);
使用动态加载so库的方案之后,实测起来跟直接依赖对比,对性能并没有明显的负面影响,功能实现的so库与JNI层完全解耦,有高度的独立内聚性。便于进行单独替换so库的热修复操作。同时支持动态加载卸载so库,也一定程度上减少了Native层的常驻内存。
腾讯音乐招聘: 腾讯音乐招聘-前端专场 腾讯音乐招聘-后台/算法专场。