(七)JNI 源码分析、动态注册

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、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文件的包名更换后)。

猜你喜欢

转载自blog.csdn.net/qq_18983205/article/details/79030781