引言

最近在做项目的时候,接触到JNI,想一想自己第一次接触这个东西的时候,还是好久之前,现在既然接触到了,那我就简单的跟大家讲一讲JNI的基本使用方法。

JNI(Java Native Interface):java本地开发接口,JNI是一个协议,这个协议用来沟通java代码和外部的本地代码(c/c++),外部的c/c++代码也可以调用java代码。

我们为什么要使用JNI呢,可以从效率和安全性两方面来说:

1. 安全性:java是版解释型语言,很容易比反编译拿到源代码,我们一些加密方面的问题,就可以用JNI来实现,

2. 效率:C/C++是本地语言,比java更高效。

做JNI,我们先的下载Android NDK(Native Development Kit )下载链接:(https://developer.android.google.cn/ndk/downloads/index.html),Android NDK是一套工具集合,允许你用像C/C++语言那样实现应用程序的一部分。

JNI和NDK的区别:

从工具上说,NDK其实多了一个把.so和.apk打包的工具,而JNI开发并没有打包,只是把.so文件放到文件系统的特定位置。

从编译库说,NDK开发C/C++只能使用NDK自带的有限的头文件,而使用JNI则可以使用文件系统中带的头文件。

从编写方式说,它们一样。

知识前瞻

Java类型和本地类型的对应关系:

Java类型

本地类型(JNI)

描述

boolean(布尔型)

jboolean

无符号8个比特

byte(字节型)

jbyte

有符号8个比特

char(字符型)

jchar

无符号16个比特

short(短整型)

jshort

有符号16个比特

int(整型)

jint

有符号32个比特

long(长整型)

jlong

有符号64个比特

float(浮点型)

jfloat

32个比特

double(双精度浮点型)

jdouble

64个比特

void(空型)

void

N/A

先就看这么多吧,网上也有很多大神写的博客,写得很好,有时间可以去看看,参考博客: ,这里我只是教大家如何使用Java与C/C++的互相调用。

实战

在这里我给大家做的例子是Java调用C语言和C语言调用Java来实现加法操作。

二话不说,开干,新建Android工程。XMl界面定义如下:

XML代码

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wiky.jnilibsdemo.MainActivity">

<TextView
    android:id="@+id/cal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="输入加数和被加数:"
    android:textSize="24dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<EditText
    android:id="@+id/et1"
    android:layout_width="50dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="120dp"
    android:layout_marginTop="30dp"
    android:gravity="center"
    android:hint="3"
    android:textColor="@android:color/holo_red_dark"
    android:textSize="24dp"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/cal" />

<TextView
    android:id="@+id/add"
    android:layout_width="30dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="+"
    android:textColor="@android:color/holo_red_dark"
    android:textSize="18dp"
    app:layout_constraintBaseline_toBaselineOf="@+id/et1"
    app:layout_constraintLeft_toRightOf="@+id/et1"
    app:layout_constraintTop_toBottomOf="@+id/cal" />

<EditText
    android:id="@+id/et2"
    android:layout_width="50dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:hint="6"
    android:textColor="@android:color/holo_red_dark"
    android:textSize="24dp"
    app:layout_constraintBaseline_toBaselineOf="@+id/et1"
    app:layout_constraintLeft_toRightOf="@+id/add"
    app:layout_constraintTop_toBottomOf="@+id/cal" />

<TextView
    android:id="@+id/equal"
    android:layout_width="30dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="="
    android:textColor="@android:color/holo_red_dark"
    android:textSize="24dp"
    app:layout_constraintBaseline_toBaselineOf="@+id/et2"
    app:layout_constraintLeft_toRightOf="@+id/et2"
    app:layout_constraintTop_toBottomOf="@+id/cal" />

<TextView
    android:id="@+id/result"
    android:layout_width="60dp"
    android:layout_height="wrap_content"
    android:gravity="left"
    android:textColor="@android:color/holo_red_dark"
    android:textSize="28dp"
    app:layout_constraintBaseline_toBaselineOf="@+id/et1"
    app:layout_constraintLeft_toRightOf="@+id/equal"
    app:layout_constraintTop_toBottomOf="@+id/cal" />

<Button
    android:id="@+id/calCMethod"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="调用C方法计算得数"
    android:textSize="24dp"
    android:layout_marginTop="30dp"
    app:layout_constraintTop_toBottomOf="@+id/result"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

<Button
    android:id="@+id/calCToJavaMethod"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="调用C方法再调用Java计算得数"
    android:layout_marginTop="30dp"
    app:layout_constraintTop_toBottomOf="@+id/calCMethod"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

</android.support.constraint.ConstraintLayout>

MainActivity代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText mEt1;
    private EditText mEt2;
    private TextView mResult;
    private int mNumber1;
    private int mNumber2;

    //加载本地C语言文件库。库名字为你写的C语言文件名
    static {
        System.loadLibrary("Test");
    }

    //调用本地C语言方法计算结果
    public native int getResult(int number1, int number2);

    //调用本地C语言方法
    public native int callCMethod(int number1, int number2);

    //本地C语言调用Java方法
    public static int calculateResult(int number1, int number2){
        return number1 + number2;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }

    private void initView(){
        mEt1 = findViewById(R.id.et1);
        mEt2 = findViewById(R.id.et2);
        mResult = findViewById(R.id.result);
        Button calCMethod = findViewById(R.id.calCMethod);
        calCMethod.setOnClickListener(this);
        Button calCToJavaMethod = findViewById(R.id.calCToJavaMethod);
        calCToJavaMethod.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.calCMethod) {
            getNumber();
            int result = getResult(mNumber1, mNumber2);
            if (mNumber1 + mNumber2 != result){
            Toast.makeText(this, "结果错误了!调用本地C语言失败!", Toast.LENGTH_LONG).show();
        }else {
            Toast.makeText(this, "结果正确!调用本地C语言成功!", Toast.LENGTH_LONG).show();
        }
        mResult.setText(String.valueOf(result));
    	}else if (id == R.id.calCToJavaMethod){
            getNumber();
            int result = callCMethod(mNumber1, mNumber2);
            if (mNumber1 + mNumber2 != result){
                Toast.makeText(this, "结果错误了!本地C语言调用Java方法失败!", Toast.LENGTH_LONG).show();
            }else {
                Toast.makeText(this, "结果正确!本地C语言调用Java方法成功!", Toast.LENGTH_LONG).show();
            }
	            mResult.setText(String.valueOf(result));
	    }

    }
    
    private void getNumber(){
        String text1 = mEt1.getText().toString().trim();
        String text2 = mEt2.getText().toString().trim();
        if (text1.equals("")) {
            text1 = mEt1.getHint().toString().trim();
        }
        if (text2.equals("")) {
            text2 = mEt2.getHint().toString().trim();
        }
        mNumber1 = Integer.parseInt(text1);
        mNumber2 = Integer.parseInt(text2);

    }
}

上述都是入门级别的代码了,除了ConstraintLayout(参考https://www.jianshu.com/p/17ec9bd6ca8a)使用较少之外,其他的so easy!不过ConstraintLayout没有使用过的小伙伴,建议大家学习和使用一下。

我们接下来将围绕这段代码展开分析

//加载本地C语言文件库。
    static {
        System.loadLibrary("Test");
    }

    //调用本地C语言方法计算结果
    public native int getResult(int number1, int number2);

    //调用本地C语言方法
    public native int callCMethod(int number1, int number2);

    //本地C语言调用Java方法
    public static int calculateResult(int number1, int number2){
        return number1 + number2;
    }

Java调用本地C语言

Java文件中定义public native int getResult(int number1, int number2)这样的一个native方法,接下来我们需要调用javah命令来自动生成本地C语言头文件(javah -v -d F:\AndroidProject\JniLibsDemo\app\src\main\jni -jni com.wiky.jnilibsdemo.jni)。javah命令使用帮助


或者你也可以使用AS扩展工具, 自定义一些命令行工具。参考https://www.jianshu.com/p/9cb8514d1ba0 生成的头文件文件如下:

这个时候我们在jni目录下新建一个Test.c的文件。
Test.c的代码如下

//引用我们生成的头文件
#include<com_wiky_jnilibsdemo_MainActivity.h>

JNIEXPORT jint JNICALL Java_com_wiky_jnilibsdemo_MainActivity_getResult
    (JNIEnv *env, jobject obj, jint number1, jint number2) {
return number1 + number2;
}

这个时候要想java调用本地语言,我们还需要添加一个Android的.mk配置文件,例如Android.mk

LOCAL_PATH       :=  $(call my-dir)
include              $(CLEAR_VARS)
LOCAL_MODULE     :=  Test        //对应加载的文件库名字(System.loadLibrary("Test"))
LOCAL_SRC_FILES  :=  Test.c      //C语言文件名
include              $(BUILD_SHARED_LIBRARY)

有的人以为这样就可以了,有没有发现我们还咩有配置Android运行本地语言的环境,而且发现我们的Android.mk文件还没用到么,点击运行,报错如下:

Error: Your project contains C++ files but it is not using a supported native build system.
Consider using CMake or ndk-build integration. For more information, go to:
 https://d.android.com/r/studio-ui/add-native-code.html
Alternatively, you can use the experimental plugin:
 https://developer.android.com/r/tools/experimental-plugin.html

这个时候我们就需要配置一下

这样我们就关联起来了,或者在模块及build.gradle配置

externalNativeBuild {
    ndkBuild {
        path file('src/main/jni/Android.mk')
    }
}

点击同步,就可以了。我们运行一下

运行OK,大功告成!

本地C语言调用Java方法

同样的操作,我们利用我们输入的数字传入到C,在C中调用java方法计算得数,

定义这两个方法

//调用本地C语言方法
    public native int callCMethod(int number1, int number2);

    //本地C语言调用Java方法
    public static int calculateResult(int number1, int number2){
        return number1 + number2;
    }

头文件中的方法定义这里就不再叙述,按照上述方法生成即可,我们接下来看Test.c文件中的实现

JNIEXPORT jint JNICALL Java_com_wiky_jnilibsdemo_MainActivity_callCMethod
    (JNIEnv *env, jobject obj, jint number1, jint number2) {
    char *classname = "com/wiky/jnilibsdemo/MainActivity";
    jclass jClazz = (*env)->FindClass(env, classname);
    //这里实现了互相调用,Java中调用了C的getStringFormc方法传递了x,y参数,这里C又调用了Java的add方法将x,y回传回去求和;
    jmethodID methodID = (*env)->GetStaticMethodID(env, jClazz, "calculateResult", "(II)I");
    jint result = (*env)->CallStaticIntMethod(env, jClazz, methodID, number1, number2);
    return result;
}

这段代码的就是利用反射获取到Java中的方法,然后去调用。在这里我要讲的是GetStaticMethodID方法中的第四个参数"(II)I",这个代表的是调用Java方法的的方法签名,关于签名,如下:

java类型

Signature

备注

boolean

Z

byte

B

char

C

short

S

int

I

long

L

float

F

double

D

void V object L用/分割的完整类名 例如: Ljava/lang/String表示String类型 Array [签名 例如: [I表示int数组, [Ljava/lang/String表示String数组 Method (参数签名)返回类型签名 例如: ([I)I表示参数类型为int数组, 返回int类型的方法

如本例中的函数声明:

JNIEXPORT jint JNICALL Java_J2C_write2proc(JNIEnv *, jobject, jint);

注释中的签名是 Signature: (I)I

通过上述的表格之后我们很自然的清楚上述代码的意思,点击运行

看到这里,比较欣喜,真正的打完收工。本人C++的已经忘记了,所以。。。。。。

扩展

如果你需要将本地语言移植到另一个项目,你可以考虑将本地语言编译成.so文件,首先添加.mk文件,例如Application.mk文件(参考)

APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := all
APP_PLATFORM := android-8

利用AS扩展工具, 自定义ndk-build命令,运行命令就可以看到.so文件了,或者cd到Application.mk的文件所在目录,输入ndk-build运行,这个命令在你的ndk文件夹中,记得配置环境变量。

总结

这些只是简单的JNI的开发使用,真正的实现,可以去参考大神们的博客。上述如有问题,可以给我留言,欢迎大家批评指正!谢谢大家!