传统的关于android使用JNI调用C/C++程序,首先javah 生产头文件,然后拷贝头文件里面的方法到C文件中进行映射调用,也就是JNI的静态注册,上一篇文章已经讲了,由于这种方法生成的映射方法名不太规则也比较长,二呢是调用数据较慢;因此可以使用JNI动态注册方法的方式来解决这个问题。
学习这个JNI动态注册的时候还是遇到了挺多的问题的,现在就讲这些问题做下记录。
首先是对Java的不了解,不懂Java中对于packet的运用,导致程序无法运行,找不到class,参考了一下下面这个做法:
java 源文件带有包名,往往容易出错
如:H:\code\Hello2.java
package com.example;
public class Hello2{
public static void main(String[]args){
System.out.println("Hello2");
}
}
这代码看上去没什么问题,执行:
H:\code>javac Hello2.java
H:\code>java Hello2
错误: 找不到或无法加载主类 Hello2
解决办法:
+.删除包名 或者
+.在code 下创建一个与包名相同的文件结构(H:\code\com\example\Hello2.java)
编译:H:\code>javac com/example/Hello2.java
运行:H:\code>java com.example.Hello2
通过这个例子,我知道了运行带packet的Java的class文件,需要补全前缀。
遇到这个问题的时候,我想到了几个解决的思路,写一个apk然后调用库,或者写一个Java service调用库。但是,一时之间不太想去学这些,有没有什么其他的解决办法?有,Android上虽然不能直接运行Java程序,但是可以通过jar包来运行。详细的方式可以查看前一篇博客:Android == 在Android系统上运行JAVA程序
有了准备工作之后,就开始动手吧!首先在Android工程下随便建一个文件夹jnitest,继续建四个文件夹 src -> com -> example -> myjni,注意:这几个文件夹是嵌套的。在myjni文件夹中新建两个java文件,jnitest_javaclass_a.java和jnitest_javaclass_b.java。然后编写这两个Java程序,格式其实和静态注册的是一样的:
/* jnitest_javaclass_a.java */
package com.pioneer.jnitest;
import java.lang.ref.WeakReference;
public class jnitest_javaclass_a
{
static
{
System.loadLibrary("jnitest_nativelibrary"); //加载库
}
public native void jnitest_nativefunction_01();
public native void jnitest_nativefunction_02();
public void jnitest_javafunction()
{
jnitest_nativefunction_01();
jnitest_nativefunction_02();
}
public static void main(String args[])
{
jnitest_javaclass_a class_instance = new jnitest_javaclass_a();
class_instance.jnitest_javafunction();
}
}
/* jnitest_javaclass_b.java */
package com.pioneer.jnitest;
public class jnitest_javaclass_b
{
static
{
System.loadLibrary("jnitest_nativelibrary");
}
public native void jnitest_nativefunction_withpara(int para);
public void jnitest_javafunction()
{
jnitest_nativefunction_withpara(10);
}
public static void main(String args[])
{
jnitest_javaclass_b class_instance = new jnitest_javaclass_b();
class_instance.jnitest_javafunction();
}
}
这样,两个Java程序就编写好了,接下来就是编译,
javac jnitest_javaclass_a.java jnitest_javaclass_b.java
然后切换到com那一级目录,将两个class文件打包成jar包,
dx --dex --output=jnitest.jar com/pioneer/jnitest/jnitest_javaclass_a.class com/pioneer/jnitest/jnitest_javaclass_b.class
如此,便生成了jnitest.jar这个jar包。
动态注册和静态注册的不同之处就是动态注册没有了生成.h文件,在Java中调用System.loadLibrary("jnitest_nativelibrary");这句话的时候会去so库里面,也就是cpp中寻找JNI_OnLoad这个函数,然后在JNI_OnLoad这个函数中会将Java中的接口和本文件中的接口进行绑定,如下:
static JNINativeMethod gMethods_class[] = {
{"jni_nativefunction", "()V", (void *)android_jni_nativefunction},
};
通过这个结构体可以进行Java程序和c++程序之间绑定,在{}中间的参数,第一个是Java中调用的native接口名称,第二个是接口的参数和返回值,第三个参数是本文件函数的指针,用于回调。第二个参数中()中的字符表示参数,后面的则代表返回值,如“()V”就表示void Func(), “(II)V”表示void Func(int, int)等。其对应关系如下:
字符 | Java类型 | C类型 |
V | void | void |
Z | jboolean | boolean |
I | jint | int |
J | jlong | long |
D | jdouble | double |
F | jfloat | float |
B | jbyte | byte |
C | jchar | char |
S | jshort | short |
数组则以“[”开始, 用两个字符表示 |
||
[I | jintArray | int[] |
[F | jfloatArray | float[] |
[D | jdoubleArray | double[] |
[Z | jbooleanArray | boolean[] |
[B | jbyteArray | byte[] |
[C | jcharArray | char[] |
[S | jshortArray | short[] |
[J | jlongArray | long[] |
如果java函数的参数是class,则以“L”开头,以“;”结尾,中间是用“/”隔开的包及类名,对应的C函数名的参数则为jobject,一个例外是String类,其对应类为jstring。
字符 | java类型 | C类型 |
Ljava/lang/String | String | jstring |
Ljava/net/Socket | Socket | jobject |
以MediaPlayer为例,Java层定义了如下native函数:
private native void getParameter(int key, Parcel reply);
其对应的签名信息为:"(ILandroid/os/Parcel;)V"
好了,有了JNI的签名信息对照,然后就能撸代码了:
#define LOG_TAG "JNITEST"
#include "utils/Log.h"
#include
#include
#include
#include
#include
#include
#include "jni.h"
#include "JNIHelp.h"
#include "android_runtime/AndroidRuntime.h"
#include "utils/Errors.h" // for status_t
#include "utils/KeyedVector.h"
#include "utils/String8.h"
using namespace android;
//Java jnitest_javaclass_a类接口jnitest_nativefunction_01的本地C++实现
void android_jnitest_nativefunction_01(JNIEnv* env, jobject thiz)
{
LOGD("android_jnitest_nativefunction_01!\n");
}
//Java jnitest_javaclass_a类接口jnitest_nativefunction_02的本地C++实现
void android_jnitest_nativefunction_02(JNIEnv* env, jobject thiz)
{
LOGD("android_jnitest_nativefunction_02!\n");
}
//Java jnitest_javaclass_b类接口jnitest_nativefunction_withpara的本地C++实现
void android_jnitest_nativefunction_withpara(JNIEnv* env, jobject thiz, jint para)
{
LOGD("This the native fuction with parameter input!\n");
LOGD("Input parameter is %d!\n", para);
}
//结构体绑定jnitest_javaclass_a中的两个接口和本地两个实现
static JNINativeMethod gMethods_class_a[] = {
{"jnitest_nativefunction_01", "()V", (void *)android_jnitest_nativefunction_01},
{"jnitest_nativefunction_02", "()V", (void *)android_jnitest_nativefunction_02},
};
//结构体绑定jnitest_javaclass_b中的接口和本地实现
static JNINativeMethod gMethods_class_b[] = {
{"jnitest_nativefunction_withpara", "(I)V", (void *)android_jnitest_nativefunction_withpara},
};
//开始注册注册jnitest_javaclass_a和结构体
static int register_android_jnitest_nativefunction_forclass_a(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"com/pioneer/jnitest/jnitest_javaclass_a", gMethods_class_a, NELEM(gMethods_class_a));
}
//开始注册注册jnitest_javaclass_b和结构体
static int register_android_jnitest_nativefunction_forclass_b(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"com/pioneer/jnitest/jnitest_javaclass_b", gMethods_class_b, NELEM(gMethods_class_b));
}
//此函数在Java中调用System.loadLibrary("");时会被自动触发,在这个函数中将调用上面的两个注册函数,最终返回JNI版本号
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
if (register_android_jnitest_nativefunction_forclass_a(env) < 0) {
LOGE("ERROR: jnitest_1nativefunction native registration failed\n");
goto bail;
}
if (register_android_jnitest_nativefunction_forclass_b(env) < 0) {
LOGE("ERROR: jnitest_1nativefunction native registration failed\n");
goto bail;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
bail:
return result;
}
总的来说,思路还是比较清晰的,我画了一个简单的思维导图来梳理一下思路:
这就是我对于这个JNI动态注册的理解吧,其实和静态的注册相比,最终要实现的效果是一样的,都是通过在Java程序中调用System.loadLibrary();这个接口来访问libxxx.so库文件,然后两者的区别就是怎么去实现这个.so文件,静态注册是通过javah先生成.h文件,然后根据.h文件中的函数声明来实现具体的接口,最终得到libxxx.so这个库。
而动态注册则不然,动态注册的时候在System.loadLibrary();这个接口被调用的时候,会去c++文件中调用JNI_OnLoad这个函数,然后在这个函数实现所有逻辑,包括Javafunction和nativefunction的绑定以及注册等等。
最后声明一下,静态注册可以在Ubuntu上面直接测试运行,但是动态注册我是在机器上测试的,将cpp生成的库拷贝到安卓系统中的/system/lib/目录下,然后采用dalvikvm -cp jnitest.jar jnitest_javaclass_a来在Android上执行Java程序,具体的问题之前已经解释过了。最后在机器上运行的结果如下: