jni开发初试
简单jni流程初试
JNI java本地开发接口
JNI 是一个协议
这个协议用来沟通java代码和外部的本地代码(c/c++)。
通过这个协议,java代码就可以调用外部的c/c++ 代码,
外部的c/c++代码也可以调用java代码。
首先学习熟悉下简单的jni开发流程:
1.创建一个android工程
这个工程实现一个简单的功能:
使用jni这种技术打印一个由底层C语言返回的一个字符串2.JAVA代码中声明native 方法如下;
//第一步,在java代码中定义一个c方法的接口
//相当于在java代码中定义了一个接口,接口的实现是由c语言来实现的
public native String helloWorldFromC();
3. 创建jni目录,编写c代码,方法名字要对应
- 在jni目录下,新建一个hello.c文件
首先引入两个头文件
#include <stdio.h>
#include <jni.h>
- 在java中声明的方法是下面这样的
public native String helloWorldFromC();
- 实现该方法,方法名要对应
方法名:需要在方法名前面加上Java表示是java中的方法,另外,需要使用类的全名,将.替换为下划线
jstring Java_com_example_hellowordfromc_MainActivity_helloWorldFromC(JNIEnv * env,jobject obj)
{
}
这里的方法名采用的是手动写的方式,由于这种书写方式容易出错,另一种比较简单的方式就是以cmd模式进入到工程的src目录,使用javah + native方法的全类名,就可以自动生成头文件,这样native方法的c语言实现就不用手动写了,避免了不必要的错误。
+ 现在需要返回一个java String类型的字符串
注意:要想实现jni这类的程序,必须下载安装ndk,因为将需要使用该工具将c语言编译生成动态库
在ndk的ndk\platforms\android-8\arch-arm\usr\include目录下,有个jni.h文件
我们在前面引入了这个这个头文件后,可以使用里面声明的方法来实现我们想要的功能。
查看jni.h文件,可以找到==jstring (NewStringUTF)(JNIEnv, const char*);==
这个方法能够以UTF-8格式返回一个字符串
那么如何获得这个方法呢?
注意到方法中第一个参数JNIEnv * env,通过这个参数就可以拿到上面的方法了。
在jni.h中,有下面个声明:
//表示JNIEnv其实是JNINativeInterface这个结构体的指针类型
typedef const struct JNINativeInterface* JNIEnv;
在JNINativeInterface这个结构体中,有(NewStringUTF)(JNIEnv, const char*)这个方法,
当我们通过传过来的参数env就可以拿到JNINativeInterface,再通过JNINativeInterface去调用这个构造字符串的方法
/*
* Table of interface function pointers.
*/
struct JNINativeInterface {
....
· jstring (*NewStringUTF)(JNIEnv*, const char*);
....
}
代码的实现逻辑如下:
jstring Java_com_example_hellowordfromc_MainActivity_helloWorldFromC(JNIEnv * env,jobject obj) {
//(*env) 相当于JNINativeInterface* JNIEnv
//*(*env) 相当于JNINativeInterface
//return (**env).NewStringUTF(env,"helloworldfromc");
//另一种写法
return (*env)->NewStringUTF(env,"helloworldfromc");
}
4.编写Android.mk文件
在完成了C语言的实现后,需要编写Android.mk文件,使得ndk能够编译C语言生成函数库
在这个路径的文档介绍了如何编写Android.mk文件:ndk目录/android-ndk-r7b/docs/ANDROID-MK.html,可以看这个文档,写的很详细。
下面是最简单的mk文件的写法:
#将当前路径赋值给LOCAL_PATH这个变量
LOCAL_PATH := $(call my-dir)
#清空之前定义的变量
include $(CLEAR_VARS)
#对应的打包函数库的名字
LOCAL_MODULE := hello
#对应的C代码的文件
LOCAL_SRC_FILES := hello.c
#表示编译成动态库,在linux下的动态库一般后缀名为.so
include $(BUILD_SHARED_LIBRARY)
5.Ndk编译生成动态库
在完成上面的步骤后,就可以使用ndk将c程序打包成函数库了。
另外,编译之前需要配置下ndk的环境变量,这样就可以在任意目录使用ndk-build这个工具了。
进入到对应android工程下,以cmd模式输入ndk-build运行,这样就可以生成动态库了。6.Java代码load 动态库.调用native代码
在java代码中引入库函数,然后就可以直接调用native方法名就可以了。
//在java代码中引入库函数
static {
System.loadLibrary("hello");
}
这样就可以运行程序看效果了。
最后贴一下整个工程完整的程序:
MainActivity
package com.example.hellowordfromc;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends Activity {
//第一步,在java代码中定义一个c方法的接口
//相当于在java代码中定义了一个接口,接口的实现是由c语言来实现的
public native String helloWorldFromC();
//第五步,在java代码中引入库函数
static {
System.loadLibrary("hello");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View view) {
Toast.makeText(getApplicationContext(), helloWorldFromC(), 0).show();
}
}
hello.c文件
#include <stdio.h>
#include <jni.h>
// public native String helloWorldFromC();
//jstring 方法的返回值类型
//方法名:需要在方法名前面加上Java表示是java中的方法,另外,需要使用类的取名,将.替换为下划线
/**
//Reference types, in C.
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
*/
jstring Java_com_example_hellowordfromc_MainActivity_helloWorldFromC(JNIEnv * env,jobject obj) {
//第二步,生成c代码
//第三步,编写Android.mk文件
//第四步,使用ndk将c程序打包成函数库
//现在需要返回一个java String类型的字符串
//在ndk的ndk\platforms\android-8\arch-arm\usr\include目录下,有个jni.h文件
//在引入了jni.h后,我们需要利用里面声明的下面这个方法来构造一个字符串
//jstring (*NewStringUTF)(JNIEnv*, const char*);
//注意到这个方法jstring (*NewStringUTF)(JNIEnv*, const char*)是
//定义在JNINativeInterface这个结构体中的
//当我们通过传过来的参数env就可以拿到JNINativeInterface,再通过JNINativeInterface去调用这个构造字符串的方法
//(*env) 相当于JNINativeInterface* JNIEnv
//*(*env) 相当于JNINativeInterface
//return (**env).NewStringUTF(env,"helloworldfromc");
//另一种写法
return (*env)->NewStringUTF(env,"helloworldfromc");
}
java向c语言传递数据
接下来我们看java如何向c语言传递数据
首先创建一个类,主要用于声明java的native方法
现在主要实现以下三个方法:
第一个,使用jni技术实现两个数的相加
第二个,在字符串后面拼接字符串,应用场景为web url的拼接或者一些加密运算,由于java语言的特性,应用程序容易被反编译,对于有些敏感信息需要进行加密,使用c语言实现一般很难被反编译。
第三个,由java传递int数组,C语言对其进行操作后进行返回,这个应用的场景一般为图像的处理方面
package com.example.ndkpassdata;
public class DataProvider {
/**
* 计算x和y的加法
* 315
* @param x
* @param y
* @return
*/
public native int add(int x ,int y); // char String short kiss keep it simple and stupid String[] "123:234"
/**
* 给字符串后面拼装字符 加密运算 web url
* @param s
* @return
*/
public native String sayHelloInC(String s);
//
/**
* 给c代码传递int数组 让c代码给这个数组进行操作
* 图形 声音的处理
* @param iNum
* @return
*/
public native int[] intMethod(int[] iNum);
}
接着以windows命令行模式,进入到该android工程的src目录,
运行如下命令:javah com.example.ndkpassdata.DataProvider
这样生成如下的头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_ndkpassdata_DataProvider */
#ifndef _Included_com_example_ndkpassdata_DataProvider
#define _Included_com_example_ndkpassdata_DataProvider
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_ndkpassdata_DataProvider
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_example_ndkpassdata_DataProvider_add
(JNIEnv *, jobject, jint, jint);
/*
* Class: com_example_ndkpassdata_DataProvider
* Method: sayHelloInC
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_ndkpassdata_DataProvider_sayHelloInC
(JNIEnv *, jobject, jstring);
/*
* Class: com_example_ndkpassdata_DataProvider
* Method: intMethod
* Signature: ([I)[I
*/
JNIEXPORT jintArray JNICALL Java_com_example_ndkpassdata_DataProvider_intMethod
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
这样在工程目录创建jni目录,然后将生成的头文件剪切到jni目录下,新建hello.c文件,引入#include “com_example_ndkpassdata_DataProvider.h”头文件
#include <stdio.h>
#include "com_example_ndkpassdata_DataProvider.h"
//要实现在C语言中打印log,需要加入如下代码
//并且需要在Android.mk文件中引入log相应的函数库
#include <android/log.h>
#include <string.h>
#define LOG_TAG "clog"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
char* Jstring2CStr(JNIEnv* env, jstring jstr)
{
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env,"java/lang/String");
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
jmethodID mid = (*env)->GetMethodID(env,clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr= (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env,barr);
jbyte* ba = (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
if(alen > 0)
{
rtn = (char*)malloc(alen+1); //"\0"
memcpy(rtn,ba,alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env,barr,ba,0); //
return rtn;
}
//注意JNIEXPORT和JNICALL都是由javah自动生成的,可以删掉,不影响
JNIEXPORT jint JNICALL Java_com_example_ndkpassdata_DataProvider_add
(JNIEnv * env, jobject jobject, jint x, jint y){
// 想在logcat控制台上 打印日志
LOGD("x=%d",x);
LOGI("y=%d",y);
// log.i(TAG,"sss");
return x+y;
}
JNIEXPORT jstring JNICALL Java_com_example_ndkpassdata_DataProvider_sayHelloInC
(JNIEnv * env, jobject jobject, jstring str){
char* c="hello";
// 在C语言中不能直接操作java中的字符串
// 把java中的字符串转换成c语言中 char数组
char* cstr=Jstring2CStr(env,str);
strcat(cstr,c);
LOGD("%s",cstr);
return (*env)->NewStringUTF(env,cstr);
}
JNIEXPORT jintArray JNICALL Java_com_example_ndkpassdata_DataProvider_intMethod
(JNIEnv * env, jobject jobject, jintArray jarray){
// jArray 遍历数组 jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
// 数组的长度 jsize (*GetArrayLength)(JNIEnv*, jarray);
// 对数组中每个元素 +5
int length=(*env)->GetArrayLength(env,jarray);
int* array=(*env)->GetIntArrayElements(env,jarray,0);
int i=0;
for(;i<length;i++){
*(array+i)+=5;
}
return jarray;
}
最后在android工程中loadLibrary,就可以使用这些方法了。
c语言回调java方法
==接下来我们看如何在C语言回调java方法==
这种应用场景相对比较少,另外这种实现的方式也比较复杂点。
同样,前面的步骤都是一样的,声明native方法。首先在创建一个DataProvider,我们在这里面声明native方法。
这里不同的是,要想实现c语言回调java方法,需要通过声明native方法,通过实现该native方法,然后再回调会java代码中
package com.example.ndkcallback;
public class DataProvider {
//C调用java空方法
public void helloFromJava(){
System.out.println("哈哈哈 我被调用了");
}
//C调用java中的带两个int参数的方法
public int Add(int x,int y){
int result=x+y;
System.out.println("result:"+result);
return result;
}
//C调用java中参数为string的方法
public void printString(String s){
System.out.println(s);
}
public static void demo(){
System.out.println("哈哈哈,我是静态方法");
}
public native void callMethod1();
public native void callMethod2();
public native void callMethod3();
public native void callMethod4();
public native void callMethod5();
}
这里回调的机制类似于java的反射机制,通过jni.h中声明的FindClass方法获得java类的字节码,然后获得方法的ID,将字节码参数,方法名,以及方法签名传到CallVoidMethod这样的方法中,就可以实现在c语言中回调java方法。
注意,可以使用javap来获得对应类内部方法的签名,签名一般由方法的参数和赶回值组成。
javap -s 打印方法的签名 注意要cd到 C:\workspace\HelloWorldFromC\bin\classes 传全类名
#include "com_example_ndkcallback_DataProvider.h"
JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod1
(JNIEnv * env, jobject jobject){
/*
*
Class<?> forName = Class.forName("com.example.ndkcallback.DataProvider");
Method declaredMethod = forName.getDeclaredMethod("helloFromJava", new Class[]{});
declaredMethod.invoke(forName.newInstance(), new Object[]{});
*
*
*/
///jclass (*FindClass)(JNIEnv*, const char*);
jclass clazz=(*env)->FindClass(env,"com/example/ndkcallback/DataProvider");
// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
// 方法签名 参数和返回值
jmethodID methodId=(*env)->GetMethodID(env,clazz,"helloFromJava","()V");
// void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
(*env)->CallVoidMethod(env,jobject,methodId);
}
JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod2
(JNIEnv * env, jobject jobject){
jclass clazz=(*env)->FindClass(env,"com/example/ndkcallback/DataProvider");
jmethodID methodId=(*env)->GetMethodID(env,clazz,"Add","(II)I");
// jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
(*env)->CallIntMethod(env,jobject,methodId,3,5);
}
JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod3
(JNIEnv * env, jobject jobject){ // 参数 object 就是native方法所在的类
jclass clazz=(*env)->FindClass(env,"com/example/ndkcallback/DataProvider");
jmethodID methodId=(*env)->GetMethodID(env,clazz,"printString","(Ljava/lang/String;)V");
// jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
jstring str=(*env)->NewStringUTF(env,"hello");
(*env)->CallVoidMethod(env,jobject,methodId,str);
}
JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod4
(JNIEnv * env, jobject j){
jclass clazz=(*env)->FindClass(env,"com/example/ndkcallback/MainActivity");
// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
// 方法签名 参数和返回值
jmethodID methodId=(*env)->GetMethodID(env,clazz,"helloFromJava","()V");
// void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
// 需要创建DataProvider的 对象
// jobject (*AllocObject)(JNIEnv*, jclass);
jobject obj=(*env)->AllocObject(env,clazz); // new MainActivity();
(*env)->CallVoidMethod(env,obj,methodId);
}
JNIEXPORT void JNICALL Java_com_example_ndkcallback_DataProvider_callMethod5
(JNIEnv * env, jobject j){
jclass clazz=(*env)->FindClass(env,"com/example/ndkcallback/DataProvider");
// jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID methodid=(*env)->GetStaticMethodID(env,clazz,"demo","()V");
//void (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);
(*env)->CallStaticVoidMethod(env,clazz,methodid);
}
那么这个JNIEnv是干什么用的?
其实从这个参数的名称就可以看到,就是指JNI的运行环境,我觉得它就是对Java虚拟环境的一个引用,在Android中,就是指Dalvik VM。