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 __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 <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;
#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>
从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_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的效果图: