Android学习笔记: JNI
概念整理
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库的载入
载入方法有两种:
- 用
System.load
,参数是库文件的绝对路径,例如Windows下:
System.load("C://Documents and Settings//TestJNI.dll");
- 用
System.loadLibrary
,参数是库的名称,例如:
System.loadLibrary ("TestJNI");
第二种方式下,库文件必须在库索路径下,可以通过System.getProperty("java.library.path");
打印出搜索路径。默认的搜索路径因系统而异,一般包括:
- JRE目录。
- 操作系统库文件目录。
可以通过两种方法改变其值:
- 改写
java.library.path
的值。这样做会完全覆盖路径,包括系统的路径。所以不推荐这么做。
java -Djava.library.path=/jni/library/path
- 通过设置环境变量。这样修改的仅仅是用户的库文件路径,并不会影响系统的路径。
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的方法步骤相似:
- GetObjectClass获得类引用
1.1. Optional:类型转换 - GetFieldID/GetMethodID
- 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库。
- 直接把so文件拷贝到默认文件夹,src/main/jniLibs
- 在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文件你所需要知道的