Android 平台下Java与C/C++的相互调用
来源:互联网 发布:淘宝网,方太燃气灶配件 编辑:程序博客网 时间:2024/05/16 10:33
Android主要使用的是Java语言进行编程的,应用层以及Framework使用的都是Java。对于java语言优势嘛,主要就是语法简单,跨平台。当然劣势也是非常的明显,执行效率和速度相比于C/C++来说,比较的低下。举个例子来说,使用Java处理图片的颜色的变化和使用c/c++处理图片颜色的变化,后者的处理速度是前者的10倍。在Java中要想使用c/c++代码就必须要借用JNI这玩意儿了!JNI全称Java Native Interface 。同时JNI也是通往Android高手路上必须跨越的东西。现在,智能家居等和硬件相关的东西越来越火,掌握JNI是很有必要的。此外特别是一些消耗性能的东西,一般都是底层C/C++做的,在项目中最直接的体现就是你libs目录里面那些.so文件(动态库)。
一、写JNI的第一步是在上层(java层)写好相应的native方法。然后通话javah.exe生成对应的头文件(.h)ps:当然要写jni你还必须掌握必要的C以及C++的知识,因为jni实际上就是java和C/C++的沟通桥梁。一般公司都有专门写c/c++的程序员,但是人家写好了c/c++层,你至少要知道怎么调用吧。 下面先上上层native方法以及对应生成的头文件(com_example_jnidemo_DemoAPI.h)。
public native String getStringFromC(); //从c层返回的字符串 public native String getStringFromHC(); //从C++层返回的字符串 public native void callC(); //让c层代码调用java层代码 public native void callHC(); //让c++层代码调用java层的代码 public void CMessage(int a){ Toast.makeText(mContext,"c层回调java层 CMessage方法\n",Toast.LENGTH_SHORT).show(); } public void HCMessage(String message){ Toast.makeText(mContext,"c++层回调java层HCMessage方法\n"+message,Toast.LENGTH_SHORT).show(); }
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_jnidemo_DemoAPI */#ifndef _Included_com_example_jnidemo_DemoAPI#define _Included_com_example_jnidemo_DemoAPI#ifdef __cplusplusextern "C" {#endif/* * Class: com_example_jnidemo_DemoAPI * Method: getStringFromC * Signature: ()Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_com_example_jnidemo_DemoAPI_getStringFromC (JNIEnv *, jobject);/* * Class: com_example_jnidemo_DemoAPI * Method: getStringFromHC * Signature: ()Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_com_example_jnidemo_DemoAPI_getStringFromHC (JNIEnv *, jobject);/* * Class: com_example_jnidemo_DemoAPI * Method: callC * Signature: ()V */JNIEXPORT void JNICALL Java_com_example_jnidemo_DemoAPI_callC (JNIEnv *, jobject);/* * Class: com_example_jnidemo_DemoAPI * Method: callHC * Signature: ()V */JNIEXPORT void JNICALL Java_com_example_jnidemo_DemoAPI_callHC (JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif
头文件(自动生成的不要改动里面的任何东西)里面对应的四个方法,就是上层native对应生成的!里面的方法都是以JNIEXPORT +返回类型 + JNICALL + native方法的全名(全类名+方法名)+参数列表 ;由头文件中的native方法的全类名,可以知道这些native方法都在一个叫DemoAPI的类中。
1.java与c的交互(Hello.c):
#include <jni.h>#include <stdlib.h>#include "com_example_jnidemo_DemoAPI.h"JNIEXPORT jstring JNICALL Java_com_example_jnidemo_DemoAPI_getStringFromC(JNIEnv *env, jobject obj){ char * s="from C Hello Java"; return (*env)->NewStringUTF(env,s);}//c层回调java上层JNIEXPORT void JNICALL Java_com_example_jnidemo_DemoAPI_callC(JNIEnv *env, jobject obj){ jclass jc=(*env)->GetObjectClass(env,obj);jmethodID methodId=(*env)->GetMethodID(env,jc,"CMessage","(I)V");(*env)->CallVoidMethod(env,obj,methodId,((int)3));}
#include <jni.h>#include "com_example_jnidemo_DemoAPI.h"#include <stdlib.h>JNIEXPORT jstring JNICALL Java_com_example_jnidemo_DemoAPI_getStringFromHC(JNIEnv * env, jobject obj){ char *s="from c++ Hello Java!"; return env->NewStringUTF(s);}//c++层回调java上层JNIEXPORT void JNICALL Java_com_example_jnidemo_DemoAPI_callHC(JNIEnv * env, jobject obj){ jclass jc=env->GetObjectClass(obj); jmethodID methodId=env->GetMethodID(jc,"HCMessage","(Ljava/lang/String;)V"); jstring s=env->NewStringUTF("Hello Java From C++"); env->CallVoidMethod(obj,methodId,s);}
由上面的jni层的程序代码,我们可以看出,在.c和.cpp文件中调用的方法(如调用的java层的方法名称是相同的只是传递的参数是不一样的)。JNIEnv * env这个 JNIEnv可以理解为一种环境,是java和底层沟通的一种环境。jobject obj ,这个jobject是随着你写的native方法的位置的不同而改变的,jobject是java上层native所在类在jni层对应的变量,在本例中其实就是DemoAPI类在jni层对应的对象。
在c层中一般适用(*env)->调用API,而在c++层中则直接适用env->调用API这是为什么了? 原因就在于:
#ifdef __cplusplus/* * Reference types, in C++ */
#if defined(__cplusplus)typedef _JNIEnv JNIEnv;typedef _JavaVM JavaVM;#elsetypedef const struct JNINativeInterface* JNIEnv;typedef const struct JNIInvokeInterface* JavaVM;#endif
struct _JNIEnv { /* do not rename this; it does not seem to be entirely opaque */ const struct JNINativeInterface* functions;#if defined(__cplusplus) jint GetVersion() { return functions->GetVersion(this); } jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen) { return functions->DefineClass(this, name, loader, buf, bufLen); } jclass FindClass(const char* name) { return functions->FindClass(this, name); } jmethodID FromReflectedMethod(jobject method) { return functions->FromReflectedMethod(this, method); }使用jni不论是在.c还是在.cpp文件首先必须要#include <jni.h> 从jni.h文件中可以看出C中JNIEnv是 JNINativeInterface* (指针)。而C++中的JNIEnv是_JNIEnv 而_JNIEnv实质是一个结构体env->其实就是在调用_JNIEnv里面的方法,而这些方法的实现又是通过JNINativeInterface * functions实现的,本质和C是一样的。而JNINativeInterface的实质也是一个结构体。
struct JNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void* reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize); jclass (*FindClass)(JNIEnv*, const char*); jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); jfieldID (*FromReflectedField)(JNIEnv*, jobject); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean);
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_DemoAPI_getStringFromHC(JNIEnv * env, jobject obj) 如果是c++,(PS:env->与*env是等价的),env->就是直接访问_JNIEnv结构体中的方法。而在C层中*env 拿到只是JNINativeInterface*指针(*env)->就是直接访问JNINativeInterface中的方法。从上面的源码中我们也可以看出,无论是c还是c++本质是通过调用JNINativeInterface结构体里面的方法进行完成相应的功能。
扫除了这些基本的概念后,我们来看看具体的方法:
NewStringUTF这个方法是创建一个在底层经过Utf-8编码的jstring 对于c++只需要传入Char * 一个参数即可,而对于c则还需要传入env 。(备注:这里返回到上层的是英文,中文会乱码,报错,解决方法可以返回jcharArray,然后再上层进行转码)。
对于底层回调上层,首先需要拿到上层方法,即获得方法的Id值,而要获取方法Id就又必须获取上层类对应的jclass。所以用到如下代码:
- jclass jc=env->GetObjectClass(obj);
- jmethodID methodId=env->GetMethodID(jc,"HCMessage","(Ljava/lang/String;)V");
- jstring s=env->NewStringUTF("Hello Java From C++");
- env->CallVoidMethod(obj,methodId,s);
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
env->GetMethodID获取方法ID,这里传入3个参数,第一个jclass,第二个上层(java层)那个方法的名称,第三个是信号名。例如:(Ljava/lang/String;)V ,看着就头大是不是,这玩意儿谁记得住啊!,好在这是有方法可查的:在Windows平台下可以通过命令行进行查询:
首先要cd 命令切入到native方法所在的.java文件的目录下面,然后通过javac命令对该java文件进行编译(比如:我这里对DemoAPI.java进行编译 javac DemoAPI.java);
编译完成之后使用 Javap -s +.java文件名 获得我们想要的。(例如 :Javap -s DemoAPI),HCMessage方法下面的descriptor就是我们需要的。但是进行javac 和javap命令有时是会失败的,最大的原因莫过于,在该java文件中使用了android sdk里面的内容,而这些内容JDK是没有的,所以是会报错的。最后调用CallVoidMethod方法,通过API的名字我们就知道它是下层回到上层的方法。注意:还有这种 env->CallStaticVoidMethod() 这是下层回调上层静态方法的。
二、特殊类型的传递以及处理:
jni中最复杂的莫过于上层直接传递java对象到下层,例如:
public class Data {public ByteBuffer data;public String name;public int age;public Data(){//内存中开辟长度为640字节的ByteBuffer数组,该内存不收GC管制data=ByteBuffer.allocateDirect(640);} @Override public String toString() {return name+" "+age; }}
public native void chageValue(Data data); //底层C++,改变上层Data对象的值头文件里面的信息:
/* * Class: com_example_jnidemo_DemoAPI * Method: chageValue * Signature: (Lcom/example/jnidemo/Data;)V */JNIEXPORT void JNICALL Java_com_example_jnidemo_DemoAPI_chageValue(JNIEnv *, jobject, jobject);cpp里面的实现方法:
JNIEXPORT void JNICALL Java_com_example_jnidemo_DemoAPI_chageValue(JNIEnv *env, jobject obj, jobject data){jclass jc=env->GetObjectClass(data);//对于ByteBuffer通过allocateDirect 创建的,底层主要是获取其地址,然后进行操作。jfieldID b=env->GetFieldID(jc,"data","Ljava/nio/ByteBuffer;");jobject b_f=env->GetObjectField(data,b);void * buff=env->GetDirectBufferAddress(b_f);jstring s=env->NewStringUTF("Alice");jfieldID name=env->GetFieldID(jc,"name","Ljava/lang/String;");env->SetObjectField(data,name,s);//给年龄进行赋值jfieldID age=env->GetFieldID(jc,"age","I");env->SetIntField(data,age,30);}这里的事例是,底层改变上层Data对象里面的成员变量 name和age的值。要想到达这样的目的:首先必须拿到上层的成员变量对应的jfieldID 拿到这个之后,你可对这个jfieldID对应的上层的成员变量进行赋值或者拿到这个成员变量的值。这里主要是进行赋值操作。对于取值操作(比如拿Int类型的成员变量值 env->GetIntField())。首先来看看
GetFieldID这个方法的源码:
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig){ return functions->GetFieldID(this, clazz, name, sig); }这个方法第一个参数是jclass,第二个是成员变量的名字,第三个是信号名称,第三个值的获取方法,用上面的命令行方法可以获取。(备注:因为要用上面成员变量的名字,所以Data类中的成员变量名不能随意改变,更不能被混淆)。同时allocateDirect出来的ByteBuffer是操作其指针的。
三、mk文件。
mk文件是android平台下面的脚本文件,我们写好的jni最终不可能以源码的形式交付出去,一般打包成.so(动态库)或者.a(静态库)文件。其中又以动态库的形式居多。如图jni目录:
jni工程中可以有多个mk文件但是,名称叫Android和Application的整个工程却只有一个。
1.首先来讲讲application文件:
APP_ABI := armeabi 一般appliction.mk 文件中都会有这句,这句是说生成的库(一般是动态库需要哪几个平台,ndk是可以交叉编译的)常见的三大平台 intel、armeabi、mips(很小众基本可以忽略)。主要还是armeabi平台 。又分为:armeabi 、armeabi-v7a、arm64-v8a三个v7a是armeabi的升级版,v8a是是64位架构的。如果这些平台你都想生成等于的后面填all即可。
2.android.mk文件:
这个文件是控制生成的类库的名字,以及所用的底层代码文件,或者依赖的库:
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := Hello #生成库的名字LOCAL_SRC_FILES := test.cpp hello.c #所用到的代码源文件include $(BUILD_SHARED_LIBRARY) #标志生成动态库(.so文件)
上面这个是个比较简单的android.mk 文件,下里来一个稍微复杂点的:
LOCAL_PATH := $(call my-dir)LOCAL_STREAMER :=streamer #声明变量LOCAL_SERVICE :=service #声明变量include $(CLEAR_VARS)LOCAL_MODULE := first #预加载之后,静态库的别名LOCAL_SRC_FILES := $(LOCAL_STREAMER)/we.a #依赖的静态库include $(PREBUILT_STATIC_LIBRARY) #标志预加载静态库,预加载动态库的标志是:PREBUILT_SHARED_LIBRARYinclude $(CLEAR_VARS)LOCAL_C_INCLUDES := $(LOCAL_STREAMER) $(LOCAL_SERVICE) #所用到的头文件LOCAL_MODULE := service #生成动态库名称LOCAL_SRC_FILES := $(LOCAL_SERVICE)/Service.cpp #所编写用到的cpp文件LOCAL_LDLIBS :=-llog #cpp文件中有日志时,需要添加这个LOCAL_STATIC_LIBRARIES += first #添加依赖的静态库别名include $(BUILD_SHARED_LIBRARY) #标志生成动态库当然mk文件里面的内容远远不止这些,我在这里只是起抛砖引玉的作用,读者仍需多多的研究探索!
四、备注
该Demo的效果图:
JNIDemo代码 ndk r12b 64位
- Android 平台下Java与C/C++的相互调用
- java与C函数相互调用整理
- Android java层和C层的相互调用
- Android 中Java 和C/C++的相互调用方法
- Android 中Java 和C/C++的相互调用方法
- Android 中Java 和C/C++的相互调用方法
- Android 中Java 和C/C++的相互调用方法
- Android 中Java 和C/C++的相互调用方法
- C语言与汇编语言的相互调用
- C 与 Fortran 的相互调用
- C++与C的相互调用
- C与C++的相互调用问题
- C与C++函数的相互调用
- 汇编与c的相互调用
- C/C++与Lua的相互调用
- Python与C之间的相互调用
- C与C++的相互调用问题
- C与C++的相互调用
- swift学习笔记--计算属性和存储属性的区别
- sublime text2使用Package Control组件安装
- 十万个为什么 —— 名词解释(历史)
- Android下拉刷新上拉加载控件,对所有View通用! 非常好用,暂未发现bug
- 深入理解ART虚拟机—ImageSpace的加载过程分析
- Android 平台下Java与C/C++的相互调用
- Spring MVC
- Java Http接口加签、验签操作
- restrict关键字用法
- 编写Android.mk中的LOCAL_SRC_FILES的终极技巧
- SystemProperties.get(String key,String def)获取系统属性
- Python类方法如何调用?
- epoll使用详解(精髓)
- 信号signal()、alarm()、信号集函数、sigprocmask()