湖畔镇

JNI初学

JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C/C++)

参考使用JNI进行Java与C/C++语言混合编程

JNI

JNI(Java Native Interface,Java本地接口),用于打通Java层与Native(C/C++)层。这不是Android系统所独有的,而是Java所有。众所周知,Java语言是跨平台的语言,而这跨平台的背后都是依靠Java虚拟机,虚拟机采用C/C++编写,适配各个系统,通过JNI为上层Java提供各种服务,保证跨平台性。

JNI注册

Android系统在启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会AndroidRuntime.cpp中的startVm创建虚拟机,VM创建完成后,紧接着调用startReg完成虚拟机中的JNI方法注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int AndroidRuntime::startReg(JNIEnv* env) {
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
env->PushLocalFrame(200);
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) {
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}

// 该数组的每个成员都代表一个类文件的JNI映射,其中REG_JNI是一个宏定义,该宏的作用就是调用相应的方法
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_os_MessageQueue),
REG_JNI(register_android_os_Binder),
...
};

JNI查找

系统方法

MessageQueue为例:

  • android.os.MessageQueue.nativePollOnce的本地方法是android_os_MessageQueue_nativePollOnce
  • MessageQueue对应的注册方法是register_android_os_MessageQueue,存在于gRegJNI数组中,在启动时就完成了注册
  • 对应的本地文件是android_os_MessageQueue.cpp

自定义方法

1
2
3
4
5
6
7
8
9
public class MediaPlayer{
static {
System.loadLibrary("media_jni");
native_init();
}

private static native final void native_init();
...
}

通过static静态代码块中System.loadLibrary方法来加载动态库,库名为media_jni, Android平台则会自动扩展成所对应的libmedia_jni.so

一般都是通过Android.mk文件定义LOCAL_MODULE:= libmedia_jni

JNI原理

System.java

1
2
3
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

Runtime.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}

String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return;
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

private String doLoad(String name, ClassLoader loader) {
...
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}

调用本地方法nativeLoad,其中会调用dlopen函数,打开一个so文件并创建一个handle;调用dlsym函数,查看相应so文件的JNI_OnLoad()函数指针,并执行相应函数。

android_media_MediaPlayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
if (register_android_media_MediaPlayer(env) < 0) {
goto bail;
}
...
}

static int register_android_media_MediaPlayer(JNIEnv *env) {
return AndroidRuntime::registerNativeMethods(env, "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}

// gMethods,记录Java层和C/C++层方法的一一映射关系
static JNINativeMethod gMethods[] = {
{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},
{"_start", "()V", (void *)android_media_MediaPlayer_start},
{"_stop", "()V", (void *)android_media_MediaPlayer_stop},
{"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo},
{"_release", "()V", (void *)android_media_MediaPlayer_release},
{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
...
};

jni.h

1
2
3
4
5
6
7
8
typedef struct {
// Java层native函数名
const char* name;
// Java函数签名,记录参数类型和个数,以及返回值类型
const char* signature;
// Native层对应的函数指针
void* fnPtr;
} JNINativeMethod;

AndroidRuntime.cpp

1
2
3
int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

JNIHelper.cpp

1
2
3
4
5
6
7
8
9
10
11
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
scoped_local_ref<jclass> c(env, findClass(env, className));
if (c.get() == NULL) {
e->FatalError("");
}
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
e->FatalError("");
}
return 0;
}

jni.h

1
2
3
4
5
6
7
struct _JNIEnv {
const struct JNINativeInterface* functions;

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
{ return functions->RegisterNatives(this, clazz, methods, nMethods); }
...
}

虚拟机相关的变量中有两个非常重要的量JavaVMJNIEnv
JavaVM是指进程虚拟机环境,每个进程有且只有一个JavaVM实例
JNIEnv是指线程上下文环境,每个线程有且只有一个JNIEnv实例

数据结构

基本数据类型

Signature格式 Java Native
B byte jbyte
C char jchar
D double jdouble
F float jfloat
I int jint
S short jshort
J long jlong
Z boolean jboolean
V void void

数组数据类型

Signature格式 Java Native
[B byte[] jbyteArray
[C char[] jcharArray
[D double[] jdoubleArray
[F float[] jfloatArray
[I int[] jintArray
[S short[] jshortArray
[J long[] jlongArray
[Z boolean[] jbooleanArray

复杂数据类型

Signature格式 Java Native
Ljava/lang/String; String jstring
L+classname +; 所有对象 jobject
[L+classname +; Object[] jobjectArray
Ljava.lang.Class; Class jclass
Ljava.lang.Throwable; Throwable jthrowable

签名实例

Java函数 对应的签名
void foo() ()V
float foo(int i) (I)F
long foo(int[] i) ([I)J
double foo(Class c) (Ljava/lang/Class;)D
boolean foo(int[] i,String s) ([ILjava/lang/String;)Z
String foo(int i) (I)Ljava/lang/String;

Java调用C/C++

  • 新建一个Sample.java源文件

  • 获得Sample.class类文件

javac Sample.java
  • 获得头文件
javah Sample
  • 通过C或者C++来实现这些函数,然后生成dll或者so,在mac上,需要生成libSample.jnilib
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -c Sample.c  
gcc -dynamiclib -o libSample.jnilib Samplie.o
  • 运行
java Sample

Sample.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
JNIEXPORT jint JNICALL Java_Sample_intMethod
(JNIEnv *env, jobject obj, jint num)
{
return num * num;
}

JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod
(JNIEnv *env, jobject obj, jboolean boolean)
{
return !boolean;
}

JNIEXPORT jstring JNICALL Java_Sample_stringMethod
(JNIEnv *env, jobject obj, jstring string)
{
const char* str = (*env)->GetStringUTFChars(env, string, 0);
char cap[128];
strcpy(cap, str);
(*env)->ReleaseStringUTFChars(env, string, 0);
return (*env)->NewStringUTF(env, strupr(cap));
}

JNIEXPORT jint JNICALL Java_Sample_intArrayMethod
(JNIEnv *env, jobject obj, jintArray array)
{
int i, sum = 0;
jsize len = (*env)->GetArrayLength(env, array);
jint *body = (*env)->GetIntArrayElements(env, array, 0);

for (i = 0; i < len; ++i)
{
sum += body[i];
}
(*env)->ReleaseIntArrayElements(env, array, body, 0);
return sum;
}

(*env)->GetStringUTFChars()这个方法,是用来在Java和C之间转换字符串的,因为Java本身都使用了双字节的字符, 而C语言本身都是单字节的字符,所以需要进行转换

JNIEnv*是每个函数都有的参数, 它包含了很多有用的方法, 使用起来类似Java的反射,也提供了这样一个编码转换的函数

GetStringUTFChars()NewStringUTF(), 第一个是从UTF8转换为C的编码格式, 第二个是根据C的字符串返回一个UTF8字符串

ReleaseStringUTFChars()是用来释放对象的, 在Java中有虚拟机进行垃圾回收,但是在C语言中,这些对象必须手动回收,否则可能造成内存泄漏

C/C++调用Java

Sample.java

1
2
3
4
5
6
7
8
9
10
11
public class Sample {
public String name;

public static String sayHello(String name) {
return "Hello, " + name + "!";
}

public String sayHello() {
return "Hello, " + name + "!";
}
}

SampleTest.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <jni.h>
#include <string.h>
#include <stdio.h>

// 环境变量PATH在windows下和linux下的分割符定义
#ifdef _WIN32
#define PATH_SEPARATOR ';'
#else
#define PATH_SEPARATOR ':'
#endif


int main(void) {
JavaVMOption options[1];
JNIEnv *env;
JavaVM *jvm;
JavaVMInitArgs vm_args;

long status;
jclass cls;
jmethodID mid;
jfieldID fid;
jobject obj;

options[0].optionString = "-Djava.class.path=.";
memset(&vm_args, 0, sizeof(vm_args));
vm_args.version = JNI_VERSION_1_4;
vm_args.nOptions = 1;
vm_args.options = options;

// 启动虚拟机
status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

if (status != JNI_ERR) {
// 先获得class对象
cls = (*env)->FindClass(env, "Sample2");
if (cls != 0) {
// 获取方法ID, 通过方法名和签名, 调用静态方法
mid = (*env)->GetStaticMethodID(env, cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;");
if (mid != 0) {
const char* name = "World";
jstring arg = (*env)->NewStringUTF(env, name);
jstring result = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid, arg);
const char* str = (*env)->GetStringUTFChars(env, result, 0);
printf("Result of sayHello: %s\n", str);
(*env)->ReleaseStringUTFChars(env, result, 0);
}

// 调用指定的构造函数, 构造函数的名字叫做<init>
mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);
if (obj == 0) {
printf("Create object failed!\n");
}

// 获取属性ID, 通过属性名和签名
fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
if (fid != 0) {
const char* name = "Tom";
jstring arg = (*env)->NewStringUTF(env, name);
(*env)->SetObjectField(env, obj, fid, arg); // 修改属性
}

// 调用成员方法
mid = (*env)->GetMethodID(env, cls, "sayHello", "()Ljava/lang/String;");
if (mid != 0) {
jstring result = (jstring)(*env)->CallObjectMethod(env, obj, mid);
const char* str = (*env)->GetStringUTFChars(env, result, 0);
printf("Result of sayHello: %s\n", str);
(*env)->ReleaseStringUTFChars(env, result, 0);
}
}

(*jvm)->DestroyJavaVM(jvm);
return 0;
} else {
printf("JVM Created failed!\n");
return -1;
}
}

代码比较好理解,做了这些事:

  • 创建虚拟机JVM, 在程序结束的时候销毁虚拟机JVM
  • 寻找class对象
  • 创建class对象的实例
  • 调用方法和修改属性

JavaVM就是我们需要创建的虚拟机实例
JavaVMOption相当于在命令行里传入的参数
JNIEnv在Java调用C/C++中每个方法都会有的一个参数, 拥有一个JNI的环境
JavaVMInitArgs就是虚拟机创建的初始化参数,这个参数里面会包含JavaVMOption

在MAC下用gcc编译遇到找不到JVM库的问题,还不知道怎么解决

AIDL

AIDL(Android 接口定义语言)与您可能使用过的其他IDL类似。 您可以利用它定义客户端与服务使用进程间通信(IPC)进行相互通信时都认可的编程接口。 在Android上,一个进程通常无法访问另一个进程的内存。 尽管如此,进程需要将其对象分解成操作系统能够识别的原语,并将对象编组成跨越边界的对象。编写执行这一编组操作的代码是一项繁琐的工作,因此Android会使用AIDL来处理。

只有允许不同应用的客户端用IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用AIDL。 如果您不需要执行跨越不同应用的并发IPC,就应该通过实现一个Binder创建接口;或者,如果您想执行IPC,但根本不需要处理多线程,则使用Messenger类来实现接口。无论如何,在实现AIDL之前,请您务必理解绑定服务。

定义AIDL接口

您必须使用Java编程语言语法在.aidl文件中定义AIDL接口,然后将它保存在托管服务的应用以及任何其他绑定到服务的应用的源代码内。

您开发每个包含.aidl文件的应用时,Android SDK工具都会生成一个基于该.aidl文件的IBinder接口,并将其保存在项目的gen/目录中。服务必须视情况实现IBinder接口。然后客户端应用便可绑定到该服务,并调用IBinder中的方法来执行IPC。

如需使用 AIDL 创建绑定服务,请执行以下步骤:

  1. 创建 .aidl 文件
    此文件定义带有方法签名的编程接口。
  2. 实现接口
    Android SDK工具基于您的.aidl文件,使用Java编程语言生成一个接口。此接口具有一个名为Stub的内部抽象类,用于扩展Binder类并实现AIDL接口中的方法。您必须扩展Stub类并实现方法。
  3. 向客户端公开该接口
    实现Service并重写onBind()以返回Stub类的实现。

创建AIDL文件

AIDL使用简单语法,使您能通过可带参数和返回值的一个或多个方法来声明接口。 参数和返回值可以是任意类型,甚至可以是其他AIDL生成的接口。

您必须使用Java编程语言构建.aidl文件。每个.aidl文件都必须定义单个接口,并且只需包含接口声明和方法签名。

默认情况下,AIDL支持下列数据类型:

  • Java编程语言中的所有原语类型(如intlongcharboolean 等等)
  • String
  • CharSequence
  • List
    List中的所有元素都必须是以上列表中支持的数据类型、其他AIDL生成的接口或您声明的可打包类型。可选择将List用作通用类(例如,List<String>)。另一端实际接收的具体类始终是ArrayList,但生成的方法使用的是List接口。
  • Map
    Map中的所有元素都必须是以上列表中支持的数据类型、其他AIDL生成的接口或您声明的可打包类型。不支持通用Map(如Map<String,Integer>形式的Map)。另一端实际接收的具体类始终是HashMap,但生成的方法使用的是Map接口。

您必须为以上未列出的每个附加类型加入一个import语句,即使这些类型是在与您的接口相同的软件包中定义。

定义服务接口时,请注意:

  • 方法可带零个或多个参数,返回值或空值。
  • 所有非原语参数都需要指示数据走向的方向标记。可以是inoutinout。原语默认为in,不能是其他方向。

    注意:您应该将方向限定为真正需要的方向,因为编组参数的开销极大。

  • .aidl 文件中包括的所有代码注释都包含在生成的IBinder接口中(import 和 package 语句之前的注释除外)
  • 只支持方法;您不能公开AIDL中的静态字段。

示例aidl文件

1
2
3
4
5
6
package com.example.android;

interface IRemoteService {
int getPid();
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString);
}

实现接口

示例

1
2
3
4
5
6
7
8
9
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
return Process.myPid();
}

public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) {

}
};

在实现 AIDL 接口时应注意遵守以下这几个规则:

  • 由于不能保证在主线程上执行传入调用,因此您一开始就需要做好多线程处理准备,并将您的服务正确地编译为线程安全服务。
  • 默认情况下,RPC调用是同步调用。如果您明知服务完成请求的时间不止几毫秒,就不应该从Activity的主线程调用服务,因为这样做可能会使应用挂起。您通常应该从客户端内的单独线程调用服务。
  • 您引发的任何异常都不会回传给调用方。

向客户端公开接口

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RemoteService extends Service {
@Override
public void onCreate() {
super.onCreate();
}

@Override
public IBinder onBind(Intent intent) {
return mBinder;
}

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
return Process.myPid();
}
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) {

}
};
}

当客户端(如Activity)调用bindService()以连接此服务时,客户端的onServiceConnected()回调会接收服务的onBind()方法返回的mBinder实例。

客户端还必须具有对接口类的访问权限,因此如果客户端和服务在不同的应用内,则客户端的应用src/ 目录内必须包含.aidl文件(它生成 android.os.Binder接口 — 为客户端提供对AIDL方法的访问权限)的副本。

当客户端在onServiceConnected()回调中收到IBinder时,它必须调用asInterface(service)以将返回的参数转换。

1
2
3
4
5
6
7
8
9
10
IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
mIRemoteService = IRemoteService.Stub.asInterface(service);
}

public void onServiceDisconnected(ComponentName className) {
mIRemoteService = null;
}
};

通过IPC传递对象

通过IPC接口把某个类从一个进程发送到另一个进程是可以实现的。不过,您必须确保该类的代码对IPC通道的另一端可用,并且该类必须支持Parcelable接口。支持Parcelable接口很重要,因为Android系统可通过它将对象分解成可编组到各进程的原语。

如需创建支持Parcelable协议的类,您必须执行以下操作:

  • 让您的类实现Parcelable接口
  • 实现writeToParcel,它会获取对象的当前状态并将其写入Parcel。
  • 为您的类添加一个名为CREATOR的静态字段,这个字段是一个实现Parcelable.Creator接口的对象。
  • 最后,创建一个声明可打包类的.aidl 文件
    如果您使用的是自定义编译进程,切勿在您的编译中添加.aidl文件。 此.aidl文件与C语言中的头文件类似,并未编译。
    AIDL在它生成的代码中使用这些方法和字段将您的对象编组和取消编组。

Rect.aidl

1
2
3
package android.graphics;

parcelable Rect;

Rect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
public int left;
public int top;
public int right;
public int bottom;

public static final Parcelable.Creator<Rect> CREATOR = new
Parcelable.Creator<Rect>() {
public Rect createFromParcel(Parcel in) {
return new Rect(in);
}

public Rect[] newArray(int size) {
return new Rect[size];
}
};

public Rect() {
}

private Rect(Parcel in) {
readFromParcel(in);
}

public void writeToParcel(Parcel out) {
out.writeInt(left);
out.writeInt(top);
out.writeInt(right);
out.writeInt(bottom);
}

public void readFromParcel(Parcel in) {
left = in.readInt();
top = in.readInt();
right = in.readInt();
bottom = in.readInt();
}
}

调用IPC方法

调用类必须执行以下步骤,才能调用使用AIDL定义的远程接口:

  • 在项目src/目录中加入.aidl文件
  • 声明一个IBinder接口实例(基于AIDL生成)
  • 实现ServiceConnection
  • 调用Context.bindService(),以传入您的ServiceConnection实现
  • 在您的onServiceConnected()实现中,您将收到一个IBinder实例(名为 service)。调用asInterface((IBinder)service),以将返回的参数转换类型
  • 调用您在接口上定义的方法。您应该始终捕获DeadObjectException异常,它们是在连接中断时引发的;这将是远程方法引发的唯一异常。
  • 如需断开连接,请使用您的接口实例调用Context.unbindService()

有关调用IPC服务的几点说明:

  • 对象是跨进程计数的引用
  • 您可以将匿名对象作为方法参数发送

问题

  1. 传递对象
    .aidl文件和实现文件都放在系统创建的aidl目录下,编译时可能出现找不到符号的问题,需要对build.gradle做修改,增加源码集

    sourceSets {
        main {
            java.srcDirs = ['src/main/java', 'src/main/aidl']
        }
    }
    

另外注意要指定对象的in/out/inout类型,否则会报错

  1. 绑定远程服务
    可能出现服务必须为显示调用的错误,这样解决,通过指定包名
1
2
3
4
Intent intent = new Intent();
intent.setAction("android.intent.action.RemoteService");
intent.setPackage("lakeshire.gitlab.com.aidlserver");
bindService(intent, mConnectionRemote, Context.BIND_AUTO_CREATE);

服务端的AndroidManifest.xml注册一下这个服务

1
2
3
4
5
6
<service android:name=".RemoteService" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.RemoteService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
分享