版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、native 作用
JNITest :
public class JNITest {
static {
System.loadLibrary("native-lib");
}
public static native String getString();
public static String getJavaString(){
return "java string";
}
}
我们知道,JNI 声明的方法需要加上关键字 native,当调用到这个方法的时候,虚拟机会去对应的 .so 文件中查找该方法,并调用。
在控制台切换到 JNITest.java 所在的目录下,使用 javac JNITest.java 命令进行编译,生产 .class 文件(eclipse 下会自动生产)。
接着使用 javap -v JNITest 命令对 JNITest 进行反编译,下拉找到 getString() 和 getJavaString()两个方法。可以发现, native 的方法在 flags 多了一个 ACC_NATIVE 这个标志。
java 在执行到 JNITest 的时候,对于 flags 中有 ACC_NATIVE 这个标志的方法,就会去 native 区间去寻找这个方法,没有这个标志的话就在本地虚拟机中寻找该方法的实现。
二、so 库
1.寻找 .so 库
static {
System.loadLibrary("native-lib");
}
这边从加载库文件 System.loadLibrary(“native-lib”) 开始分析,点击查看源码。
System.loadLibrary:
@CallerSensitive
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
查看 Runtime.getRuntime() 方法,非常典型的单例模式,返回一个 Runtime 。
Runtime 的 getRuntime:
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
接着调用 Runtime 的 loadLibrary0 方法,传进去两个参数,VMStack.getCallingClassLoader() 是当前栈的类加载器,这个方法本身也是一个 native 的,不继续深入。libname 就是我们传进来的 .so 库的名字。
Runtime 的 loadLibrary0:
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
...
}
正常情况下 loader 是不会为 null 的,所以后面流程不分析。继续分析 loader 不为空的情况, loader.findLibrary(libraryName) 直接点进去会发现是返回一个 null,方法的参数是 ClassLoader,实际运行时候传进来的是 ClassLoader 的子类。
在代码中添加日志, 把实际运行中的 ClassLoader 打印出来,会发现是 PathClassLoader,但是 PathClassLoader 只是简单的实现了一下,没有重写 findLibrary 这个方法,这个方法是在 PathClassLoader 的父类 BaseDexClassLoader 中。
Log.d(TAG, "onCreate: ClassLoader" + this.getClassLoader().toString());
PathClassLoader:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
BaseDexClassLoader 中 findLibrary 是调用了 pathList 的 findLibrary 方法,pathList 是在 BaseDexClassLoader 的构造函数中进行初始化。
BaseDexClassLoader:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
...
}
DexPathList 的 findLibrary:
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories) {
String path = new File(directory, fileName).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
}
return null;
}
System.mapLibraryName(libraryName) 是一个native 方法,根据注释可以知道,是根据运行的平台获取到文件的名称,我们原先加载库的时候只传入文件名,没有后缀,在这里会把后缀加上去。然后遍历 nativeLibraryDirectories,nativeLibraryDirectories 是一个 File 数组,在这些路径下寻找对应的 fileName 文件。
nativeLibraryDirectories 包含两大路径,一个是 BaseDexClassLoader 构造函数中传递进来的 libraryPath,这个路径是 apk 下 lib 中添加的 so库路劲,可以把一个apk解压出来查看 lib 文件下的目录。还有一个路径是 System.getProperty(“java.library.path”),这个对应的是系统的环境变量里面,可以用日志打印出来。
apk 下添加的 so:
System.getProperty(“java.library.path”):
系统路径又分为两个,/vendor/lib 是厂商路径,/system/lib 是系统路径,中间用 : 隔开。
DexPathList :
final class DexPathList {
private final File[] nativeLibraryDirectories;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
private static File[] splitLibraryPath(String path) {
ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true);
return result.toArray(new File[result.size()]);
}
private static ArrayList<File> splitPaths(String path1, String path2,
boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
private static void splitAndAdd(String searchPath, boolean directoriesOnly,
ArrayList<File> resultList) {
if (searchPath == null) {
return;
}
for (String path : searchPath.split(":")) {
try {
StructStat sb = Libcore.os.stat(path);
if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
resultList.add(new File(path));
}
} catch (ErrnoException ignored) {
}
}
}
}
小结:在安卓环境中,JNI 运行时加载的 so 库,一个是从 apk 中添加的 lib目录下去搜索,一个是系统环境变量下搜索。
上面打印 ClassLoader 的时候,会把 DexPathList 一起打印出来。
2.加载 .so 库
继续 Runtime 的 loadLibrary0 方法往下,找到 .so 库的路径后,执行 doLoad(filename, loader) 方法。
Runtime 的 loadLibrary0:
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
...
}
Runtime 的 doLoad:
private String doLoad(String name, ClassLoader loader) {
// Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
// which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.
// The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
// libraries with no dependencies just fine, but an app that has multiple libraries that
// depend on each other needed to load them in most-dependent-first order.
// We added API to Android's dynamic linker so we can update the library path used for
// the currently-running process. We pull the desired path out of the ClassLoader here
// and pass it to nativeLoad so that it can call the private dynamic linker API.
// We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
// beginning because multiple apks can run in the same process and third party code can
// use its own BaseDexClassLoader.
// We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
// dlopen(3) calls made from a .so's JNI_OnLoad to work too.
// So, find out what the native library search path is for the ClassLoader in question...
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.getLdLibraryPath();
}
// nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
// of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
在 Runtime 的 doLoad 的末尾,调用了 nativeLoad(name, loader, librarySearchPath) 这个方法去加载 so 库,
nativeLoad 这个方法的实现是在 Android 系统源码,不是 Android 源码。在 /libcore/ojluni/src/main/native/Runtime.c 下。
nativeLoad 的 C 实现:
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader, jstring javaLibrarySearchPath)
{
return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}
注:nativeLoad 的 C 实现的方法名与我们前面要求的 JNI 方法名规则明显不符,这边采用的是动态注册方式。
nativeLoad 调用了 JVM_NativeLoad 这个方法,这个是位于安卓系统源码的 art/runtime/openjdkjvm/OpenjdkJvm.cc 下。
JVM_NativeLoad:
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
jstring javaFilename,
jobject javaLoader,
jstring javaLibrarySearchPath) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL) {
return NULL;
}
std::string error_msg;
{
art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
bool success = vm->LoadNativeLibrary(env,
filename.c_str(),
javaLoader,
javaLibrarySearchPath,
&error_msg);
if (success) {
return nullptr;
}
}
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env->ExceptionClear();
return env->NewStringUTF(error_msg.c_str());
}
在 JVM_NativeLoad 中获取到 javaVM,对于一个应用程序来说只有一个 javaVM,同事吧加载的 so 库下的方法存放在 javaVM 中,这样在其他地方调用 JNI 方法的时候,只要获取到当前应用的 javaVM 即可获取到要调用的方法。
三、动态注册
动态注册的实现主要在 JVM_NativeLoad 下的 LoadNativeLibrary 方法(代码较复杂,只提供具体思路)。
LoadNativeLibrary()
---->sym = library->FindSymbol("JNI_OnLoad", nullptr);
在我们要加载 so 库中查找是否含有 JNI_OnLoad 这个方法,如果没有系统就认为是静态注册方式进行的,直接返回 true,代表 so 库加载成功;如果有找到 JNI_OnLoad 认为是动态注册的,然后调用JNI_OnLoad 方法,JNI_OnLoad 方法中一般存放的是方法注册的函数。所以如果采用动态注册就必须要实现 JNI_OnLoad 方法,否则调用 java 中申明的 native 方法时会抛出异常。
动态加载时候, java 与 C/C++ 方法间的映射关系是使用 jni.h 中的 JNINativeMethod 结构。
typedef struct {
const char* name; //java层函数名
const char* signature; //函数的签名信息
void* fnPtr; //C/C++ 中对应的函数指针。
} JNINativeMethod;
下面是一个动态加载的 demo,这是模仿底层动态加载的过程进行加载。
C++:
#include <jni.h>
#include <string>
#include <android/log.h>
#include <assert.h>
#define TAG "JNITest"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
//数组大小
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
extern "C"
JNIEXPORT jstring JNICALL native_getString
(JNIEnv *env, jclass jclz){
LOGI("JNI test 动态注册");
return env->NewStringUTF("JNI test return");
}
//gMethods 记录所有动态注册方法的映射关系
static const JNINativeMethod gMethods[] = {
{
"getString","()Ljava/lang/String;",(void*)native_getString
}
};
static int registerNatives(JNIEnv *env)
{
LOGI("registerNatives begin");
jclass clazz;
clazz = env->FindClass("com/xiaoyue/jnidemo/JNITest");
if (clazz == NULL) {
LOGI("clazz is null");
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
LOGI("RegisterNatives error");
return JNI_FALSE;
}
return JNI_TRUE;
}
//会自动调用 JNI_OnLoad 方法
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
LOGI("jni_OnLoad begin");
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGI("ERROR: GetEnv failed\n");
return -1;
}
assert(env != NULL);
registerNatives(env);
return JNI_VERSION_1_4;
}
静态注册:每个 class 都需要使用 javah 生成一个头文件,并且生成的名字很长书写不便;初次调用时需要依据名字搜索对应的 JNI 层函数来建立关联关系,会影响运行效率。用javah 生成头文件方便简单。
动态注册:使用一种数据结构 JNINativeMethod 来记录 java native 函数和 JNI 函数的对应关系,
移植方便(一个java文件中有多个native方法,java文件的包名更换后)。