美文网首页Android JNI
Android学习笔记: JNI

Android学习笔记: JNI

作者: EddieLin | 来源:发表于2018-07-08 06:47 被阅读13次

    概念整理

    JNI是Java native interface 的简称,它是Jave功能组建和native C或者C++协同工作的一种方式。Android提供了NDK(Native Development Kit)用来编译打包C或者C++编写的代码,以供Java端调用。这些功能在Android Studio中有比较好的图形话界面支持,当然也可以通过命令行来运行。JNI是双向的,可以由Java端来invoke C/C++代码编译出来的binary,也可以由C/C++端来invoke Java代码。一篇不错的快速入门。总结一下,核心主要在于跨语言的类型转换,在转换成当前语言数据类型之后,就是常规编程了,然后在返回时再转换回去对方语言的数据类型。
    从Java端invoke C++的话,就是由javah/javac通过Java method的native modifier生成.h头文件,再由程序员来实现头文件里的interface。具体实现中需要用到JNIEnv这个pointer来运行其指向method table里的method,从而做到一些类型转换。从C++端invoke Java主要是用类似reflection的操作,jclass,jmethodID和jfieldID。

    NativeActivity是一个Android提供的Helper class,用来使得开发者更方便开发native activity。所谓的Native activity就是程序员把UI的逻辑全部写在Native C++层。这样的好处是可以更方便地进行OpenGL的rendering。事实上,开发者只需要实现native_activity.h头文件里的一些callback方法就可以了。Android官方也给出了android_native_app_glue这样的interface来进一步简化开发。付一个比较简单的例子和一个官方样例

    ABI和API的对比。首先API大家比较熟悉,是Application Programming Interface,是一种调用外部函数或功能组件的方式, 包括protocol,tools或者OS功能组建。这种Interface是基于source code(源代码)。ABI是Application Binary Interface的简称,这种interface则是基于binary code的。在程序员写代码调用Library时,是针对API编程的,而在source code编译之后,程序则是调用ABI来实现功能。API设计时尽量保证稳定性,但是功能扩展或者业务逻辑变动,则无法避免改变原来的interface。而ABI,由于它所定义的是比较底层的功能,本身的操作比较简单。在设计时要求有更高的稳定性,很多时候只允许增加新功能,而不能改变现有功能。

    数据类型

    JNI的Java端就是基本的Java数据类型,在C/C++端则专门定义了一些数据类型,主要有以下这些。

    JNI基本数据类型

    对于Java对象,有相对应的native类型。


    JNI引用类型

    在C中其他的Java类都被定义为jobject类型;在C++中,这些基本类型都被用类型定义,例如:

    class _jobject {};
    class _jclass : public _jobject {};
    typedef _jobject *jobject;
    typedef _jclass *jclass;
    

    而MethodID和FieldID则是普通的C语言指针。

    struct _jfieldID; /* opaque structure */
    typedef struct _jfieldID *jfieldID; /* field IDs */
    struct _jmethodID; /* opaque structure */
    typedef struct _jmethodID *jmethodID; /* method IDs */
    

    在C/C++端来读取从Java端传递过来的对象时,需要用到GetFieldID和GetMethodID方法来获取引用。这两个方法都需要传入一个字符串来描述filed和method的signature。以下是字符串里元素和具体Java类型的一个映射表。

    Signature类型映射

    大家对比上面的映射表,再看下面这个例子就很容易看懂了。

    // 对应的Java类里有“name”field他的类型是java.lang.string
    (*env)->GetFieldID(env, class, "name", "Ljava/lang/String;");
    // 对应的Java类里有“setName” method,它的signature是void setName(String name, int[] accounts)
    (*env)->GetMethodID(env, class, "setInfo", "(Ljava/lang/String;[I)V"); 
    

    JNI编写过程

    这里以上面提到的快速入门里的代码为例,整理一下JNI编写过程。

    Java端

    public class Hello {
      // native 这个关键词是用来声明native方法的。意思就是在C/C++端会有这个方法的实现。
      // 那么在Java端,就可以执行这个方法,具体使用跟Java的一般方法并无区别。
      public native void sayHi(String who, int times); 
    
      // 通常我们用static代码段来加载native库。作为参数的字符串则是库名称。
      static {
        System.loadLibrary("HelloImpl");
      } 
    
      public static void main (String[] args) {
        Hello hello = new Hello();
        // 执行native代码,传入Java端的参数。与一般的Java并无区别。
        hello.sayHi(args[0], Integer.parseInt(args[1]));
      }
    }
    

    说明一下,对于native库的命名,使用在不同的平台中,相同的native代码被编译成不同的文件类型,上面那个库:

    • Unix: libHelloImpl.so
    • Windows:HelloImpl.dll
    • Mac:libHelloImpl.jnilib
      但是在Java代码中loadLibrary时,统一引用成“HelloImpl”。另外,lib前缀自动产生,在命名C/C++库时并不需要刻意加上lib。

    C/C++端

    可以使用JDK中自带的javah工具来自动生成头文件。
    例如,对于Hello.java文件执行以下命令。

    ## 编译Java源代码 ./classes是目标文件夹
    javac -d ./classes/ ./src/com/marakana/jniexamples/Hello.java
    cd classes
    ## 在classes文件夹下运行
    javah -jni com.marakana.jniexamples.Hello
    

    于是会产生一个如下com_marakana_jniexamples_Hello.h头文件。

    // 包括一些JNI中会用到的macro和接口。
    #include <jni.h> 
    ...
    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi (JNIEnv *, jobject, jstring, jint);
    

    接着就可以实现这个接口了。

    #include <stdio.h>
    #include "com_marakana_jniexamples_Hello.h"
    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi(JNIEnv *env, jobject obj, jstring who, jint times) { 
      jint i; 
      jboolean iscopy;
      const char *name;
      name = (*env)->GetStringUTFChars(env, who, &iscopy);
      for (i = 0; i < times; i++) { 
        printf("Hello %s\n", name); 
      }
    }
    

    接着就是编译这个代码成库文件。

    # Linux
    gcc -o libHelloImpl.so -lc -shared \
        -I/usr/local/jdk1.6.0_03/include \
        -I/usr/local/jdk1.6.0_03/include/linux com_marakana_jniexamples_Hello.c
    # Mac
    gcc -o libHelloImpl.jnilib -lc -shared \
        -I/System/Library/Frameworks/JavaVM.framework/Headers com_marakana_jniexamples_Hello.c
    

    测试

    LD_LIBRARY_PATH指向库文件所在目录。

    # 库文件在当前目录
    export LD_LIBRARY_PATH=.
    

    执行

    java com.marakana.jniexamples.Hello Student 5
    Hello Student
    Hello Student
    Hello Student
    Hello Student
    Hello Student
    

    至此,这个helloworld程序编写完成。

    native库的载入

    载入方法有两种:

    1. System.load,参数是库文件的绝对路径,例如Windows下:
    System.load("C://Documents and Settings//TestJNI.dll");
    
    1. System.loadLibrary,参数是库的名称,例如:
    System.loadLibrary ("TestJNI");
    

    第二种方式下,库文件必须在库索路径下,可以通过System.getProperty("java.library.path");打印出搜索路径。默认的搜索路径因系统而异,一般包括:

    1. JRE目录。
    2. 操作系统库文件目录。

    可以通过两种方法改变其值:

    1. 改写java.library.path的值。这样做会完全覆盖路径,包括系统的路径。所以不推荐这么做。
    java -Djava.library.path=/jni/library/path
    
    1. 通过设置环境变量。这样修改的仅仅是用户的库文件路径,并不会影响系统的路径。
    export LB_LIBRARY_PATH=$LB_LIBRARY_PATH:/jni/library/path
    

    进阶:在C/C++端access Java对像

    这个在之前已经有过举例。下面还是以代码来举一个完整的例子来解释一下。

    package com.marakana.jniexamples;
    
    public class InstanceAccess {
      // 加载native库
      static {
        System.loadLibrary("instanceaccess");
      }
      // public,会在native代码中access
      public String name;
      // public,会在native代码中access
      public void setName(String name) {
        this.name = name; 
      }
      public native void propertyAccess();
      public native void methodAccess();
      public static void main(String args[]) {
        InstanceAccess instanceAccessor = new InstanceAccess();
        ...
        // 这是一个native方法的call,可以跳转到下面的native代码查看
        instanceAccessor.propertyAccess();
        // 这是一个native方法的call,可以跳转到下面的native代码查看
        instanceAccessor.methodAccess();
        static {
          System.loadLibrary("instanceaccess");
        }
      }
    }
    

    下面是native的代码

    #include <stdio.h>
    #include "com_marakana_jniexamples_InstanceAccess.h"
    
    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_propertyAccess(JNIEnv *env, jobject object){
      jfieldID fieldId;
      jstring jstr;
      const char *cString;
      // 1. 获得类引用
      jclass class = (*env)->GetObjectClass(env, object);
      // 2. 获得fieldId引用
      fieldId = (*env)->GetFieldID(env, class, "name", "Ljava/lang/String;");
      if (fieldId == NULL) {
        return;
      }
      // 3. access field值
      jstr = (*env)->GetObjectField(env, object, fieldId);
      // 4. 数据类型转换Java->C/C++
      cString = (*env)->GetStringUTFChars(env, jstr, NULL);
      if (cString == NULL) {
        return;
      } 
      printf("C: value of name before property modification = \"%s\"\n", cString);
      (*env)->ReleaseStringUTFChars(env, jstr, cString);
      jstr = (*env)->NewStringUTF(env, "Brian");
      if (jstr == NULL) {
        return;
      }
      (*env)->SetObjectField(env, object, fieldId, jstr);
    }
    
    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_methodAccess(JNIEnv *env, jobject object){
      // 1. 获得类引用
      jclass class = (*env)->GetObjectClass(env, object);
      // 2. 获得methodId引用
      jmethodID methodId = (*env)->GetMethodID(env, class, "setName", "(Ljava/lang/String;)V");
      jstring jstr;
      if (methodId == NULL) {
        return;
      }
      // 3. 数据类型转换Java->C/C++
      jstr = (*env)->NewStringUTF(env, "Nick");
      // 4. access method
      (*env)->CallVoidMethod(env, object, methodId, jstr);
    }
    

    可以发现native两种access的方法步骤相似:

    1. GetObjectClass获得类引用
      1.1. Optional:类型转换
    2. GetFieldID/GetMethodID
    3. access field/method
      3.1. Optional:类型转换
      获取field和method有个比较方便的工具
    // ClassName是一个类名
    javap -s -p ClassName
    

    Android中引用native库

    这里简单地说一下,具体的可以参考Android官方文档,细节实在非常庞杂,就不再赘述了。大致就是
    Android.mk 定义native组件;Application.mk 定义怎么在App中使用这些native组件。ndk-build 是一个官方的脚本文件来编译源代码。更高端的可以使用 toolchain 来自定义编译过程。
    两个简单的方法来添加第三方native库

    1. 直接把so文件拷贝到默认文件夹,src/main/jniLibs
    2. 在build.gradle里指定位置。
    android {
      ...
      source_set {
        main {
          # so所在文件夹是libs
          jniLibs.srcDirs = ["libs"]
        ...
    ...
    

    参考资料

    JNI Types and Data Structures
    Java Fundamentals Tutorial: Java Native Interface (JNI)
    Android官方NDK开发文档
    关于Android的.so文件你所需要知道的

    相关文章

      网友评论

        本文标题:Android学习笔记: JNI

        本文链接:https://www.haomeiwen.com/subject/nfbnuftx.html