解析Android JNI机制

解析Android JNI机制,第1张

解析Android JNI机制 一、JNI概述 1.1 什么是JNI

        JNI,即Java Native Interface,即 "Java本地调用";

1.2 JNI有什么用

        JNI是一种技术,可以做到以下两点:

        (1)Java程序中的函数可以调用Native语言写的函数(Native一般指的是C/C++编写的函数);

        (2)Native程序中的函数可以调用Java层的函数;

1.3 为什么会划分为Native和Java两部分

        Android系统按语言来划分的话由两个世界组成,分别是Java世界和Native世界。那为什么要这么划分呢?Android系统由Java写不好吗?

        除了性能的之外,最主要的原因就是在Java诞生之前,就有很多程序和库都是由Native语言写的,因此,重复利用这些Native语言编写的库是十分必要的,况且Native语言编写的库具有更好的性能。

        这样就产生了一个问题,Java世界的代码要怎么使用Native世界的代码呢,这就需要一个桥梁来将它们连接在一起,而JNI就是这个桥梁。

                                                

二、JNI方法注册

2.1 静态注册

2.2 动态注册

三、类型转换 3.1 数据类型的转换 3.1.1 基本数据类型的转换 JavaNativeSignaturebytejbyteBcharjcharCdoublejdoubleDfloatjfloatFintjintIshortjshortSlongjlongJbooleanjbooleanZvoidvoidV

        从上表可以可看出,基本数据类型转换,除了void,其他的数据类型只需要在前面加上“j”就可以了。第三列的Signature 代表签名格式,后文会介绍它。接着来看引用数据类型的转换。

3.1.2 引用数据类型的转换 JavaNativeSignature所有对象jobjectL+classname +;ClassjclassLjava/lang/Class;StringjstringLjava/lang/String;ThrowablejthrowableLjava/lang/Throwable;Object[]jobjectArray[L+classname +;byte[]jbyteArray[Bchar[]jcharArray[Cdouble[]jdoubleArray[Dfloat[]jfloatArray[Fint[]jintArray[Ishort[]jshortArray[Slong[]jlongArray[Jboolean[]jbooleanArray[Z

        从上表可看出,数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都会有“[”。除了数组以外,其他的引用数据类型的签名格式都会以“;”结尾。 

        引用类型的继承关系:

        

四、方法签名

        前面表格已经列举了数据类型的签名格式,方法签名就由签名格式组成,那么,方法签名有什么作用呢?我们看下面的代码。

4.1 JNI层的方法

        frameworks/base/media/jni/android_media_MediaRecorder.cpp

static const JNINativeMethod gMethods[] = {
  ...
    {"native_init",       "()V",        (void *)android_media_MediaRecorder_native_init},
    {"native_setup",      "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
    (void *)android_media_MediaRecorder_native_setup},
   ...
};

        说明:

        gMethods数组中存储的是MediaRecorder的Native方法与JNI层方法的对应关系;

        其中”()V”和 “(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V”就是方法签名;

        我们知道Java是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在JNI中仅仅通过方法名是无法找到 Java中的具体方法的;

        JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。 

        JNI的方法签名的格式为: 

(参数签名格式...)返回值签名格式
4.2 Java层的方法

        拿上面gMethods数组的native_setup方法举例,他在Java中是如下定义的: 

private native final void native_setup(Object mediarecorder_this,
        String clientName, String opPackageName) throws IllegalStateException;

        它在JNI中的方法签名为:

(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V

       说明:

        参照本文的类型转换表格,native_setup方法的第一个参数的签名为“Ljava/lang/Object;”,后两个参数的签名为“Ljava/lang/String;”,返回值类型void 的签名为“V”,组合起来就是上面的方法签名。

4.3 自动生成方法签名

(1)如果我们每次编写JNI时都要写方法签名,也会是一件比较头疼的事,而Java提供了javap命令来自动生成方法签名。我们先写一个简单的MediaRecorder.java包含上面的native_setup方法:

public class MediaRecorder {//Java层
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    private static native final void native_init();
    private native final void native_setup(Object mediarecorder_this,
        String clientName, String opPackageName) throws IllegalStateException;
}

(2)这个文件的在我的本地地址为D:/Android/MediaRecorder.java,接着执行如下命令:

javac D:/Android/MediaRecorder.java

(3)执行命令后会生成MediaRecorder.class文件,最后使用javap命令:

javap -s -p D:/Android/MediaRecorder.class

        其中s 表示输出内部类型签名,p表示打印出所有的方法和成员(默认打印public成员),最终在cmd中的打印结果如下:

   

        可以很清晰的看到输出的native_setup方法的签名和此前给出的一致。 

五、JNIEnv 5.1 概述

        JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的;

5.2 作用

        主要作用有两点:
                1.调用Java的方法。
                2. *** 作Java(获取Java中的变量和对象等等)。

5.3 JNIEnv的定义

         代码位置:libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的类型 
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的类型  
typedef const struct JNIInvokeInterface* JavaVM;
#endif

        说明:

        这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,则是C++代码中的定义,否则就是C代码中的定义。

        在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。

        通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。

5.4 jfieldID和jmethodID  5.4.1 获取方法

        在JNI中用jfieldID和jmethodID来代表Java类中的成员变量和方法,可以通过JNIEnv的下面两个方法来分别得到:

//JNI层
jfieldID  GetFieldID(jclass clazz,const char *name,const char *sig);
jmethodID  GetFieldID(jclass clazz,const char *name,const char *sig);

        其中,jclass代表Java类,name代表成员方法或者成员变量的名字,sig为这个方法和变量的签名。

5.4.2 获取jfieldID和jmethodID

        我们来查看MediaRecorder框架的JNI层是如何使用上述的两个方法的,如下所示:

        代码位置:frameworks/base/media/jni/android_media_MediaRecorder.cpp

//JNI层
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaRecorder");//1
    if (clazz == NULL) {
        return;
    }
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2
    if (fields.context == NULL) {
        return;
    }
    fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");//3
    if (fields.surface == NULL) {
        return;
    }
    jclass surface = env->FindClass("android/view/Surface");
    if (surface == NULL) {
        return;
    }
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");//4
    if (fields.post_event == NULL) {
        return;
    }
}

        说明:

        注释1处,通过FindClass来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,因此,clazz就是Java层的MediaRecorder在JNI层的代表。

        注释2和注释3处的代码用来找到Java层的MediaRecorder中名为mNativeContext和mSurface的成员变量,并分别赋值给context和surface。

        注释4处获取Java层的MediaRecorder中名为postEventFromNative的静态方法,并赋值给post_event。其中fields的定义为:

struct fields_t {//JNI层
    jfieldID    context;
    jfieldID    surface;
    jmethodID   post_event;
};
static fields_t fields;

        将这些成员变量和方法赋值给jfieldID和jmethodID类型的变量主要是为了效率考虑,如果每次调用相关方法时都要进行查询方法和变量,显然会效率很低,因此在MediaRecorder框架JNI层的初始化方法android_media_MediaRecorder_native_init中将这些jfieldID和jmethodID类型的变量保存起来,以供后续使用。

5.4.3 使用jfieldID和jmethodID

        我们保存了jfieldID和jmethodID类型的变量,接着怎么使用它们呢,如下所示: 

        1. 使用jmethodID

        代码位置:frameworks/base/media/jni/android_media_MediaRecorder.cpp

void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{//JNI层
    ALOGV("JNIMediaRecorderListener::notify");

    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);//1
}

        说明:

        在注释1处调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了fields.post_event,从上面我们得知,它其实是保存了Java层MediaRecorder的静态方法postEventFromNative;

        postEventFromNative方法:

        代码位置:frameworks/base/media/java/android/media/MediaRecorder.java

private static void postEventFromNative(Object mediarecorder_ref,
                                        int what, int arg1, int arg2, Object obj)
{//Java层
    MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
    if (mr == null) {
        return;
    }
    if (mr.mEventHandler != null) {
        Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
        mr.mEventHandler.sendMessage(m);
    }
}

        这样我们就能在JNI层中访问Java的静态方法了,同理,如果想要访问Java的方法则可以使用JNIEnv的CallVoidMethod函数。

        2. 使用jfieldID

        代码位置:frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_prepare(JNIEnv *env, jobject thiz)
{//JNI层
    ALOGV("prepare");
    sp mr = getMediaRecorder(env, thiz);
    jobject surface = env->GetObjectField(thiz, fields.surface);//1
    if (surface != NULL) {
        const sp native_surface = get_surface(env, surface);
       ...
    }
    process_media_recorder_call(env, mr->prepare(), "java/io/IOException", "prepare failed.");

        说明:

        在注释1处调用了JNIEnv的GetObjectField函数,参数中的fields.surface用来保存Java层MediaRecorde中的成员变量mSurface,mSurface的类型为Surface,这样通过GetObjectField函数就得到了mSurface在JNI层中对应的jobject类型变量surface 。

六、编写JNI代码 6.1 创建文件夹

        (1)首先,先创建一个文件夹JNI,用来存放接下来所有的文件;

        (2)cd JNI进入文件夹JNI,创建com文件夹,进入com文件夹,创建test文件夹;

6.2 编写Java代码

        在test文件夹下编写源程序JNIDemo.java;

package com.test;

public class JNIDemo {
    
    //定义一个方法,该方法在C中实现
    public native void testHello();
    
    public static void main(String[] args){
        //加载C文件
        System.loadLibrary("TestJNI");
        JNIDemo jniDemo = new JNIDemo();
        jniDemo.testHello();
    }

}
6.3 编译生成class文件 6.3.1 生成class文件

        回到JNI文件夹,使用命令javac com/test/JNIDemo.java编译java文件生成JNIDemo.class(回到JNI文件夹的原因:编译时要有完整的包名,在java程序文件所在目录下编译会报错)

user@user-PC:~/Desktop/JNI/com/test$ cd ../../
user@user-PC:~/Desktop/JNI$ javac com/test/JNIDemo.java
user@user-PC:~/Desktop/JNI$ cd com/test/
user@user-PC:~/Desktop/JNI/com/test$ ls
JNIDemo.class  JNIDemo.java
6.3.2 注意点

        编译java文件时文件名带后缀:javac JNIDemo.java,执行class文件时文件名不带后缀:java JNIDemo

6.4 生成头文件 6.4.1 生成头文件

        回到JNI目录,输入命令:javah -classpath . -jni com.test.JNIDemo,可以看到在JNI目录下生成了一个名为com_test_JNIDemo.h的头文件(注:"." 表示将生成的头文件保存在当前目录);

user@user-PC:~/Desktop/JNI/com/test$ cd ../../
user@user-PC:~/Desktop/JNI$ javah -classpath . -jni com.test.JNIDemo
user@user-PC:~/Desktop/JNI$ ls
com  com_test_JNIDemo.h  TestJNI.c
6.4.2 解释说明

         com_test_JNIDemo.h文件内容如下:

#include 


#ifndef _Included_com_test_JNIDemo
#define _Included_com_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_com_test_JNIDemo_testHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

        这个头文件中便告诉了我们需要用C/C++实现的函数的原型,即

        JNIEXPORT void JNICALL Java_HelloWorld_print ( JNIEnv * env, jobject obj)

        函数名格式:Java_类名_函数名

        参数env代表java虚拟机环境,Java传过来的参数和c有很大的不同,需要调用JVM提供的接口来转换成C类型的,就是通过调用env方法来完成转换的。

        参数obj代表调用的对象,相当于c++的this。当c函数需要改变调用对象成员变量时,可以通过 *** 作这个对象来完成。

6.5 编写C/C++代码 6.5.1 编写代码

        回到JNI目录,编写C程序,代码如下:

        注:其中的头文件jni.h在后面会添加,com_test_JNIDemo.h文件前面已经生成;

//TestJNI.c
#include "jni.h"
#include "stdio.h"
#include "com_test_JNIDemo.h"

JNIEXPORT void JNICALL Java_com_test_JNIDemo_testHello
(JNIEnv *env, jobject obj) {
    printf("Hello Worldn");
}
6.5.2 添加头文件

        需求:C程序中包含的 jni.h文件需要从Linux系统安装的JDK目录下复制过来;

        (1)先要查找Linux系统当前安装的JDK目录,查找方法参考如下文章;

        查找JDK目录:在linux中查看jdk的版本以及安装路径-CSDN博客

         (2)在你的JDK目录的include目录下有一个jni.h的文件,将其复制到TestJNI.c所在的目录,即JNI目录下;

6.6 编译生成库文件 6.6.1 生成库文件(.so文件)

        C/C++程序编写好后,就可以使用gcc对其进行编译了;

        编译命令如下,在编译的时候需要注意:

        记得加上java的两个路径,该路径根据你的java环境的实际安装路径而设置,其余的和编译普通的动态库方法相同;

gcc -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux/" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/" -fPIC  -shared -o libJNIDemo.so TestJNI.c

        如上命令中的两对双引号可加可不加; 

6.6.2 遇到的报错

问题1:未填写JNI函数形参

        上图第一个红色方框中圈出了我们经常范的一个错误,就是没有填写JNI函数的两个形参,虽然我们这里用不到它们,但是也必须写上,否则无法通过编译。 

问题2:找不到头文件"iostream"

        因为这里使用的是gcc命令进行编译,gcc是GCC中的C编译器,g++才是GCC中的C++编译器

问题3:

报错如下

 

进行中

        GCC参数详解:GCC 参数详解 | 菜鸟教程

        Linux查看JDK版本和安装路径:linux 查看java版本和路径_在linux中查看jdk的版本以及安装路径_徐徐徐大仙的博客-CSDN博客

        执行Java命令时,提示找不到或无法加载主类:

        在命令窗口执行java文件时,提示找不到或无法加载主类 - 耳语 - 博客园

参考资料:

|(详细):标签: Android深入理解JNI | BATcoder - 刘望舒

|(编写JNI代码-测试成功):JNI技术基础(2)——从零开始编写JNI代码 - 雅香小筑 - 博客园

| (JNI实战例子):JNI 入门教程 | 菜鸟教程

| Android JNI原理分析 - Gityuan博客 | 袁辉辉的技术博客

| Android framework层JNI的使用浅析 - Android开发 - 开发语言与工具 - 深度开源

| Android的JNI函数注册

| jni实例介绍:Android IO监控 | 性能监控系列 - 掘金

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5678274.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存