本地库如何加载到虚拟机中
一般我们需要加载本地库的时候会调用以下方法
System.loadLibrary("native-lib");
从类图我们可以知道接下来会调用Runtime
类的loadLibrary0
方法,在这个方法里面会做两件事:
- 通过
loader.findLibrary(libraryName)
找到对应库的全路径 - 通过
doLoad(filename, loader)
加载库文件
找到对应库的全路径
ClassLoader
类是个抽象类,它里面的findLibrary
方法并没有具体实现,所以我们得找到实现该方法的具体子类,可以通过打印,如Log.d("ClassLoader",this.getClassLoader().toString());
,从打印结果dalvik.system.PathClassLoader[DexPathList[[zip file ..."
得知,实际的ClassLoder
是PathClassLoader
。而PathClassLoader
类中的findLibrary
方法是调用的父类实现,那么来看看它的父类BaseDexClassLoader
是如何实现的。
BaseDexClassLoader#findLibrary
内部实现就一句代码,这句代码就指向了真正的核心处理类DexPathList
return pathList.findLibrary(name);
DexPathList
找到findLibrary
方法,如下分为两步:
- 通过c方法拼接对应的库文件名
String fileName = System.mapLibraryName(libraryName);
- 遍历
nativeLibraryPathElements
成员变量
for (Element element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
C中如何拼接的库文件名?
- 计算两个宏定义的字符长度
int prefix_len = (int) strlen(JNI_LIB_PREFIX);
int suffix_len = (int) strlen(JNI_LIB_SUFFIX);
这两个宏根据不同系统,定义不同,如在linux系统下是:
#define JNI_LIB_PREFIX "lib"
#define JNI_LIB_SUFFIX ".so"
- 如果传入的库文件为null或字符长度超过240会报异常
if (libname == NULL) {
JNU_ThrowNullPointerException(env, 0);
return NULL;
}
if (len > 240) {
JNU_ThrowIllegalArgumentException(env, "name too long");
return NULL;
}
- 拼接前缀
cpchars(chars, JNI_LIB_PREFIX, prefix_len);
- 拼接库文件名
(*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
- 拼接后缀
len += prefix_len;
cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
nativeLibraryPathElements这个成员变量是在哪赋值的?
// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
// 1. This class loader's library path for application libraries (librarySearchPath):
// 1.1. Native library directories
// 1.2. Path to libraries in apk-files
// 2. The VM's library path from the system property for system libraries
// also known as java.library.path
//
// This order was reversed prior to Gingerbread; see http://b/2933456.
之前打印ClassLoader时,也打印出了本地库的路径:
nativeLibraryDirectories=[
/data/app/com.project.zero.myjni-1/lib/x86,
/data/app/com.project.zero.myjni-1/base.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_dependencies_apk.apk!/lib/x86, /data/app/com.project.zero.myjni-1/split_lib_slice_0_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_1_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_2_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_3_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_4_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_5_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_6_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_7_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_8_apk.apk!/lib/x86,
/data/app/com.project.zero.myjni-1/split_lib_slice_9_apk.apk!/lib/x86,
/system/lib,
/vendor/lib]
在DexPathList
类的构造方法的注释我们可以得知本地库可能存在于系统和应用程序库路径,它的搜索顺序如下:
- 这个类加载器的应用程序库的库路径,
1.1. 本地库目录,例如/data/app/com.project.zero.myjni-1/lib/x86
1.2. 在apk文件中的库的路径,例如/data/app/com.project.zero.myjni1/split_lib_dependencies_apk.apk!/lib/x86
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
- 系统库系统属性中的虚拟机库路径,
/system/lib
和/vendor/lib
这两个库,其中/vendor/lib
为手机厂商的库,这两个系统库路径也可以通过打印(System.getProperty("java.library.path")
得到
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
加载库文件
核心处理方法是\art\runtime\java_vm_ext.cc
的LoadNativeLibrary
方法:
sym = library->FindSymbol("JNI_OnLoad", nullptr);
...
if (version == JNI_ERR) {
StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (IsBadJniVersion(version)) {
StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
path.c_str(), version);
}
在我们要加载so库中查找JNI_OnLoad
方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功,如果找到JNI_OnLoad
就会调用JNI_OnLoad
方法,JNI_OnLoad
方法中一般存放的是方法注册的函数,所以如果采用动态注册就必须要实现JNI_OnLoad
方法,否则调用java中声明的native方法时会抛出异常。
IsBadJniVersion
return version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6;
JNI_OnLoad
方法只能返回上述三个值之一。
动态注册的步骤
- 动态注册的方法可以不按
"package name"+"class name"+"method name"
的方式命名
JNIEXPORT jstring JNICALL
dynamic_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
....
}
- 添加动态注册的方法数组,要按照
JNINativeMethod
这个结构体的数据结构来添加
static const JNINativeMethod methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) dynamic_stringFromJNI}
};
- 定义一个宏
#define NELEM(x) (sizeof(x)/sizeof(*(x)))
- 定义动态注册方法,参考
\libnativehelper\JNIHelp.cpp
的jniRegisterNativeMethods
方法
static jint registerNatives(JNIEnv *env) {
jclass jclz = env->FindClass("com/project/zero/myjni/MainActivity");
if (jclz == NULL) {
LOGI("class is NULL");
return JNI_FALSE;
}
if (env->RegisterNatives(jclz, methods, NELEM(methods)) < 0) {
LOGI("RegisterNatives Error");
return JNI_FALSE;
}
return JNI_OK;
}
- 定义
JNI_OnLoad
方法,参考\frameworks\base\media\jni\android_media_MediaPlayer.cpp
的JNI_OnLoad
方法
jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGI("ERROR: GetEnv failed\n");
return -1;
}
assert(env != NULL);
registerNatives(env);
LOGI("JNI REGISTER SUCCESS");
return JNI_VERSION_1_4;
}