我们从上图可以看到Android上层的Application和Application Framework都是使用Java编写,底层包括系统和使用众多的Libraries都是C/C++编写的。
所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现,当然底层C/C++函数也可以通过JNI来回调java的方法。
下面将学习Android通过Jni来实现Java对C/C++函数的调用步骤。以HelloWorld程序为例:
package com.cktdemo; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class HelloWorld extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.v("xxj", printJNI("I am HelloWorld Activity")); } static { //加载库文件 System.loadLibrary("HelloWorldJni"); } //声明原生函数 参数为String类型 返回类型为String private native String printJNI(String inputStr); }
这一步我们可以使用eclipse来编译生成一个App;
因为eclipse会自动为我们编译此Java文件,后面会用到。
进入到eclipse生成的Android Project中 :/HelloWorld/bin/classes/com/cktdemo/ 下:
可以看到里面后很多后缀为.class的文件,就是eclipse为我们自动编译好了的java文件,其中就有:HelloWorld.class文件。
退回到classes一级目录:/HelloWorld/bin/classes/
执行如下命令:javah com.cktdemo.HelloWorld
生成文件:com_cktdemo_HelloWorld.h
/* DO NOT EDIT THIS FILE - it is machine generated */ #include/* Header for class com_cktdemo_HelloWorld */ #ifndef _Included_com_cktdemo_HelloWorld #define _Included_com_cktdemo_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: com_cktdemo_HelloWorld * Method: printJNI * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_cktdemo_HelloWorld_printJNI (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
可以看到自动生成对应的函数:Java_com_cktdemo_HelloWorld_printJNI
Java_ + 包名(com.cktdemo) + 类名(HelloWorld) + 接口名(printJNI):必须要按此JNI规范来操作;
java虚拟机就可以在com.cktdemo.HelloWorld类调用printJNI接口的时候自动找到这个C实现的Native函数调用。
当然函数名太长,可以在.c文件中通过函数名映射表来实现简化。
实现JNI原生函数源文件:
新建com_cktdemo_HelloWorld.c文件:
#include#define LOG_TAG "HelloWorld" #include /* Native interface, it will be call in java code */ JNIEXPORT jstring JNICALL Java_com_cktdemo_HelloWorld_printJNI(JNIEnv *env, jobject obj,jstring inputStr) { LOGI("xxj Hello World From libhelloworld.so!"); // 从 instring 字符串取得指向字符串 UTF 编码的指针 const char *str = (const char *)(*env)->GetStringUTFChars( env,inputStr, JNI_FALSE ); LOGI("xxj--->%s",(const char *)str); // 通知虚拟机本地代码不再需要通过 str 访问 Java 字符串。 (*env)->ReleaseStringUTFChars(env, inputStr, (const char *)str ); return (*env)->NewStringUTF(env, "Hello World! I am Native interface"); } /* This function will be call when the library first be load. * You can do some init in the libray. return which version jni it support. */ jint JNI_OnLoad(JavaVM* vm, void* reserved) { void *venv; LOGI("xxj----->JNI_OnLoad!"); if ((*vm)->GetEnv(vm, (void**)&venv, JNI_VERSION_1_4) != JNI_OK) { LOGE("xxj--->ERROR: GetEnv failed"); return -1; } return JNI_VERSION_1_4; }
OnLoadJava_com_cktdemo_HelloWorld_printJNI 函数里面做一些log输出 注意JNI中的log输出的不同。
JNI_OnLoad函数JNI规范定义的,当共享库第一次被加载的时候会被回调,这个函数里面可以进行一些初始化工作,比如注册函数映射表,缓存一些变量等,最后返回当前环境所支持的JNI环境。本例只是简单的返回当前JNI环境。
编译com_cktdemo_HelloWorld.c成so库可以和app一起编译,也可以都单独编译。
在当前目录下建立jni文件夹:HelloWorld/jni/ 下建立Android.mk ,并将com_cktdemo_HelloWorld.c和 com_cktdemo_HelloWorld.h 拷贝进去.
编写编译生成so库的Android.mk文件:
LOCAL_PATH:= $(call my-dir) # 一个完整模块编译 include $(CLEAR_VARS) LOCAL_SRC_FILES:=com_cktdemo_HelloWorld.c LOCAL_C_INCLUDES := $(JNI_H_INCLUDE) LOCAL_MODULE := libHelloWorldJni LOCAL_SHARED_LIBRARIES := libutils LOCAL_PRELINK_MODULE := false LOCAL_MODULE_TAGS :=optional include $(BUILD_SHARED_LIBRARY)
看一下HelloWorld中Android.mk文件的配置
其中存在:
include $(LOCAL_PATH)/jni/Android.mk 表示编译库文件
LOCAL_JNI_SHARED_LIBRARIES := libHelloWorldJni 表示app依赖库,打包的时候会一起打包。
编译此模块:输入编译命令:mm packages/apps/HelloWorld/jni/
上面是我的工程根目录编译命令。具体编译方式根据自己系统要求执行。
编译输出: libHelloWorldJni.so (system/lib中视具体而定)
将编译好的apk安装到手机上
使用adb push到手机上去需要自己去导入库文件libHelloWorldJni.so到data/data/com.cktdemo/lib/
使用adb install方式安装则会自动导入。
启动HelloWorld :输入命令 adb logcat |grep xxj
输出log如下:
I/HelloWorld(28500): xxj Hello World From libhelloworld.so!
I/HelloWorld(28500): xxj --->I am HelloWorld Activity
V/xxj(28500): Hello World! I am Native interface
符合调用打印顺序正确,JNI调用完成。
Java_com_cktdemo_HelloWorld_printJNI函数,Java_开头,后面紧跟着调用他类名(包含包名和类名,com_cktdemo_HelloWorld),然后才是接口的名字printJNI。这样java虚拟机就可以在com.cktdemo.HelloWorld类调用printJNI接口的时候自动找到这个C实现的Native函数调用。这个名字非常的长,作为一个函数名它不是一个好的选择。JNI API允许你提供一个函数映射表,注册给Jave虚拟机,这样Java虚拟机就可以用函数映射表来调用相应的函数,就可以不必通过函数名来查找需要调用的函数了。这样你的函数名也可以随便定义了,这个将会在helloworld共享库的C++实现中演示.
JNI_OnLoad函数JNI规范定义的,当共享库第一次被加载的时候会被回调,这个函数里面可以进行一些初始化工作,比如注册函数映射表,缓存一些变量等,最后返回当前环境所支持的JNI环境。如果不实现JNI_OnLoad, printJNI只可以返回整型类型的值,如果返回其他类型的值会崩溃。
C++ HelloWorld lib 实现:
Android应用程序不需要有任何变化,我们重新用C++实现HelloWorld共享库.
com_cktdemo_Helloworld.cpp文件
#include#define LOG_TAG "HelloWorld" #include JNIEXPORT jstring JNICALL Java_com_cktdemo_HelloWorld_printJNI(JNIEnv *env, jobject obj,jstring inputStr) { LOGI("Hello World From libhelloworld.so!"); return env->NewStringUTF("Hello World! I am Native interface"); } static const char *classPathName = "com/cktdemo/HelloWorld"; static JNINativeMethod methods[] = { {"printJNI", "(Ljava/lang/String;)Ljava/lang/String;", Java_com_cktdemo_HelloWorld_printJNI }, }; static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* gMethods, int numMethods) { jclass clazz; clazz = env->FindClass(className); if (clazz == NULL) { LOGE("Native registration unable to find class '%s'", className); return JNI_FALSE; } if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { LOGE("RegisterNatives failed for '%s'", className); return JNI_FALSE; } return JNI_TRUE; } static int registerNatives(JNIEnv* env) { if (!registerNativeMethods(env, classPathName, methods, sizeof(methods) / sizeof(methods[0]))) { return JNI_FALSE; } return JNI_TRUE; } typedef union { JNIEnv* env; void* venv; } UnionJNIEnvToVoid; jint JNI_OnLoad(JavaVM* vm, void* reserved) { UnionJNIEnvToVoid uenv; JNIEnv* env = NULL; LOGI("JNI_OnLoad!"); if (vm->GetEnv((void**)&uenv.venv, JNI_VERSION_1_4) != JNI_OK) { LOGE("ERROR: GetEnv failed"); return -1; } env = uenv.env;; if (registerNatives(env) != JNI_TRUE) { LOGE("ERROR: registerNatives failed"); return -1; } return JNI_VERSION_1_4; }
1. C和C++实现共享库调用不同JNI API。Android系统JNI为C和C++提供了两套不同的API。对比NewStringUTF,GetEnv函数,就会发现JNI API不同。
2. C++版的helloworld共享库提供了函数映射表,标准JNI不能通过标准函数名找到C++实现的Helloworld共享库中的函数,但是C实现的helloworld共享就没有这个问题。
Java层原型方法:
... //Native层回调的方法实现 public void callback(String fromNative){ System.out.println(" I was call by native method == " + fromNative); }; public native void doCallBack(); //Native层会调用callback()方法 ... // main函数 public static void main(String[] args) { new HelloJni().doCallBack(); } }
Native层该方法实现为 :
... //Native层回调Java类方法 JNIEXPORT void JNICALL Java_com_cktdemo_jni_HelloJni_doCallBack(JNIEnv * env , jobject obj) { //回调Java中的方法 jclass cls = env->GetObjectClass(obj);//获得Java类实例 jmethodID callbackID = env->GetMethodID(cls,"callback", "(Ljava/lang/String;)V");//或得该回调方法句柄 if(callbackID == NULL) { cout << "getMethodId is failed \n" << endl; } jstring native_desc = env->NewStringUTF(" I am Native"); env->CallVoidMethod(obj,callbackID,native_desc);//回调该方法,并且传递参数值 }
在JNI中存在两种数据类型: 基本类型 和 引用类型
1、基本数据类型,如:int、 float 、char等基本类型
2、引用类型,如:类、实例、数组
基本数据类型映射参见下表:
引用数据类型映射参见下表
类描述符是类的完整名称(包名+类名),将原来的 . 分隔符换成 / 分隔符。
例如:在java代码中的java.lang.String类的类描述符就是java/lang/String
数组类型的描述符则为:[ + 其类型的域描述符
例如: int [ ] 其描述符为[I float [ ] 其描述符为[F String [ ] 其描述符为[Ljava/lang/String;
1、基本类型的描述符定义如下表所示:
2、引用类型的描述符
一般引用类型则为 L + 该类型类描述符 + ; (注意,这儿的分号“;”是JNI的一部分)
例如:String类型的域描述符为 Ljava/lang/String;
对于数组,其为 : [ + 其类型的域描述符 + ;
int[ ] 其描述符为[I float[ ] 其描述符为[F String[ ] 其描述符为[Ljava/lang/String; Object[ ] 类型的域描述符为[Ljava/lang/Object;
多维数组则是 n个[ +该类型的域描述符 , N代表的是几维数组。例如:
int [ ][ ] 其描述符为[[I float[ ][ ] 其描述符为[[F
将参数类型的域描述符按照声明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回类型描述符。 对于,没有返回值的,用V(表示void型)表示。举例如下:
Java层方法 JNI函数签名 String test ( ) (Ljava/lang/String;)V int f (int i, Object object) (ILjava/lang/Object;)I void set (byte[ ] bytes) ([B)V
在开发过程中,如果是利用javah工具的话,这些都不需要我们手动编写对应的类型转换,可以自动生成。如果不用javah工具,就只能手动进行类型转换。