Android 平台下Java与C/C++的相互调用

    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 
/* Header for class com_example_jnidemo_DemoAPI */

#ifndef _Included_com_example_jnidemo_DemoAPI
#define _Included_com_example_jnidemo_DemoAPI
#ifdef __cplusplus
extern "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 
#include 
#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 
#include "com_example_jnidemo_DemoAPI.h"
#include 
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;
#else
typedef 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文件中可以看出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平台下可以通过命令行进行查询:

Android 平台下Java与C/C++的相互调用_第1张图片


首先要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目录:

Android 平台下Java与C/C++的相互调用_第2张图片

    

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_LIBRARY

include $(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的效果图:


Android 平台下Java与C/C++的相互调用_第3张图片



JNIDemo代码               ndk r12b 64位

你可能感兴趣的:(安卓JNI)