jni开发初试

Posted JasonGaoH

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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。

以上是关于jni开发初试的主要内容,如果未能解决你的问题,请参考以下文章

NDK:JNI 的数据结构

初试ARM开发板

初试webpack4--开发使用技巧

腾讯校招前端开发笔试初试总结

Windows Phone开发:竖立自信,初试锋茫

初试 vue2.0——3.项目开发之布局说明