Android JNI

Android学习笔记: JNI

2018-07-08  本文已影响13人  EddieLin

概念整理

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代码被编译成不同的文件类型,上面那个库:

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文件你所需要知道的

上一篇下一篇

猜你喜欢

热点阅读