前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JNI原理

JNI原理

作者头像
老马的编程之旅
发布2022-06-22 15:59:12
8640
发布2022-06-22 15:59:12
举报
文章被收录于专栏:深入理解Android

综述一下JNI的原理

1.通过System.loadLibrary()或System.load()加载动态库

代码语言:javascript
复制
System.load("/data/local/tmp/libgityuan_jni.so");
System.loadLibrary("gityuan_jni");

以上两个方法都用于加载动态库,两者的区别如下: 加载的路径不同:System.load(String filename)是指定动态库的完整路径名;而System.loadLibrary(String libname)则只会从指定lib目录下查找,并加上lib前缀和.so后缀; 自动加载库的依赖库的不同:System.load(String filename)不会自动加载依赖库;而System.loadLibrary(String libname)会自动加载依赖库。

loadLibrary()会将将xxx动态库的名字转换为libxxx.so,再从/data/app/[packagename]-1/lib/arm64,/vendor/lib64,/system/lib64等路径中查询对应的动态库

2.System.loadLibrary()或System.load()都会调用BaseDexClassLoader的findLibrary,走到DexPathList的findLibrary,我们在构造classloader时候都会传一个app的library路径,这个就是app的so目录

[-> BaseDexClassLoader.java]

代码语言:javascript
复制
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        //dexPath一般是指apk所在路径
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    public String findLibrary(String name) {
        return pathList.findLibrary(name); 
    }
}

[-> DexPathList.java]

代码语言:javascript
复制
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;

    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    //记录所有的dexFile文件
    this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);

    //app目录的native库
    this.nativeLibraryDirectories = splitPaths(libraryPath, false);
    //系统目录的native库
    this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
    List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
    allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
    //记录所有的Native动态库
    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
                                                      suppressedExceptions);
    ...
}

DexPathList初始化过程,主要功能是收集以下两个变量信息: dexElements: 记录所有的dexFile文件 nativeLibraryPathElements: 记录所有的Native动态库, 包括app目录的native库和系统目录的native库.

3.最终调用到LoadNativeLibrary()方法 该方法主要操作: 3.1通过dlopen打开动态共享库; dlopen返回一个handle,这个句柄是使用其它的dlxxx函数用的,dlsym就是使用dlopen返回的handle,去查找相应的符号,然后返回相应的函数指针的。 3.2通过dlsym获取JNI_OnLoad符号所对应的方法; 3.3调用该加载库中的JNI_OnLoad()方法。

4.jni注册有2种时机 4.1.Android系统启动过程中Zygote注册,可通过查询AndroidRuntime.cpp中的gRegJNI,REG_JNI是一个宏定义,看看是否存在对应的register方法; 4.2.调用System.loadLibrary()方式注册

例如系统MediaPlayer在System.loadLibrary(“media_jni”)时,内部会调用AndroidRuntime::registerNativeMethods,其中有个参数gMethods,记录java层和C/C++层方法的一一映射关系。

5.java层传递的参数会在jni进行一层转换

6.java对于native方法调用最后都是通过dlsym获取相应函数指针执行

7.jni方法签名的作用 Java是有重载方方法的,可以定义方怯名相同,但参数不同的方毡,正因为如此, JNI中仅仅通过方法名是无法找到 Java 中对应的具体方法的 ,JNI 为了解决这 问题就将参数类型和返回值类型组合在 起作为方法签名 。

8。JNIEnv 主要作用下两点: ·调用 Java 的方法 ·操作 Java (操作 Java 中的变量和对象 )。

JNI方法注册方式

Android系统在启动启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会AndroidRuntime.cpp中的startVm创建虚拟机,VM创建完成后,紧接着调用startReg完成虚拟机中的JNI方法注册。

1.android系统启动时候已经预注册好 这种多出现在android自己系统代码里提供的注册方法

startReg AndroidRuntime.cpp

代码语言:javascript
复制
int AndroidRuntime::startReg(JNIEnv* env)
{
    //设置线程创建方法为javaCreateThreadEtc
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

    env->PushLocalFrame(200);
    //进程JNI方法的注册
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);
    return 0;
}

register_jni_procs(gRegJNI, NELEM(gRegJNI), env)这行代码的作用就是就是循环调用gRegJNI数组成员所对应的方法。

代码语言:javascript
复制
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) {
    for (size_t i = 0; i < count; i++) {
        if (array[i].mProc(env) < 0) {
            return -1;
        }
    }
    return 0;
}

gRegJNI数组,有100多个成员变量,定义在AndroidRuntime.cpp:

代码语言:javascript
复制
static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_os_MessageQueue),
    REG_JNI(register_android_os_Binder),
    ...
};

2.应用程序自己注册jni 以MediaPlayer.java为例,其包名为android.media:

代码语言:javascript
复制
public class MediaPlayer{
    static {
        System.loadLibrary("media_jni");
        native_init();
    }

    private static native final void native_init();
    ...
}

通过static静态代码块中System.loadLibrary方法来加载动态库,库名为media_jni, Android平台则会自动扩展成所对应的libmedia_jni.so库。 接着通过关键字native加在native_init方法之前,便可以在java层直接使用native层方法。

接下来便要查看libmedia_jni.so库定义所在文件,一般都是通过Android.mk文件定义LOCAL_MODULE:= libmedia_jni,可以采用grep或者mgrep来搜索包含libmedia_jni字段的Android.mk所在路径。

libmedia_jni.so位于/frameworks/base/media/jni/Android.mk

该Android.mk所在目录/frameworks/base/media/jni/中找到android_media_MediaPlayer.cpp文件,并在文件中存在相应的方法:

代码语言:javascript
复制
  static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaPlayer");
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    ...
}

总结: JNI注册的两种时机: 1.Android系统启动过程中Zygote注册,可通过查询AndroidRuntime.cpp中的gRegJNI,看看是否存在对应的register方法; 2.调用System.loadLibrary()方式注册。

JNI原理分析

文件MediaPlayer.java中调用System.loadLibrary(“media_jni”),把libmedia_jni.so动态库加载到内存。以loadLibrary为起点展开JNI注册流程的过程分析。

[System.java] loadLibrary

代码语言:javascript
复制
public static void loadLibrary(String libName) {
    //调用Runtime方法
    Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

[Runtime.java]

代码语言:javascript
复制
void loadLibrary(String libraryName, ClassLoader loader) {
    if (loader != null) {
        String filename = loader.findLibrary(libraryName);
        //加载库
        String error = doLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    //loader为空,则会进入该分支
    String filename = System.mapLibraryName(libraryName);
    List<String> candidates = new ArrayList<String>();
    for (String directory : mLibPaths) {
        String candidate = directory + filename;
        candidates.add(candidate);
        if (IoUtils.canOpenReadOnly(candidate)) {
             //加载库
            String error = doLoad(candidate, loader);
            if (error == null) {
                return; //加载成功
            }
        }
    }
    ...
}

真正加载的工作是由doLoad()

doLoad 该方法内部增加同步锁,保证并发时一致性。

代码语言:javascript
复制
private String doLoad(String name, ClassLoader loader) {
    ...
    synchronized (this) {
        return nativeLoad(name, loader, ldLibraryPath);
    }
}

nativeLoad()这是一个native方法,再进入ART虚拟机的java_lang_Runtime.cc,最终的核心功能工作:

调用dlopen函数,打开一个so文件并创建一个handle; 调用dlsym()函数,查看相应so文件的JNI_OnLoad()函数指针,并执行相应函数。

总之,System.loadLibrary()的作用就是调用相应库中的JNI_OnLoad()方法。接下来说说JNI_OnLoad()过程。

JNI_OnLoad [-> android_media_MediaPlayer.cpp]

代码语言:javascript
复制
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    //注册JNI方法
    if (register_android_media_MediaPlayer(env) < 0) {
        goto bail;
    }
    ...
}

register_android_media_MediaPlayer [-> android_media_MediaPlayer.cpp]

代码语言:javascript
复制
static int register_android_media_MediaPlayer(JNIEnv *env) {
   
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}

其中gMethods,记录java层和C/C++层方法的一一映射关系。

代码语言:javascript
复制
static JNINativeMethod gMethods[] = {
    {"prepare",      "()V",  (void *)android_media_MediaPlayer_prepare},
    {"_start",       "()V",  (void *)android_media_MediaPlayer_start},
    {"_stop",        "()V",  (void *)android_media_MediaPlayer_stop},
    {"seekTo",       "(I)V", (void *)android_media_MediaPlayer_seekTo},
    {"_release",     "()V",  (void *)android_media_MediaPlayer_release},
    {"native_init",  "()V",  (void *)android_media_MediaPlayer_native_init},
    ...
};

这里涉及到结构体JNINativeMethod,其定义在jni.h文件:

代码语言:javascript
复制
typedef struct {
    const char* name;  //Java层native函数名
    const char* signature; //Java函数签名,记录参数类型和个数,以及返回值类型
    void*       fnPtr; //Native层对应的函数指针
} JNINativeMethod;

registerNativeMethods [-> AndroidRuntime.cpp]

代码语言:javascript
复制
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

jniRegisterNativeMethods该方法是由Android JNI帮助类JNIHelp.cpp来完成。

jniRegisterNativeMethods [-> JNIHelp.cpp]

代码语言:javascript
复制
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {
        e->FatalError("");//无法查找native注册方法
    }
    // 调用JNIEnv结构体的成员变量
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        e->FatalError("");//native方法注册失败
    }
    return 0;
}

RegisterNatives [-> jni.h]

代码语言:javascript
复制
struct _JNIEnv {
    const struct JNINativeInterface* functions;

    jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
            jint nMethods)
    { return functions->RegisterNatives(this, clazz, methods, nMethods); }
    ...
}

functions是指向JNINativeInterface结构体指针,也就是将调用下面方法:

代码语言:javascript
复制
struct JNINativeInterface {
    jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
    ...
}

这个过程完成了gMethods数组中的方法的映射关系,比如java层的native_init()方法,映射到native层的android_media_MediaPlayer_native_init()方法。

虚拟机相关的变量中有两个非常重要的量JavaVM和JNIEnv: JavaVM:是指进程虚拟机环境,每个进程有且只有一个JavaVM实例 JNIEnv:是指线程上下文环境,每个线程有且只有一个JNIEnv实例

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-03-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 综述一下JNI的原理
  • JNI方法注册方式
  • JNI原理分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档