What is JNI

JNI是Java Native Interface的缩写,主要是提供了一系列API,让你能在其它语言中写Java。

What JNI can bring us

JNI最大的好处就是,额,Java你懂的,跑在JVM里面,虽然有着一处编译,到处运行的优势(,方便啊),但是它的效率。。。至少相对于c和C艹来说,比较低下,而且,正是由于这个能一处编译,到处运行的原因,Java极容易被反编译。Java中一般用的加密方式就是混淆了,然而其实并没有太大的作用。你还是开源吧。。。因为不开源也会被反编译的。。。
PS:并没有贬低Java的意思,个人还是挺喜欢用Java的

然后,相反的,JNI由于是用C或者C艹写,效率很高,可以用来处理一些底层的东西,比如解码或者TCP/IP有关的。编译过后跟C(艹)编译的结果是一样的,在Android里面是.so文件。然后,因为是C(艹),所以需要针对不同的平台,不同的处理器进行编译。所以,使用JNI,你需要在编译的时候生成许多个平台的版本,否则,Java跨平台这个优点相当于直接被废了。还有就是JNI的调试会非常蛋疼。

How to use JNI

Hello World

我用的Android Studio,有各种一键生成(x),要看手撸的话,网上应该能搜到,本文主要是介绍那些遇到的坑。

AS生成的main.cpp长这样:

#include <jni.h>
#include <string>

extern "C"
jstring
Java_com_helloworld_jnidemo_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

分析一下:

  • 几个include,其中jni.h是JNI必需的,其他的可以添加C(艹)中的,比如stdio.h什么的
  • extern C,这个我也不是特别理解,自我修养里面说是声明为C语言,然而删掉过后就炸了
  • jstring,返回值类型
  • Java_com_helloworld_jnidemo_MainActivity_stringFromJNI,Java_包名_类名_方法名,这是函数声明的规范
  • JNIEnv *env, jobject /* this */,JNIEnv里面有巨量的函数,后面就知道了,jobject就是this
  • std::string hello = "Hello from C++";,C艹
  • env->NewStringUTF(hello.c_str()),这儿就出现了env的其中一个函数,这个函数会经常在后面用到,char*转String,没错,他们不一样!

然后我自己写了一个HelloWordl和求和的函数:

extern "C"
jstring
Java_com_helloworld_jnidemo_MainActivity_helloworld(JNIEnv *env, jobject /* this */) {
    return env->NewStringUTF("Hello World");
}

extern "C"
jint
Java_com_helloworld_jnidemo_MainActivity_sum(JNIEnv *env, jobject /* this */, jint a, jint b) {
    return a + b;
}

Java中该这样写:

static {
    System.loadLibrary("native-lib");
}

public native String stringFromJNI();

public native String helloworld();

其中,System.loadLibrary("native-lib");这句是加载库,static语句块中的内容只会被执行一次。native-lib为库的名称,声明方法时使用native关键字。

CMakeLists.txt:

add_library( 
            native-lib
            SHARED
            src/main/cpp/native-lib.cpp )
find_library( 
            log-lib
            log )
target_link_libraries(
                    native-lib
                    ${log-lib} )

其中,native-lib可以随便改,对应System.loadLibrary("native-lib");里面的。但是有个玄学问题,不能改成test。。。被坑了。。。
src/main/cpp/native-lib.cpp里面的文件名可以随便改,只要与你写的文件对应。

好的,JNI入门了的样子。

Learn More

写出来了Hello World,该继续深入研究了。在继续之前,我们还应该了解一下jstringjint这些是啥,这儿有个表,展示了JNI和Java里面的属性的关系:

  • jint –> int
  • jbyte –> byte
  • jshort –> short
  • jlong –> long
  • jfloat –> float
  • jdouble –> double
  • jchar –> char
  • jboolean –> boolean
  • jclass –> java.lang.Class
  • jstring –> java.lang.String
  • jarray –> Array
  • jxxxArray –> xxx[]
  • jobject –> Object

注意最后一个,一切皆为对象。

使用JNI,你应该实现Java的基本功能:

  • new对象
  • call方法
  • 获取属性

学会了以上三个操作,就可以用JNI代替Java中70%以上的操作了。让我们一个一个来看。

new对象 & Call方法

没错,new对象就是通过调用构造方法实现的。

extern "C"
jobject
Java_com_helloworld_asdf_MainActivity_newObject(JNIEnv *env, jobject /* this */) {
    jclass clazz = env->FindClass("java/lang/Object");
    jmethodID init = env->GetMethodID(clazz, "<init>", "()V");
    jobject result = env->NewObject(clazz, init);
    return result;
}

步骤:

  • 找到class,用/代替.,FindClass的参数为所在包名
  • 找到对应构造方法
  • 调用newObject,传入class和构造方法id。

再看看一般的方法调用:

extern "C"
jint
Java_com_helloworld_asdf_MainActivity_stringLen(JNIEnv *env, jobject /* this */, jstring str) {
    jclass clazz = env->GetObjectClass(str);
    jmethodID lenId = env->GetMethodID(clazz, "length", "()I");
    jint result = env->CallIntMethod(str, lenId);
    return result;
}

GetObjectClass可以直接从object中拿到class。

调用方法用CallxxxMethod,xxx为返回值类型。CallxxxMethod的第一个参数是jobject,不是jclass,这个与NewObject不同。前面有jxxxArray,然而并没有CallxxxArrayMethod哎,该怎么办呢?一切都是对象,用CallObjectMethod再强转就可以了。
比如:

extern "C"
jstring
Java_com_helloworld_asdf_MainActivity_toString(JNIEnv *env, jobject /* this */, jobject object) {
    jclass clazz = env->GetObjectClass(object);
    jmethodID lenId = env->GetMethodID(clazz, "toString", "()Ljava/lang/String;");
    jstring result = (jstring) env->CallObjectMethod(object, lenId);
    return result;
}

方法签名:
简直有毒,反人类

  • construction –>
  • void –> V
  • boolean –> Z
  • byte –> B
  • char –> C
  • short –> S
  • int –> I
  • long –> J
  • float –> F
  • double –> D
  • x[] –> [x
  • x[][] –> [[x
  • java.lang.String –> L/java/lang/String;

总结一下:

  • 每个基本类型都有对应的签名,基本法
  • 数组用[
  • 构造方法规定为
  • 其它类为L类;,注意:分号不能丢,分号不能丢,分号不能丢

获取Field

extern "C"
jint
Java_com_helloworld_asdf_MainActivity_getX(JNIEnv *env, jobject /* this */, jobject test) {
    jclass clazz = env->GetObjectClass(test);
    jfieldID lenId = env->GetFieldID(clazz, "x", "I");
    jint result = env->GetIntField(test, lenId);
    return result;
}

static

static的属性和方法与普通的有一些区别,例如CallStaticObjectMethod的第一个参数是jclass。这些在熟悉了上面的操作过后都没有太大的问题了。

多维数组的处理

  • 思路:一维一维处理
  • env->GetArrayLength()拿到第一维数组长度
  • for循环中用env->GetByteArrayRegion()将下一维的元素取出
  • 如果是多维,重复操作

分享一点经验

  • 一切都是object
  • Java里的String和C(艹)里的是不一样的,要记得NewStringUTF,被坑过
  • L/java/lang/String;
  • java/util/Listjava/util/ArrayList是不一样的。。。要看清方法的参数。。。