Android之 JNI详解

一、JNI简介
JNI 是Java Native Interface的缩写,表示"Java本地调用"。通过JNI技术可以实现Java调用C程序和C程序调用Java代码。

二、JNI函数注册
2.1 静态注册:
静态注册的方式我们平时用的比较多。我们通过javac和javah编译出头文件,然后再实现对应的cpp文件的方式就是属于静态注册的方式。这种调用的方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配,则会报错
优缺点: 系统默认方式,使用简单; 灵活性差(如果修改了java native函数所在类的包名或类名,需手动修改C函数名称(头文件、源文件))

//Java 层代码JniSdk.java
public class NativeDemo {
    static {
        System.loadLibrary("test_jni");
    }
    public native String showJniMessage();
}


//Native层代码 jnidemo.cpp
extern "C"
JNIEXPORT jstring JNICALL Java_NativeDemo_showJniMessage
  (JNIEnv* env, jobject job) {
    return env->NewStringUTF("hello world");
}

JNIEXPORT :在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,至今为止没发现它有什么特殊的用处。

jstring :这个学过编程的人都知道,当然是方法的返回值了,对应java的String类型,无返回值就是void

JNICALL :这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名)。

Java_NativeDemo_sayHello:这个就是被上一步中被调用的部分,也就是Java中的native 方法名,这里起名字的方式比较特别,是:包名+类名+方法名。

JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象。例如:env->GetObjectClass()。(详情请查看jni.h)

jobject obj:代表着native方法的调用者,本例即new NativeDemo();但如果native是静态的,那就是NativeDemo.class .

也就是说,我们的native sayHello()方法实际上是运行C的Java_NativeDemo_sayHello()这个方法,我们是不能随意写C函数名的的,只能这样写。

2.2 动态注册
动态注册不再按照特定的规则去实现native函数,只要在.c文件里面根据对应的规则声明函数即可,所以我们可以不用默认的映射规则,直接由我们告诉JVM,java的native函数对应的是C文件里面的哪个函数。
优缺点: 函数名看着舒服一些,但是需要在C代码中维护Java Native函数与C函数的对应关系; 灵活性稍高(如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息)

//Java 层代码JniSdk.java
public class JniSdk {
    static {
        System.loadLibrary("test_jni");
    }
    public static native int numAdd(int a, int b);
    public native void dumpMessage();
}

//Native层代码 jnidemo.cpp
JNINativeMethod g_methods[] = {
        {"numAdd", "(II)I", (void*)add},
        {"dumpMessage","()V",(void*)dump},
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    j_vm = vm;
    JNIEnv *env = NULL;
    if (vm->GetEnv((void**)&env, JNI_VERSION_1_2) != JNI_OK) {
        LOGI("on jni load , get env failed");
        return JNI_VERSION_1_2;
    }
    jclass clazz = env->FindClass("com/example/dragon/androidstudy/jnidemo/JniSdk");
    //clazz对应的类名的完整路径。把.换成/   g_methods定义的全局变量     1  是g_methods的数组长度。也可以用sizeof(g_methods)/sizeof(g_methods[0])
    jint ret = env->RegisterNatives(clazz, g_methods, 2);
    if (ret != 0) {
        LOGI("register native methods failed");
    }
    return JNI_VERSION_1_2;
}

上面的JNI_OnLoad函数是在我们通过System.loadlibrary函数的时候,JVM会回调的一个函数,我们就是在这里做的动态注册的事情,通过env->RegisterNatives注册。

 这里主要讲解一下g_methods对象,下面是JNINativeMethods结构体的定义
 typedef struct {
     const char* name;   //对应java中native的函数名
     const char* signature;  //java中native函数的函数签名
     void*       fnPtr;  //C这边实现的函数指针
 } JNINativeMethod;

 其中signature指的是函数的签名

三、函数签名
3.1 什么是函数签名:
所谓函数签名,简单点的理解可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名。这个是一一对应的关系。有些人可能会问:函数名不能作为标识么?答案当然是否定的
3.2 为什么需要函数的签名:
我们知道,java是支持函数重载的。一个类里面可以有多个同名但是不同参数的函数,所以函数名+参数名才唯一构成一个函数标识,因此我们需要针对参数做一个签名标识。这样jni层才能唯一识别到一个函数
3.3 如何获取函数的签名
函数的签名是针对函数的参数以及返回值进行组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3…)返回值类型。例如我们上面的numAdd函数一样。他在java层的函数声明是

3.4 public static native int numAdd(int a, int b);
两个参数都是int,并且返回值也是int,所以的函数签名是(II)I。

3.5 public native void dumpMessage();
而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V

3.6 函数类型对应的签名的映射关系:

类型标识                       Java类型
Z                             boolean
B                             byte
C                             char
S                             short
I                             int
J                             long
F                             float
D                             double
L/java/language/String        String
[I                            int[]
[Ljava/lang/object            Object[]
V                             void

四、JNIEnv
JNIEnv贯穿了整个JNI技术的核心,java层调用native函数主要通过映射关系的建立,但jni函数调用java层的函数就要通过JNIEnv了
4.1 何为JNIEnv:
JNIEnv是JVM内部维护的一个和线程相关的代表JNI环境的结构体,这个结构体是和线程相关的。并且C函数里面的线程与java函数中的线程是一一对应关系。也就是说,如果在java里的某个线程调用jni接口,不管调用多少个JNI接口,传递的JNIEnv都是同一个对象。因为这个时候java只有一个线程,对应的JNI也只有一个线程,而JNIEnv是跟线程绑定的,因此也只有一个

4.2 通过JNIEnv调用java对象方法,通过JNIEnv调用方法大致可以分为以下两步:
a、获取到对象的class,并且通过class获取成员属性
b、通过成员属性设置获取对应的值或者调用对应的方法
c、注意如果jni方法是通过static方式调用的话,这边的jobject表示的是jclass对象,需要进行强转,并不表示一个独立的对象

  public class JniSdk {
        private int mIntArg = 5;
        public int getArg() {
            return mIntArg;
        }
    }

    void dump(JNIEnv *env, jobject obj) {
        LOGI("this is dump message call: %p", obj);
        jclass jc = env->GetObjectClass(obj);
        jmethodID  jmethodID1 = env->GetMethodID(jc,"getArg","()I");
        jfieldID  jfieldID1 = env->GetFieldID(jc,"mIntArg","I");
        jint  arg1 = env->GetIntField(obj,jfieldID1);
        jint arg = env->CallIntMethod(obj, jmethodID1);
        LOGI("show int filed: %d, %d",arg, arg1);
    }

4.3 跨线程如何调用java方法
a,上面可以直接调用的原因是java调用到jni层的时候始终都在同一个线程,因此再jni层可以直接操作从java层传递下来的JNIEnv对象来实现各种操作。但是如果是在JNI层创建的一个额外的线程想调用Java方法呢?这个时候又该如何操作呢?
b,一个java线程和一个jni线程共同拥有一个JNIEnv,如果java线程调用native函数的时候,JVM还没有为这两个线程建立起映射关系,那么就会新创建一个JNIEnv并且传递到jni线程,如果之前已经有创建过映射关系。那么就直接采用原来的JNIEnv 。如上面所描述的那样,两个JNIEnv的对象是相同的。反之也一样,如果jni调用java线程的话,那么需要向JVM申请获取到已经映射的JNIEnv,如果之前未映射过的话。那么就重新创建一个,这个方法就是AttachCurrentThread。

JNIEnv *g_env;
void *func1(void* arg) {
    LOGI("into another thread");
    //使用全局保存的g_env,进行操作java对象的时候程序会崩溃
    jmethodID  jmethodID1 = g_env->GetMethodID(jc,"getArg","()I");
    jint arg = g_env->CallIntMethod(obj, jmethodID1);

    //通过这种方法获取的env,然后再进行获取方法进行操作不会崩溃
    JNIEnv *env;
    j_vm->AttachCurrentThread(&env,NULL);
}

void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg function, env :%p", env);
    g_env = env;
    pthread_t *thread;
    pthread_create(thread,NULL, func1, NULL);
}
上面表示JNIEnv跟每个线程是捆绑的,无法在线程B访问到线程A的JNIEnv,所以通过保存g_env的方式去使用是不行的。而是应该要通过AttachCurrentThread方法进行获取新的JNIEnv,然后再进行调用。

五、销毁
a,java创建的对象是由垃圾回收器来回收和释放内存的,但java的那种方式在jni那边是行不通的。在JNI层,如果使用ObjectA = ObjectB的方式来保存变量的话,这种是没办法保存变量的,随时会被回收,我们必须要通过env->NewGlobalRef和env->NewLocalRef的方式来创建,还有一个env->NewWeakGlobalRef(这种很少使用)
b,两种的生命周期的情况如下:
NewLocalRef创建的变量再函数调用结束后会被释放掉
NewGlobalRef创建的变量除非手动delete掉,否则会一直存在

六、JNIEnv操作Java端的代码,主要方法:

函数名称 作用
NewObject 创建Java类中的对象
NewString 创建Java类中的String对象
NewArray 创建类型为Type的数组对象
GetField 获得类型为Type的static的字段
SetField 创建Java类中的对象
GetStaticField 创建Java类中的对象
SetStaticField 设置类型为Type的static的字段
CallMethod 调用返回值类型为Type的static方法
CallStaticMethod 调用返回值类型为Type的static方法

七、Java 、C/C++中的常用数据类型的映射关系表

JNI中定义的别名 Java类型 C/C++类型
jint / jsize int int
jshort short short
jlong long long / long long (__int64)
jbyte byte signed char
jboolean boolean unsigned char
jchar char unsigned short
jfloat float float
jdouble double double
jobject Object _jobject*

八、函数类型对应的签名的映射关系:

Java类型 字段描述符(签名) 备注
int I int的首字母、大写
float F float的首字母、大写
double D double的首字母、大写
short S short的首字母、大写
long L long的首字母、大写
char C char的首字母、大写
byte B byte的首字母、大写
boolean Z 因B已被byte使用,所以JNI规定使用Z
object L + /分隔完整类名 String 如: Ljava/lang/String
array [ + 类型描述符 int[] 如:[I
void V i无返回值类型
Method (参数字段描述符…)返回值字段描述符 int add(int a,int b) 如:(II)I

九、JNI 打印日志
9.1 Cmake文件中有log模块引用,不然编译不通过

# 编译一个库
add_library(
    native-lib   # 库的名字
    SHARED      # 动态库(.so库)
    native-lib.cpp  # 需要编译的C++文件
)

# 相当于定义一个变量log-lib,引用安卓的打印模块
find_library(
    log-lib
    log
)

# 将变量log-lib连接到so库(我这边的so库名字是native-lib)中,这样这个库就能使用日志了
target_link_libraries(
   native-lib
   ${log-lib}
)

9.2 然后在cpp文件中加入:

#include 
#define TAG "kang"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);

9.3 使用日志打印:

 extern "C" JNIEXPORT jstring JNICALL
 Java_com_example_jnidemo_MainActivity_stringFromJNI(JNIEnv* env,jobject) {
     std::string hello = "Hello from C++";
     LOGD("jni打印(LOGD)")
     LOGE("jni打印(LOGE)")
     LOGI("jni打印(LOGI)")
     return env->NewStringUTF(hello.c_str());
 }

十、完整源码
10.1,静态注册:
Native层:NativeLib.java

package com.bob.nativelib;
public class NativeLib {
    static {
        System.loadLibrary("nativelib");
    }
    public native String stringFromJNI();
}

C++层:nativelib.cpp

#include 
#include 

#include 
#define TAG "kang"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);


extern "C" JNIEXPORT jstring JNICALL
Java_com_bob_nativelib_NativeLib_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    LOGE("Hello from C++");
    return env->NewStringUTF(hello.c_str());
}

java调用层

//静态注册
public static void main(String[] args) {
    NativeLib nativeLib=new NativeLib();
    System.out.println(nativeLib.stringFromJNI());
}

10.2,动态注册:C语言
native层:JNITools.java

package com.bob.nativelib;
public class JNITools {
    static {
        System.loadLibrary("dynamicnativelib");
    }
    public static native int  add(int a,int b);
}

C层:dynamicnativelib.c

#include "jni.h"

//日志打印
#include 
#define TAG "kang"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);

//加
jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
    return a+b;
}

//三个参数,java层函数名,java 层方法签名,C 层方法指针
//获取签名方法: javap -s -p DynamicRegister.class
static const JNINativeMethod methods[]={
        {"add","(II)I",(void*)addNumber},
};
//java层load时,便会自动调用该方法
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved){

    JNIEnv* env = NULL;
    //获得 JniEnv
    int r = (*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6);
    if(r != JNI_OK){
        return  -1;
    }
    //FindClass,反射,通过类的名字反射
    jclass mainActivityCls = (*env)->FindClass(env, "com/bob/nativelib/JNITools");//注册 如果小于0则注册失败

    //注册方法
    r = (*env)->RegisterNatives(env,mainActivityCls,methods,sizeof(methods)/sizeof(methods[0]));
    if(r != JNI_OK){
        return -1;
    }
    return JNI_VERSION_1_6;
}

java调用层:

//动态注册
public static void main(String[] args) {
     //动态注册c库
    JNITools jniTools=new JNITools();
    System.out.println(String.valueOf(jniTools.add(100,100)));
}

10.3,动态注册:C++语言
native层:JNITools2.java

package com.bob.nativelib;
public class JNITools2 {
    static {
        System.loadLibrary("dynamicnativelib2");
    }
    public static native int  add(int a,int b);
}

C++层:dynamicnativelib2.cpp

#include 

//加
jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
    return a+b;
}

//三个参数,java层函数名,java 层方法签名,C 层方法指针
//获取签名方法: javap -s -p DynamicRegister.class
static const JNINativeMethod methods[]={
        {"add","(II)I",(void*)addNumber},
};
//java层load时,便会自动调用该方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
    //获得 JniEnv
    JNIEnv *jniEnv{nullptr};
    if (vm->GetEnv((void **) &jniEnv, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    //FindClass,反射,通过类的名字反射
    jclass mainActivityCls = jniEnv->FindClass("com/bob/nativelib/JNITools2");//注册 如果小于0则注册失败

    //注册方法
    jint ret=jniEnv->RegisterNatives(mainActivityCls,methods,sizeof(methods)/sizeof(methods[0]));
    if (ret != 0) {

        return -1;
    }

    return JNI_VERSION_1_6;
}

java调用层:

//动态注册
public static void main(String[] args) {
    //动态注册c++库
    JNITools2 jniTools2=new JNITools2();
    System.out.println(String.valueOf(jniTools2.add(100,100)));
}

10.4 jin调用java层
native层:TestCallBack.java

package com.bob.nativelib;
public class TestCallBack {
    static {
        System.loadLibrary("jnitojava");
    }
    //回调方法 里面调用了add
    public native void callBackAdd();

    public int add(int x,int y){
        return x+y;
    }
}

C++层:jnitojava.cpp

#include 
#include 

#include 
#define TAG "kang"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);

extern "C" JNIEXPORT void JNICALL
Java_com_bob_nativelib_TestCallBack_callBackAdd(JNIEnv* env,jobject) {
    //1得到字节码 包名:com.bob.nativelib
    jclass jclazz = env->FindClass("com/bob/nativelib/TestCallBack");
    //2得到方法
    jmethodID jmethodIds = env->GetMethodID(jclazz,"add","(II)I");
    //3实例化
    jobject object = env->AllocObject(jclazz);
    //4调用方法
    jint result= env->CallIntMethod(object,jmethodIds,100,1);
    //5打印结果
    LOGE("result:%d",result);
}

java调用层:

public static void main(String[] args) {
        TestCallBack testCallBack=new TestCallBack();
        testCallBack.callBackAdd();
      System.out.println(String.valueOf(testCallBack.add(600,600)));
}

10.5,完整的CMakeLists.txt
原来的JNI项目是需要自己手动配置的,有了CMake就简单多了,会自动帮我们配置项目,第11节将讲述CMake的优势

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.

project("nativelib")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.


# 编译一个库
add_library( # Sets the name of the library.
        nativelib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        nativelib.cpp)

add_library(
        # 库的名字
        dynamicnativelib

        # 动态库(.so库)
        SHARED

        # 需要编译的C++文件
        dynamicnativelib.c)

add_library(
        # 库的名字
        dynamicnativelib2

        # 动态库(.so库)
        SHARED

        # 需要编译的C++文件
        dynamicnativelib2.cpp)

add_library(
        # 库的名字
        jnitojava

        # 动态库(.so库)
        SHARED

        # 需要编译的C++文件
        jnitojava.cpp)


# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.


# 将变量log-lib连接到so库(我这边的so库名字是native-lib)中,这样这个库就能使用日志打印功能了
target_link_libraries( # Specifies the target library.
        nativelib
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )
        
target_link_libraries( # Specifies the target library.
	     dynamicnativelib
	     # Links the target library to the log library
	     # included in the NDK.
	      ${log-lib} )

target_link_libraries( # Specifies the target library.
        dynamicnativelib2
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )
        
target_link_libraries( # Specifies the target library.
        jnitojava
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )

10.5:总结
c库和c++库还是有些区别的,下面要注意的几点,不然编译不通过
a,头部引用

C语言 #include "jni.h"
C++   #include 

b,JNIEnv指针引用和FindClass函数参数有区别

//C语言,FindClass,反射,通过类的名字反射
jclass mainActivityCls = (*env)->FindClass(env, "com/bob/nativelib/JNITools");
    
//C++语言,FindClass,反射,通过类的名字反射
jclass mainActivityCls = env->FindClass("com/bob/nativelib/JNITools2");

十一,CMake
10.1 CMake的优势:

  • 可以直接的在C/C++代码中加入断点,进行调试
  • java引用的C/C++中的方法,可以直接ctrl+左键进入
  • 对于include的头文件或者库,也可以直接进入
  • 不需要配置命令行操作,手动的生成头文件,不需要配置android.useDeprecatedNdk=true属性

10.2 传统JNI方式步骤:

  1. 新建jni目录,写好C/C++代码。静态注册JNI时我们使用了javah
    -jni对JAVA类进行操作自动生成了jni目录以及对应的头文件(事实上,当我们有一定经验后可以自己写,而不再需要使用该辅助命令来保证不写错,另外动态注册也是一个很值得提倡的方式),然后根据头文件写了C/C++代码。但在动态注册JNI时我们可以自己先创建好jni目录且写好C/C++代码。
  2. 在jni目录下创建且配置好Android.mk和Application.mk两个文件。
  3. build.gradle文件中根据情况进行配置,可不进行配置使用默认值。
  4. 通过ndk-build操作,我们能得到对应的so文件,放置在相应位置,java代码中即可调用C/C++代码,运行程序。

10.3 CMake方式步骤:

  1. 新建cpp目录,写好C/C++代码。
  2. 创建且配置CMakeLists.txt文件。
  3. build.gradle文件中根据情况进行配置,CMakeLists.txt文件的路径必须配置。
  4. java代码中即可调用C/C++代码,运行程序。
  5. project的build.gradle文件中,gradle版本不能低于2.2,否则会报错。

10.4 CMake和传统JNI的主要区别

  1. 以前的jni目录改成cpp,名字更换了,下面还是存放C/C++文件。
  2. 之前对C/C++文件的编译配置Android.mk、Application.mk文件放在jni目录下,现在改成CMakeLists.txt文件。(事实上这些文件的位置是可任意存放的,只需要配置好就行。但最好还是按照默认习惯放置。)

你可能感兴趣的:(android)