JNI是Java Native Interface
的缩写,它提供了若干的API实现了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 | int AndroidRuntime::startReg(JNIEnv* env) { |
JNI查找
系统方法
以MessageQueue
为例:
android.os.MessageQueue.nativePollOnce
的本地方法是android_os_MessageQueue_nativePollOnce
MessageQueue
对应的注册方法是register_android_os_MessageQueue
,存在于gRegJNI
数组中,在启动时就完成了注册- 对应的本地文件是
android_os_MessageQueue.cpp
自定义方法
1 | public class MediaPlayer{ |
通过static静态代码块中System.loadLibrary
方法来加载动态库,库名为media_jni
, Android平台则会自动扩展成所对应的libmedia_jni.so
库
一般都是通过Android.mk文件定义LOCAL_MODULE:= libmedia_jni
JNI原理
System.java
1 | public static void loadLibrary(String libName) { |
Runtime.java
1 | void loadLibrary(String libraryName, ClassLoader loader) { |
调用本地方法nativeLoad
,其中会调用dlopen
函数,打开一个so文件并创建一个handle;调用dlsym
函数,查看相应so文件的JNI_OnLoad()
函数指针,并执行相应函数。
android_media_MediaPlayer.cpp
1 | jint JNI_OnLoad(JavaVM* vm, void* reserved) { |
jni.h
1 | typedef struct { |
AndroidRuntime.cpp
1 | int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { |
JNIHelper.cpp
1 | extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { |
jni.h
1 | struct _JNIEnv { |
虚拟机相关的变量中有两个非常重要的量JavaVM
和JNIEnv
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 | JNIEXPORT jint JNICALL Java_Sample_intMethod |
(*env)->GetStringUTFChars()
这个方法,是用来在Java和C之间转换字符串的,因为Java本身都使用了双字节的字符, 而C语言本身都是单字节的字符,所以需要进行转换
JNIEnv*
是每个函数都有的参数, 它包含了很多有用的方法, 使用起来类似Java的反射,也提供了这样一个编码转换的函数
GetStringUTFChars()
和NewStringUTF()
, 第一个是从UTF8转换为C的编码格式, 第二个是根据C的字符串返回一个UTF8字符串
ReleaseStringUTFChars()
是用来释放对象的, 在Java中有虚拟机进行垃圾回收,但是在C语言中,这些对象必须手动回收,否则可能造成内存泄漏
C/C++调用Java
Sample.java
1 | public class Sample { |
SampleTest.c
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 创建绑定服务,请执行以下步骤:
- 创建 .aidl 文件
此文件定义带有方法签名的编程接口。 - 实现接口
Android SDK工具基于您的.aidl文件,使用Java编程语言生成一个接口。此接口具有一个名为Stub
的内部抽象类,用于扩展Binder
类并实现AIDL接口中的方法。您必须扩展Stub
类并实现方法。 - 向客户端公开该接口
实现Service
并重写onBind()
以返回Stub类的实现。
创建AIDL文件
AIDL使用简单语法,使您能通过可带参数和返回值的一个或多个方法来声明接口。 参数和返回值可以是任意类型,甚至可以是其他AIDL生成的接口。
您必须使用Java编程语言构建.aidl文件。每个.aidl文件都必须定义单个接口,并且只需包含接口声明和方法签名。
默认情况下,AIDL支持下列数据类型:
- Java编程语言中的所有原语类型(如
int
、long
、char
、boolean
等等) String
CharSequence
List
List
中的所有元素都必须是以上列表中支持的数据类型、其他AIDL生成的接口或您声明的可打包类型。可选择将List
用作通用类(例如,List<String>
)。另一端实际接收的具体类始终是ArrayList
,但生成的方法使用的是List
接口。Map
Map
中的所有元素都必须是以上列表中支持的数据类型、其他AIDL生成的接口或您声明的可打包类型。不支持通用Map
(如Map<String,Integer>
形式的Map
)。另一端实际接收的具体类始终是HashMap
,但生成的方法使用的是Map
接口。
您必须为以上未列出的每个附加类型加入一个import语句,即使这些类型是在与您的接口相同的软件包中定义。
定义服务接口时,请注意:
- 方法可带零个或多个参数,返回值或空值。
- 所有非原语参数都需要指示数据走向的方向标记。可以是
in
、out
或inout
。原语默认为in
,不能是其他方向。注意:您应该将方向限定为真正需要的方向,因为编组参数的开销极大。
- .aidl 文件中包括的所有代码注释都包含在生成的
IBinder接口中
(import 和 package 语句之前的注释除外) - 只支持方法;您不能公开AIDL中的静态字段。
示例aidl文件
1 | package com.example.android; |
实现接口
示例
1 | private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { |
在实现 AIDL 接口时应注意遵守以下这几个规则:
- 由于不能保证在主线程上执行传入调用,因此您一开始就需要做好多线程处理准备,并将您的服务正确地编译为线程安全服务。
- 默认情况下,RPC调用是同步调用。如果您明知服务完成请求的时间不止几毫秒,就不应该从Activity的主线程调用服务,因为这样做可能会使应用挂起。您通常应该从客户端内的单独线程调用服务。
- 您引发的任何异常都不会回传给调用方。
向客户端公开接口
示例
1 | public class RemoteService extends Service { |
当客户端(如Activity)调用bindService()
以连接此服务时,客户端的onServiceConnected()
回调会接收服务的onBind()
方法返回的mBinder实例。
客户端还必须具有对接口类的访问权限,因此如果客户端和服务在不同的应用内,则客户端的应用src/
目录内必须包含.aidl文件(它生成 android.os.Binder
接口 — 为客户端提供对AIDL方法的访问权限)的副本。
当客户端在onServiceConnected()
回调中收到IBinder时,它必须调用asInterface(service)
以将返回的参数转换。
1 | IRemoteService mIRemoteService; |
通过IPC传递对象
通过IPC接口把某个类从一个进程发送到另一个进程是可以实现的。不过,您必须确保该类的代码对IPC通道的另一端可用,并且该类必须支持Parcelable
接口。支持Parcelable
接口很重要,因为Android系统可通过它将对象分解成可编组到各进程的原语。
如需创建支持Parcelable
协议的类,您必须执行以下操作:
- 让您的类实现
Parcelable
接口 - 实现
writeToParcel
,它会获取对象的当前状态并将其写入Parcel。 - 为您的类添加一个名为
CREATOR
的静态字段,这个字段是一个实现Parcelable.Creator
接口的对象。 - 最后,创建一个声明可打包类的.aidl 文件
如果您使用的是自定义编译进程,切勿在您的编译中添加.aidl文件。 此.aidl文件与C语言中的头文件类似,并未编译。
AIDL在它生成的代码中使用这些方法和字段将您的对象编组和取消编组。
Rect.aidl
1 | package android.graphics; |
Rect.java
1 | import android.os.Parcel; |
调用IPC方法
调用类必须执行以下步骤,才能调用使用AIDL定义的远程接口:
- 在项目
src/
目录中加入.aidl文件 - 声明一个
IBinder
接口实例(基于AIDL生成) - 实现
ServiceConnection
- 调用
Context.bindService()
,以传入您的ServiceConnection
实现 - 在您的
onServiceConnected()
实现中,您将收到一个IBinder实例
(名为service
)。调用asInterface((IBinder)service)
,以将返回的参数转换类型 - 调用您在接口上定义的方法。您应该始终捕获
DeadObjectException
异常,它们是在连接中断时引发的;这将是远程方法引发的唯一异常。 - 如需断开连接,请使用您的接口实例调用
Context.unbindService()
。
有关调用IPC服务的几点说明:
- 对象是跨进程计数的引用
- 您可以将匿名对象作为方法参数发送
问题
传递对象
.aidl文件和实现文件都放在系统创建的aidl
目录下,编译时可能出现找不到符号的问题,需要对build.gradle
做修改,增加源码集sourceSets { main { java.srcDirs = ['src/main/java', 'src/main/aidl'] } }
另外注意要指定对象的in
/out
/inout
类型,否则会报错
- 绑定远程服务
可能出现服务必须为显示调用的错误,这样解决,通过指定包名
1 | Intent intent = new Intent(); |
服务端的AndroidManifest.xml注册一下这个服务
1 | <service android:name=".RemoteService" android:exported="true"> |