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。

 

posted @ 2016-09-25 19:36  凌风天涯  阅读(119)  评论(0编辑  收藏  举报