Android开发艺术探索:JNI和NDK编程

NDK是Android所提供的工具集合,通过NDK可以方便地通过JNI来访问本地代码,还提供交叉编译器,开发人员只需要简单的修改mk文件就可以生成特定CPU平台的动态库。

NDK好处:

JNI开发流程(Java工程)

1.在Java中声明native方法

package com.wujingchao.hellojni;


public class HelloJni {

    static {
            System.loadLibrary("hello-jni"); 
    }

    public native static String sendAndReceive(String msg) ;

        public static void main(String [] args) {
            String result  = sendAndReceive("Hello");
            System.out.println(result);
        }
}

so库的名称为libhello-jni.so,这是加载so库的规范。

2.编译Java源文件得到class文件,javah命令导出JNI的头文件

编译class文件:

javac com/wujingchao/hellojni/HelloJni.java

结果:

└── com
    └── wujingchao
        └── hellojni
            ├── HelloJni.class
            └── HelloJni.java

导出Jni头文件:

javah com.wujingchao.hellojni.HelloJni

结果:

├── com
│   └── wujingchao
│       └── hellojni
│           ├── HelloJni.class
│           └── HelloJni.java
└── com_wujingchao_hellojni_HelloJni.h

生成的.h文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_wujingchao_hellojni_HelloJni */

#ifndef _Included_com_wujingchao_hellojni_HelloJni
#define _Included_com_wujingchao_hellojni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_wujingchao_hellojni_HelloJni
 * Method:    sendAndReceive
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_wujingchao_hellojni_HelloJni_sendAndReceive
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

函数名规则:Java包名类名_方法名
JNIEnv*:表示一个指向JNI环境的指针,可以通过它来访问JNI提供的接口方法
jobject:表示Java对象本身即this
JNIEXPORT和JNICALL:JNI定义的宏,可以在java.h这个头文件里面找到

extern “C” 采用C语言的命名风格来编译。

3.实现JNI方法

实现JNI方法可以选择C++或者C来实现。

在工程目录下新建文件夹jni(名字不固定),将生成的头文件放到jni目录下面,将实现的.c或者.cpp文件放到jni目录下。

HelloJni.c:

#include "com_wujingchao_hellojni_HelloJni.h"
#include <stdio.h>

JNIEXPORT jstring JNICALL Java_com_wujingchao_hellojni_HelloJni_sendAndReceive
  (JNIEnv *env, jclass thiz, jstring str) {
    char* message = (char*)(*env)->GetStringUTFChars(env,str,NULL);
    printf("receive message = %s\n",message);
    return (*env)->NewStringUTF(env,"Bye!");
}

或者HelloJni.cpp:

#include "com_wujingchao_hellojni_HelloJni.h"
#include <stdio.h>

JNIEXPORT jstring JNICALL Java_com_wujingchao_hellojni_HelloJni_sendAndReceive
  (JNIEnv *env, jclass thiz, jstring str) {
    char* message = (char*)env->GetStringUTFChars(str,NULL);
    printf("receive message = %s\n",message);
    return  env->NewStringUTF("Bye!");
}

主要区别集中在对env的操作上。

4.编译so库在Java中调用

gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC HelloJni.c -o libhello-jni.so

或者

gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC HelloJni.cpp -o libhello-jni.so

制定so库的路径执行Java程序,-Djava.library.path为so库的路径:

java -Djava.library.path=../jni com.wujingchao.hellojni.HelloJni

C和C++的调用结果是一样的:

wujingchao@N4050:~/WorkSpace/Java/HelloJNI/src$ java -Djava.library.path=../jni com.wujingchao.hellojni.HelloJni
receive message = Hello
Bye!

NDK开发流程(Android Studio)

在HelloJni/local.properties里声明ndk的路径:

ndk.dir=/home/wujingchao/Android/android-ndk-r10e
sdk.dir=/home/wujingchao/Android/Sdk

定义本地方法,编译之后生成HelloJni/app/build/intermediates/classes/debug/com/wujingchao/android/hellojni/NDKUtils.class:

package com.wujingchao.android.hellojni;

public class NDKUtils {

    static {
        System.loadLibrary("hello-jni");
    }

    public native static String sendAndReceive(String message);
}

然后在debug目录下导出头文件:

javah com.wujingchao.android.hellojni.NDKUtils

├── com
│   └── wujingchao
│       └── android
│           └── hellojni
│               ├── BuildConfig.class
│               ├── MainActivity$1.class
│               ├── MainActivity.class
│               ├── NDKUtils.class
|                /....
└── com_wujingchao_android_hellojni_NDKUtils.h

将其复制到app/src/main/jni,然后新建.c或者.cpp实现方法:

#include "com_wujingchao_android_hellojni_NDKUtils.h"
#include <stdio.h>

JNIEXPORT jstring JNICALL Java_com_wujingchao_android_hellojni_NDKUtils_sendAndReceive
        (JNIEnv *env, jclass clazz, jstring s){
    const char* str = (*env)->GetStringUTFChars(env,s,NULL);
    printf("%s",str);
    return (*env)->NewStringUTF(env,"Hello,Bye");
}

然后在build.gradle声明生成so:

defaultConfig {
    applicationId "com.wujingchao.android.hellojni"
    minSdkVersion 9
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"

     ndk {
        moduleName "hello-jni" //和System.loadLibrary("hello-jni");里面加载的一致
        abiFilters "armeabi", "armeabi-v7a", "x86" //可以不写,那么就是全平台
     }
}

JNI的数据类型和类型签名

JNI的数据类型基本数据类型和引用数据类型。

基本数据类型:

JNI类型 Java类型 描述
jboolean boolean ######
jbyte byte
jint int
..
void void ..

引用数据类型:

JNI类型 Java类型 描述
jobject Object
jclass Class
jobjectArray Object[]
jstring String
jintArray int[]
jthrowable Throwable
..

JNI的类型签名标识了Java类型,可以是类,方法,数据类型。

类的签名采用 “L + 包名 + 类名 +;”,例如java.lang.String,签名为 Ljava/lang/String;

基本数据类型采用大写字母表示:

JNI类型 签名
boolean Z
byte B
char C
short S
int I
Long J
float F
double D
void V

对象的签名就是所属类名的签名,数组签名为 [ + 类型签名,比如:
int [] : [I
float[] : [F
int[][] : [[I

方法的签名为 (参数类型签名) + 返回值类型签名,例如:
boolean fun1(int a,double b,int[] c) : (ID[C)Z
boolean fun2(int a,String b,int[] c) : (ILjava/lang/String;[I)Z
void fun3(int i) : (I)V

JNI调用Java方法的流程

JNI调用Java方法的流程是先通过类名找到类,然后在根据方法名找到方法的id。非构造方法需要构造类的对象才能调用,或者使用已有的对象。

调用静态方法:

jclass clazz = (*env)->FindClass("com/wujingchao/android/NDKUtils");
if(clazz == NULL){
    //...
    return;
}

jmethodId id = (*env)->GetStaticMethodID(clazz,"testStaticJni","()V");//第三个参数为方法签名
if(id == NULL) {
    //...
    return;
}

(*env)->CallStaticVoidMethod(clazz,id,NULL);//第三个参数为方法参数

调用对象方法:

    jclass clazz = (*env)->FindClass(env, "com/wujingchao/android/NDKUtils");  
    if (clazz == NULL) {  
        //...
        return;  
    }  

    jmethodId construct = (*env)->GetMethodID(env,clazz, "<init>","()V");  
    if (mid_construct == NULL) {  
        //...
        return;  
    }  

    jmethodId testJniMethod = (*env)->GetMethodID(env, clazz, "testJni", "(Ljava/lang/String;I)V");  
    if (testJniMethod == NULL) {  
        //...
        return;  
    }  

    jobject jobj = (*env)->NewObject(env,clazz,mid_construct);  
    if (jobj == NULL) {  
        //...
        return;  
    }  
    jstring arg = (*env)->NewStringUTF(env,"Hello,Bye");

    (*env)->CallVoidMethod(env,jobj,testJniMethod,arg);