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));}


2.java与C++的交互(test.cpp)

#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。所以用到如下代码:

  1. jclass jc=env->GetObjectClass(obj);
  2. jmethodID methodId=env->GetMethodID(jc,"HCMessage","(Ljava/lang/String;)V");
  3. jstring s=env->NewStringUTF("Hello Java From C++");
  4.  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位

0 0
原创粉丝点击